ndarray_histogram/histogram/
strategies.rs

1//! Strategies used by [`GridBuilder`] to infer optimal parameters from data for building [`Bins`]
2//! and [`Grid`] instances.
3//!
4//! The docs for each strategy have been taken almost verbatim from [`NumPy`].
5//!
6//! Each strategy specifies how to compute the optimal number of [`Bins`] or the optimal bin width.
7//! For those strategies that prescribe the optimal number of [`Bins`], the optimal bin width is
8//! computed by `bin_width = (max - min)/n`.
9//!
10//! Since all bins are left-closed and right-open, it is guaranteed to add an extra bin to include
11//! the maximum value from the given data when necessary, so that no data is discarded.
12//!
13//! # Strategies
14//!
15//! Currently, the following strategies are implemented:
16//!
17//! - [`Auto`]: Maximum of the [`Sturges`] and [`FreedmanDiaconis`] strategies. Provides good all
18//!   around performance.
19//! - [`FreedmanDiaconis`]: Robust (resilient to outliers) strategy that takes into account data
20//!   variability and data size.
21//! - [`Rice`]: A strategy that does not take variability into account, only data size. Commonly
22//!   overestimates number of bins required.
23//! - [`Sqrt`]: Square root (of data size) strategy, used by Excel and other programs
24//!   for its speed and simplicity.
25//! - [`Sturges`]: R’s default strategy, only accounts for data size. Only optimal for gaussian data
26//!   and underestimates number of bins for large non-gaussian datasets.
27//!
28//! # Notes
29//!
30//! In general, successful inference on optimal bin width and number of bins relies on
31//! **variability** of data. In other word, the provided observations should not be empty or
32//! constant.
33//!
34//! In addition, [`Auto`] and [`FreedmanDiaconis`] requires the [`interquartile range (IQR)`][iqr],
35//! i.e. the difference between upper and lower quartiles, to be positive.
36//!
37//! [`GridBuilder`]: ../struct.GridBuilder.html
38//! [`Bins`]: ../struct.Bins.html
39//! [`Grid`]: ../struct.Grid.html
40//! [`NumPy`]: https://docs.scipy.org/doc/numpy/reference/generated/numpy.histogram_bin_edges.html#numpy.histogram_bin_edges
41//! [`Auto`]: struct.Auto.html
42//! [`Sturges`]: struct.Sturges.html
43//! [`FreedmanDiaconis`]: struct.FreedmanDiaconis.html
44//! [`Rice`]: struct.Rice.html
45//! [`Sqrt`]: struct.Sqrt.html
46//! [iqr]: https://www.wikiwand.com/en/Interquartile_range
47
48use crate::{
49	histogram::{Bins, Edges, errors::BinsBuildError},
50	quantile::{Quantile1dExt, QuantileExt, interpolate::Nearest},
51};
52use ndarray::{Data, prelude::*};
53use num_traits::{FromPrimitive, NumOps, ToPrimitive, Zero};
54
55/// A trait implemented by all strategies to build [`Bins`] with parameters inferred from
56/// observations.
57///
58/// This is required by [`GridBuilder`] to know how to build a [`Grid`]'s projections on the
59/// coordinate axes.
60///
61/// [`Bins`]: ../struct.Bins.html
62/// [`GridBuilder`]: ../struct.GridBuilder.html
63/// [`Grid`]: ../struct.Grid.html
64pub trait BinsBuildingStrategy {
65	#[allow(missing_docs)]
66	type Elem: Ord + Send;
67	/// Returns a strategy that has learnt the required parameter for building [`Bins`] for given
68	/// 1-dimensional array, or an `Err` if it is not possible to infer the required parameter
69	/// with the given data and specified strategy.
70	///
71	/// Calls [`Self::from_array_with_max`] with `max_n_bins` of [`u16::MAX`].
72	///
73	/// # Errors
74	///
75	/// See each of the `struct`-level documentation for details on errors an implementation may
76	/// return.
77	///
78	/// [`Bins`]: ../struct.Bins.html
79	fn from_array<S>(array: &ArrayBase<S, Ix1>) -> Result<Self, BinsBuildError>
80	where
81		S: Data<Elem = Self::Elem>,
82		Self: std::marker::Sized,
83	{
84		Self::from_array_with_max(array, u16::MAX.into())
85	}
86
87	/// Returns a strategy that has learnt the required parameter for building [`Bins`] for given
88	/// 1-dimensional array, or an `Err` if it is not possible to infer the required parameter
89	/// with the given data and specified strategy.
90	///
91	/// # Errors
92	///
93	/// See each of the `struct`-level documentation for details on errors an implementation may
94	/// return. Fails if the strategy requires more bins than `max_n_bins`.
95	///
96	/// [`Bins`]: ../struct.Bins.html
97	fn from_array_with_max<S>(
98		array: &ArrayBase<S, Ix1>,
99		max_n_bins: usize,
100	) -> Result<Self, BinsBuildError>
101	where
102		S: Data<Elem = Self::Elem>,
103		Self: std::marker::Sized;
104
105	/// Returns a [`Bins`] instance, according to parameters inferred from observations.
106	///
107	/// [`Bins`]: ../struct.Bins.html
108	fn build(&self) -> Bins<Self::Elem>;
109
110	/// Returns the optimal number of bins, according to parameters inferred from observations.
111	fn n_bins(&self) -> usize;
112}
113
114#[derive(Debug)]
115struct EquiSpaced<T> {
116	bin_width: T,
117	min: T,
118	max: T,
119}
120
121/// Square root (of data size) strategy, used by Excel and other programs for its speed and
122/// simplicity.
123///
124/// Let `n` be the number of observations. Then
125///
126/// `n_bins` = `sqrt(n)`
127///
128/// # Notes
129///
130/// This strategy requires the data
131///
132/// - not being empty
133/// - not being constant
134#[derive(Debug)]
135pub struct Sqrt<T> {
136	builder: EquiSpaced<T>,
137}
138
139/// A strategy that does not take variability into account, only data size. Commonly
140/// overestimates number of bins required.
141///
142/// Let `n` be the number of observations and `n_bins` be the number of bins.
143///
144/// `n_bins` = 2`n`<sup>1/3</sup>
145///
146/// `n_bins` is only proportional to cube root of `n`. It tends to overestimate
147/// the `n_bins` and it does not take into account data variability.
148///
149/// # Notes
150///
151/// This strategy requires the data
152///
153/// - not being empty
154/// - not being constant
155#[derive(Debug)]
156pub struct Rice<T> {
157	builder: EquiSpaced<T>,
158}
159
160/// R’s default strategy, only accounts for data size. Only optimal for gaussian data and
161/// underestimates number of bins for large non-gaussian datasets.
162///
163/// Let `n` be the number of observations.
164/// The number of bins is 1 plus the base 2 log of `n`. This estimator assumes normality of data and
165/// is too conservative for larger, non-normal datasets.
166///
167/// This is the default method in R’s hist method.
168///
169/// # Notes
170///
171/// This strategy requires the data
172///
173/// - not being empty
174/// - not being constant
175#[derive(Debug)]
176pub struct Sturges<T> {
177	builder: EquiSpaced<T>,
178}
179
180/// Robust (resilient to outliers) strategy that takes into account data variability and data size.
181///
182/// Let `n` be the number of observations and `at = 1 / 4`.
183///
184/// `bin_width` = `IQR` × `n`<sup>−1/3</sup> / (1 - 2 × `at`)
185///
186/// The bin width is proportional to the interquartile range ([`IQR`]) from `at` to `1 - at` and
187/// inversely proportional to cube root of `n`. It can be too conservative for small datasets, but
188/// it is quite good for large datasets. In case the [`IQR`] is close to zero, `at` is halved and an
189/// improper [`IQR`] is computed. This is repeated as long as `at >= 1 / 512`. If no [`IQR`] is
190/// found by then, Scott's rule is used as asymptotic resort which is based on the standard
191/// deviation (SD). If the SD is close to zero as well, this strategy fails with
192/// [`BinsBuildError::Strategy`]. As there is no one-fit-all epsilon, whether the IQR or standard
193/// deviation is close to zero is indirectly tested by requiring the computed number of bins to not
194/// exceed `max_n_bins` with a default of [`u16::MAX`].
195///
196/// The [`IQR`] is very robust to outliers.
197///
198/// # Notes
199///
200/// This strategy requires the data
201///
202/// - not being empty
203/// - not being constant
204/// - having positive [`IQR`]
205///
206/// [`IQR`]: https://en.wikipedia.org/wiki/Interquartile_range
207#[derive(Debug)]
208pub struct FreedmanDiaconis<T> {
209	builder: EquiSpaced<T>,
210}
211
212#[derive(Debug)]
213enum SturgesOrFD<T> {
214	Sturges(Sturges<T>),
215	FreedmanDiaconis(FreedmanDiaconis<T>),
216}
217
218/// Maximum of the [`Sturges`] and [`FreedmanDiaconis`] strategies. Provides good all around
219/// performance.
220///
221/// A compromise to get a good value. For small datasets the [`Sturges`] value will usually be
222/// chosen, while larger datasets will usually default to [`FreedmanDiaconis`]. Avoids the overly
223/// conservative behaviour of [`FreedmanDiaconis`] and [`Sturges`] for small and large datasets
224/// respectively.
225///
226/// # Notes
227///
228/// This strategy requires the data
229///
230/// - not being empty
231/// - not being constant
232/// - having positive [`IQR`]
233///
234/// [`Sturges`]: struct.Sturges.html
235/// [`FreedmanDiaconis`]: struct.FreedmanDiaconis.html
236/// [`IQR`]: https://en.wikipedia.org/wiki/Interquartile_range
237#[derive(Debug)]
238pub struct Auto<T> {
239	builder: SturgesOrFD<T>,
240}
241
242impl<T> EquiSpaced<T>
243where
244	T: Ord + Send + Clone + FromPrimitive + ToPrimitive + NumOps + Zero,
245{
246	/// Returns `Err(BinsBuildError::Strategy)` if `bin_width<=0` or `min` >= `max`.
247	/// Returns `Ok(Self)` otherwise.
248	fn new(bin_width: T, min: T, max: T) -> Result<Self, BinsBuildError> {
249		if (bin_width <= T::zero()) || (min >= max) {
250			Err(BinsBuildError::Strategy)
251		} else {
252			Ok(Self {
253				bin_width,
254				min,
255				max,
256			})
257		}
258	}
259
260	fn build(&self) -> Bins<T> {
261		let n_bins = self.n_bins();
262		let mut edges: Vec<T> = vec![];
263		for i in 0..=n_bins {
264			let edge = self.min.clone() + T::from_usize(i).unwrap() * self.bin_width.clone();
265			edges.push(edge);
266		}
267		Bins::new(Edges::from(edges))
268	}
269
270	fn n_bins(&self) -> usize {
271		let min = self.min.to_f64().unwrap();
272		let max = self.max.to_f64().unwrap();
273		let bin_width = self.bin_width.to_f64().unwrap();
274		usize::from_f64(((max - min) / bin_width + 0.5).ceil()).unwrap_or(usize::MAX)
275	}
276
277	fn bin_width(&self) -> T {
278		self.bin_width.clone()
279	}
280}
281
282impl<T> BinsBuildingStrategy for Sqrt<T>
283where
284	T: Ord + Send + Clone + FromPrimitive + ToPrimitive + NumOps + Zero,
285{
286	type Elem = T;
287
288	/// Returns `Err(BinsBuildError::Strategy)` if the array is constant.
289	/// Returns `Err(BinsBuildError::EmptyInput)` if `a.len()==0`.
290	/// Returns `Ok(Self)` otherwise.
291	fn from_array_with_max<S>(
292		a: &ArrayBase<S, Ix1>,
293		max_n_bins: usize,
294	) -> Result<Self, BinsBuildError>
295	where
296		S: Data<Elem = Self::Elem>,
297	{
298		let n_elems = a.len();
299		// casting `n_elems: usize` to `f64` may casus off-by-one error here if `n_elems` > 2 ^ 53,
300		// but it's not relevant here
301		#[allow(clippy::cast_precision_loss)]
302		// casting the rounded square root from `f64` to `usize` is safe
303		#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
304		let n_bins = (n_elems as f64).sqrt().round() as usize;
305		let min = a.min()?;
306		let max = a.max()?;
307		let bin_width = compute_bin_width(min.clone(), max.clone(), n_bins);
308		let builder = EquiSpaced::new(bin_width, min.clone(), max.clone())?;
309		if builder.n_bins() > max_n_bins {
310			Err(BinsBuildError::Strategy)
311		} else {
312			Ok(Self { builder })
313		}
314	}
315
316	fn build(&self) -> Bins<T> {
317		self.builder.build()
318	}
319
320	fn n_bins(&self) -> usize {
321		self.builder.n_bins()
322	}
323}
324
325impl<T> Sqrt<T>
326where
327	T: Ord + Send + Clone + FromPrimitive + ToPrimitive + NumOps + Zero,
328{
329	/// The bin width (or bin length) according to the fitted strategy.
330	pub fn bin_width(&self) -> T {
331		self.builder.bin_width()
332	}
333}
334
335impl<T> BinsBuildingStrategy for Rice<T>
336where
337	T: Ord + Send + Clone + FromPrimitive + ToPrimitive + NumOps + Zero,
338{
339	type Elem = T;
340
341	/// Returns `Err(BinsBuildError::Strategy)` if the array is constant.
342	/// Returns `Err(BinsBuildError::EmptyInput)` if `a.len()==0`.
343	/// Returns `Ok(Self)` otherwise.
344	fn from_array_with_max<S>(
345		a: &ArrayBase<S, Ix1>,
346		max_n_bins: usize,
347	) -> Result<Self, BinsBuildError>
348	where
349		S: Data<Elem = Self::Elem>,
350	{
351		let n_elems = a.len();
352		// casting `n_elems: usize` to `f64` may casus off-by-one error here if `n_elems` > 2 ^ 53,
353		// but it's not relevant here
354		#[allow(clippy::cast_precision_loss)]
355		// casting the rounded cube root from `f64` to `usize` is safe
356		#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
357		let n_bins = (2. * (n_elems as f64).powf(1. / 3.)).round() as usize;
358		let min = a.min()?;
359		let max = a.max()?;
360		let bin_width = compute_bin_width(min.clone(), max.clone(), n_bins);
361		let builder = EquiSpaced::new(bin_width, min.clone(), max.clone())?;
362		if builder.n_bins() > max_n_bins {
363			Err(BinsBuildError::Strategy)
364		} else {
365			Ok(Self { builder })
366		}
367	}
368
369	fn build(&self) -> Bins<T> {
370		self.builder.build()
371	}
372
373	fn n_bins(&self) -> usize {
374		self.builder.n_bins()
375	}
376}
377
378impl<T> Rice<T>
379where
380	T: Ord + Send + Clone + FromPrimitive + ToPrimitive + NumOps + Zero,
381{
382	/// The bin width (or bin length) according to the fitted strategy.
383	pub fn bin_width(&self) -> T {
384		self.builder.bin_width()
385	}
386}
387
388impl<T> BinsBuildingStrategy for Sturges<T>
389where
390	T: Ord + Send + Clone + FromPrimitive + ToPrimitive + NumOps + Zero,
391{
392	type Elem = T;
393
394	/// Returns `Err(BinsBuildError::Strategy)` if the array is constant.
395	/// Returns `Err(BinsBuildError::EmptyInput)` if `a.len()==0`.
396	/// Returns `Ok(Self)` otherwise.
397	fn from_array_with_max<S>(
398		a: &ArrayBase<S, Ix1>,
399		max_n_bins: usize,
400	) -> Result<Self, BinsBuildError>
401	where
402		S: Data<Elem = Self::Elem>,
403	{
404		let n_elems = a.len();
405		// casting `n_elems: usize` to `f64` may casus off-by-one error here if `n_elems` > 2 ^ 53,
406		// but it's not relevant here
407		#[allow(clippy::cast_precision_loss)]
408		// casting the rounded base-2 log from `f64` to `usize` is safe
409		#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
410		let n_bins = (n_elems as f64).log2().round() as usize + 1;
411		let min = a.min()?;
412		let max = a.max()?;
413		let bin_width = compute_bin_width(min.clone(), max.clone(), n_bins);
414		let builder = EquiSpaced::new(bin_width, min.clone(), max.clone())?;
415		if builder.n_bins() > max_n_bins {
416			Err(BinsBuildError::Strategy)
417		} else {
418			Ok(Self { builder })
419		}
420	}
421
422	fn build(&self) -> Bins<T> {
423		self.builder.build()
424	}
425
426	fn n_bins(&self) -> usize {
427		self.builder.n_bins()
428	}
429}
430
431impl<T> Sturges<T>
432where
433	T: Ord + Send + Clone + FromPrimitive + ToPrimitive + NumOps + Zero,
434{
435	/// The bin width (or bin length) according to the fitted strategy.
436	pub fn bin_width(&self) -> T {
437		self.builder.bin_width()
438	}
439}
440
441impl<T> BinsBuildingStrategy for FreedmanDiaconis<T>
442where
443	T: Ord + Send + Clone + FromPrimitive + ToPrimitive + NumOps + Zero,
444{
445	type Elem = T;
446
447	/// Returns `Err(BinsBuildError::Strategy)` if improper IQR and SD are close to zero.
448	/// Returns `Err(BinsBuildError::EmptyInput)` if `a.len()==0`.
449	/// Returns `Ok(Self)` otherwise.
450	fn from_array<S>(a: &ArrayBase<S, Ix1>) -> Result<Self, BinsBuildError>
451	where
452		S: Data<Elem = Self::Elem>,
453	{
454		Self::from_array_with_max(a, u16::MAX.into())
455	}
456
457	/// Returns `Err(BinsBuildError::Strategy)` if improper IQR and SD are close to zero.
458	/// Returns `Err(BinsBuildError::EmptyInput)` if `a.len()==0`.
459	/// Returns `Ok(Self)` otherwise.
460	fn from_array_with_max<S>(
461		a: &ArrayBase<S, Ix1>,
462		max_n_bins: usize,
463	) -> Result<Self, BinsBuildError>
464	where
465		S: Data<Elem = Self::Elem>,
466	{
467		let n_points = a.len();
468		if n_points == 0 {
469			return Err(BinsBuildError::EmptyInput);
470		}
471
472		let n_cbrt = f64::from_usize(n_points).unwrap().powf(1. / 3.);
473		let min = a.min()?;
474		let max = a.max()?;
475		let mut a_copy = a.to_owned();
476		// As there is no one-fit-all epsilon to decide whether IQR is zero, translate it into
477		// number of bins and compare it against `max_n_bins`. More bins than `max_n_bins` is a hint
478		// for an IQR close to zero. If so, deviate from proper Freedman-Diaconis rule by widening
479		// percentiles range and try again with `at` of 1/8, 1/16, 1/32, 1/64, 1/128, 1/256, 1/512.
480		let mut at = 0.5;
481		while at >= 1. / 512. {
482			at *= 0.5;
483			let first_quartile = a_copy.quantile_mut(at, &Nearest).unwrap();
484			let third_quartile = a_copy.quantile_mut(1. - at, &Nearest).unwrap();
485			let iqr = third_quartile - first_quartile;
486			let denom = T::from_f64((1. - 2. * at) * n_cbrt).unwrap();
487			if denom == T::zero() {
488				continue;
489			}
490			let bin_width = iqr.clone() / denom;
491			let builder = EquiSpaced::new(bin_width, min.clone(), max.clone())?;
492			if builder.n_bins() > max_n_bins {
493				continue;
494			}
495			return Ok(Self { builder });
496		}
497		// If the improper IQR is still close to zero, use Scott's rule as asymptotic resort before
498		// giving up where `m` is the mean and `s` its SD.
499		let m = a.iter().cloned().fold(T::zero(), |s, v| s + v) / T::from_usize(n_points).unwrap();
500		let s = a
501			.iter()
502			.cloned()
503			.map(|v| (v.clone() - m.clone()) * (v - m.clone()))
504			.fold(T::zero(), |s, v| s + v);
505		let s = (s / T::from_usize(n_points - 1).unwrap())
506			.to_f64()
507			.unwrap()
508			.sqrt();
509		let bin_width = T::from_f64(3.49 * s).unwrap() / T::from_f64(n_cbrt).unwrap();
510		let builder = EquiSpaced::new(bin_width, min.clone(), max.clone())?;
511		if builder.n_bins() > max_n_bins {
512			return Err(BinsBuildError::Strategy);
513		}
514		Ok(Self { builder })
515	}
516
517	fn build(&self) -> Bins<T> {
518		self.builder.build()
519	}
520
521	fn n_bins(&self) -> usize {
522		self.builder.n_bins()
523	}
524}
525
526impl<T> FreedmanDiaconis<T>
527where
528	T: Ord + Send + Clone + FromPrimitive + ToPrimitive + NumOps + Zero,
529{
530	/// The bin width (or bin length) according to the fitted strategy.
531	pub fn bin_width(&self) -> T {
532		self.builder.bin_width()
533	}
534}
535
536impl<T> BinsBuildingStrategy for Auto<T>
537where
538	T: Ord + Send + Clone + FromPrimitive + ToPrimitive + NumOps + Zero,
539{
540	type Elem = T;
541
542	/// Returns `Err(BinsBuildError::Strategy)` if `IQR==0`.
543	/// Returns `Err(BinsBuildError::EmptyInput)` if `a.len()==0`.
544	/// Returns `Ok(Self)` otherwise.
545	fn from_array_with_max<S>(
546		a: &ArrayBase<S, Ix1>,
547		max_n_bins: usize,
548	) -> Result<Self, BinsBuildError>
549	where
550		S: Data<Elem = Self::Elem>,
551	{
552		let fd_builder = FreedmanDiaconis::from_array_with_max(a, max_n_bins);
553		let sturges_builder = Sturges::from_array_with_max(a, max_n_bins);
554		match (fd_builder, sturges_builder) {
555			(Err(_), Ok(sturges_builder)) => {
556				let builder = SturgesOrFD::Sturges(sturges_builder);
557				Ok(Self { builder })
558			}
559			(Ok(fd_builder), Err(_)) => {
560				let builder = SturgesOrFD::FreedmanDiaconis(fd_builder);
561				Ok(Self { builder })
562			}
563			(Ok(fd_builder), Ok(sturges_builder)) => {
564				let builder = if fd_builder.bin_width() > sturges_builder.bin_width() {
565					SturgesOrFD::Sturges(sturges_builder)
566				} else {
567					SturgesOrFD::FreedmanDiaconis(fd_builder)
568				};
569				Ok(Self { builder })
570			}
571			(Err(err), Err(_)) => Err(err),
572		}
573	}
574
575	fn build(&self) -> Bins<T> {
576		// Ugly
577		match &self.builder {
578			SturgesOrFD::FreedmanDiaconis(b) => b.build(),
579			SturgesOrFD::Sturges(b) => b.build(),
580		}
581	}
582
583	fn n_bins(&self) -> usize {
584		// Ugly
585		match &self.builder {
586			SturgesOrFD::FreedmanDiaconis(b) => b.n_bins(),
587			SturgesOrFD::Sturges(b) => b.n_bins(),
588		}
589	}
590}
591
592impl<T> Auto<T>
593where
594	T: Ord + Send + Clone + FromPrimitive + ToPrimitive + NumOps + Zero,
595{
596	/// The bin width (or bin length) according to the fitted strategy.
597	pub fn bin_width(&self) -> T {
598		// Ugly
599		match &self.builder {
600			SturgesOrFD::FreedmanDiaconis(b) => b.bin_width(),
601			SturgesOrFD::Sturges(b) => b.bin_width(),
602		}
603	}
604}
605
606/// Returns the `bin_width`, given the two end points of a range (`max`, `min`), and the number of
607/// bins, consuming endpoints
608///
609/// `bin_width = (max - min)/n`
610///
611/// **Panics** if `n_bins == 0` and division by 0 panics for `T`.
612fn compute_bin_width<T>(min: T, max: T, n_bins: usize) -> T
613where
614	T: Ord + Send + Clone + FromPrimitive + ToPrimitive + NumOps + Zero,
615{
616	let range = max - min;
617	range / T::from_usize(n_bins).unwrap()
618}
619
620#[cfg(test)]
621mod equispaced_tests {
622	use super::EquiSpaced;
623
624	#[test]
625	fn bin_width_has_to_be_positive() {
626		assert!(EquiSpaced::new(0, 0, 200).is_err());
627	}
628
629	#[test]
630	fn min_has_to_be_strictly_smaller_than_max() {
631		assert!(EquiSpaced::new(10, 0, 0).is_err());
632	}
633}
634
635#[cfg(test)]
636mod sqrt_tests {
637	use super::{BinsBuildingStrategy, Sqrt};
638	use ndarray::array;
639
640	#[test]
641	fn constant_array_are_bad() {
642		assert!(
643			Sqrt::from_array(&array![1, 1, 1, 1, 1, 1, 1])
644				.unwrap_err()
645				.is_strategy()
646		);
647	}
648
649	#[test]
650	fn empty_arrays_are_bad() {
651		assert!(
652			Sqrt::<usize>::from_array(&array![])
653				.unwrap_err()
654				.is_empty_input()
655		);
656	}
657}
658
659#[cfg(test)]
660mod rice_tests {
661	use super::{BinsBuildingStrategy, Rice};
662	use ndarray::array;
663
664	#[test]
665	fn constant_array_are_bad() {
666		assert!(
667			Rice::from_array(&array![1, 1, 1, 1, 1, 1, 1])
668				.unwrap_err()
669				.is_strategy()
670		);
671	}
672
673	#[test]
674	fn empty_arrays_are_bad() {
675		assert!(
676			Rice::<usize>::from_array(&array![])
677				.unwrap_err()
678				.is_empty_input()
679		);
680	}
681}
682
683#[cfg(test)]
684mod sturges_tests {
685	use super::{BinsBuildingStrategy, Sturges};
686	use ndarray::array;
687
688	#[test]
689	fn constant_array_are_bad() {
690		assert!(
691			Sturges::from_array(&array![1, 1, 1, 1, 1, 1, 1])
692				.unwrap_err()
693				.is_strategy()
694		);
695	}
696
697	#[test]
698	fn empty_arrays_are_bad() {
699		assert!(
700			Sturges::<usize>::from_array(&array![])
701				.unwrap_err()
702				.is_empty_input()
703		);
704	}
705}
706
707#[cfg(test)]
708mod fd_tests {
709	use super::{BinsBuildingStrategy, FreedmanDiaconis};
710	use ndarray::array;
711
712	#[test]
713	fn constant_array_are_bad() {
714		assert!(
715			FreedmanDiaconis::from_array(&array![1, 1, 1, 1, 1, 1, 1])
716				.unwrap_err()
717				.is_strategy()
718		);
719	}
720
721	#[test]
722	fn zero_iqr_is_bad() {
723		assert!(
724			FreedmanDiaconis::from_array(&array![-20, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 20])
725				.unwrap_err()
726				.is_strategy()
727		);
728	}
729
730	#[test]
731	fn empty_arrays_are_bad() {
732		assert!(
733			FreedmanDiaconis::<usize>::from_array(&array![])
734				.unwrap_err()
735				.is_empty_input()
736		);
737	}
738}
739
740#[cfg(test)]
741mod auto_tests {
742	use super::{Auto, BinsBuildingStrategy};
743	use ndarray::array;
744
745	#[test]
746	fn constant_array_are_bad() {
747		assert!(
748			Auto::from_array(&array![1, 1, 1, 1, 1, 1, 1])
749				.unwrap_err()
750				.is_strategy()
751		);
752	}
753
754	#[test]
755	fn zero_iqr_is_handled_by_sturged() {
756		assert!(Auto::from_array(&array![-20, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 20]).is_ok());
757	}
758
759	#[test]
760	fn empty_arrays_are_bad() {
761		assert!(
762			Auto::<usize>::from_array(&array![])
763				.unwrap_err()
764				.is_empty_input()
765		);
766	}
767}