simd_kernels/kernels/
aggregate.rs

1// Copyright Peter Bower 2025. All Rights Reserved.
2// Licensed under Mozilla Public License (MPL) 2.0.
3
4//! # **Aggregation Kernels Module** - *High-Performance Statistical Reduction Operations*
5//!
6//! Null-aware aggregation kernels implementing statistical reductions with SIMD acceleration.
7//! Core building blocks for analytical workloads requiring high-throughput data summarisation.
8//!
9//! ## Core Operations
10//! - **Sum aggregation**: Optimised summation with overflow handling and numerical stability
11//! - **Min/Max operations**: Efficient extrema detection with proper null handling
12//! - **Count operations**: Fast counting with distinct value detection capabilities
13//! - **Statistical moments**: Mean, variance, and standard deviation calculations
14//! - **Percentile calculations**: Quantile computation with interpolation support
15//! - **String aggregation**: Concatenation and string-based reduction operations
16include!(concat!(env!("OUT_DIR"), "/simd_lanes.rs"));
17
18#[cfg(not(feature = "fast_hash"))]
19use std::collections::HashMap;
20use std::collections::HashSet;
21use std::hash::Hash;
22use std::ops::{Mul, Sub};
23#[cfg(feature = "simd")]
24use std::simd::{
25    Mask, Simd, SimdElement,
26    cmp::SimdOrd,
27    num::{SimdFloat, SimdInt, SimdUint},
28};
29
30#[cfg(feature = "fast_hash")]
31use ahash::AHashMap;
32use minarrow::{Bitmask, Vec64};
33use num_traits::{Float, NumCast, One, ToPrimitive, Zero};
34
35use crate::kernels::sort::{sort_float, total_cmp_f};
36use crate::traits::dense_iter::collect_valid;
37use crate::traits::to_bits::ToBits;
38use crate::utils::has_nulls;
39use minarrow::utils::is_simd_aligned;
40
41#[cfg(feature = "simd")]
42use crate::utils::bitmask_to_simd_mask;
43
44// --- SIMD/stat-moments helpers -----------------------------------------------
45
46/// Generates stat‐moment aggregation (sum, sum², count)
47/// for float types using SIMD or scalar fallback.
48macro_rules! impl_stat_moments_float {
49    ($name:ident, $ty:ty, $LANES:expr) => {
50        #[doc = concat!("Computes sum, sum-of-squares, and count for a `", stringify!($ty), "` slice.")]
51        #[doc = "Skips nulls if present. Uses SIMD if available."]
52        #[inline(always)]
53        pub fn $name(
54            data: &[$ty],
55            mask: Option<&Bitmask>,
56            null_count: Option<usize>,
57        ) -> (f64, f64, usize) {
58            let has_nulls = has_nulls(null_count, mask);
59
60            #[cfg(feature = "simd")]
61            {
62                // Check if both arrays are 64-byte aligned for SIMD
63                if is_simd_aligned(data) {
64                    type V<const L: usize> = Simd<$ty, L>;
65                    // The mask‐type for `Simd<$ty, N>` is `<$ty as SimdElement>::Mask`.
66                    type M<const L: usize> = <$ty as SimdElement>::Mask;
67
68                    const N: usize = $LANES;
69                    let len = data.len();
70                    let mut i = 0;
71                    let (mut sum, mut sum2) = (0.0_f64, 0.0_f64);
72                    let mut cnt = 0_usize;
73
74                    if !has_nulls {
75                        // dense path
76                        while i + N <= len {
77                            let v = V::<N>::from_slice(&data[i..i + N]);
78                            let dv = v.cast::<f64>();
79                            cnt += N;
80                            sum += dv.reduce_sum();
81                            sum2 += (dv * dv).reduce_sum();
82                            i += N;
83                        }
84                        // tail
85                        for &v in &data[i..] {
86                            let x = v as f64;
87                            sum += x;
88                            sum2 += x * x;
89                            cnt += 1;
90                        }
91                    } else {
92                        // null‐aware SIMD path
93                        let mb = mask.expect("Mask must be Some if nulls are present");
94                        let mask_bytes = mb.as_bytes();
95
96                        while i + N <= len {
97                            let v = V::<N>::from_slice(&data[i..i + N]);
98                            // extract a SIMD mask of valid lanes
99                            let lane_mask: Mask<M<N>, N> =
100                                bitmask_to_simd_mask::<N, M<N>>(mask_bytes, i, len);
101                            // zero‐out invalid lanes
102                            let zeros = V::<N>::splat(0.0 as $ty);
103                            let vv = lane_mask.select(v, zeros);
104
105                            // count valid lanes
106                            cnt += lane_mask.to_bitmask().count_ones() as usize;
107
108                            let dv = vv.cast::<f64>();
109                            sum += dv.reduce_sum();
110                            sum2 += (dv * dv).reduce_sum();
111
112                            i += N;
113                        }
114                        // scalar tail
115                        for j in i..len {
116                            if unsafe { mb.get_unchecked(j) } {
117                                let x = data[j] as f64;
118                                sum += x;
119                                sum2 += x * x;
120                                cnt += 1;
121                            }
122                        }
123                    }
124                    return (sum, sum2, cnt);
125                }
126                // Fall through to scalar path if alignment check failed
127            }
128
129            // Scalar fallback - alignment check failed
130            let (mut sum, mut sum2, mut cnt) = (0.0_f64, 0.0_f64, 0_usize);
131            if !has_nulls {
132                for &v in data {
133                    let x = v as f64;
134                    sum += x;
135                    sum2 += x * x;
136                    cnt += 1;
137                }
138            } else {
139                let mb = mask.expect("Mask must be Some if nulls are present");
140                for (i, &v) in data.iter().enumerate() {
141                    if unsafe { mb.get_unchecked(i) } {
142                        let x = v as f64;
143                        sum += x;
144                        sum2 += x * x;
145                        cnt += 1;
146                    }
147                }
148            }
149            (sum, sum2, cnt)
150        }
151    };
152}
153
154/// Generates stat-moment aggregation (sum, sum², count)
155/// for integer types using SIMD or scalar fallback.
156macro_rules! impl_stat_moments_int {
157    ($name:ident, $ty:ty, $acc:ty, $LANES:expr, $mask_ty:ty) => {
158        #[doc = concat!("Computes sum, sum-of-squares, and count for `", stringify!($ty), "` integers.")]
159        #[doc = concat!(" Uses `", stringify!($acc), "` for accumulation. Skips nulls if present.")]
160        #[inline(always)]
161        pub fn $name(
162            data: &[$ty],
163            mask: Option<&Bitmask>,
164            null_count: Option<usize>
165        ) -> (f64, f64, usize) {
166            let has_nulls = has_nulls(null_count, mask);
167
168            #[cfg(feature = "simd")]
169            {
170                // Check if both arrays are 64-byte aligned for SIMD
171                if is_simd_aligned(data) {
172                const N: usize = $LANES;
173                let mut i = 0;
174                let mut sum: $acc = 0;
175                let mut sum2: $acc = 0;
176                let mut cnt = 0usize;
177
178                if !has_nulls {
179                    while i + N <= data.len() {
180                        let v = Simd::<$ty, N>::from_slice(&data[i..i + N]);
181                        sum += v.reduce_sum() as $acc;
182                        sum2 += (v * v).reduce_sum() as $acc;
183                        cnt += N;
184                        i += N;
185                    }
186                    for &x in &data[i..] {
187                        let x = x as $acc;
188                        sum += x;
189                        sum2 += x * x;
190                        cnt += 1;
191                    }
192                } else {
193                    let mb = mask.expect("Mask must be Some if nulls are present or mask was supplied");
194                    let mask_bytes = &mb.bits;
195                    while i + N <= data.len() {
196                        let v = Simd::<$ty, N>::from_slice(&data[i..i + N]);
197                        // use signed type for mask to satisfy trait bound
198                        let lane_mask = bitmask_to_simd_mask::<N, $mask_ty>(mask_bytes, i, data.len());
199                        let vv = lane_mask.select(v, Simd::<$ty, N>::splat(0 as $ty));
200                        sum += vv.reduce_sum() as $acc;
201                        sum2 += (vv * vv).reduce_sum() as $acc;
202                        cnt += lane_mask.to_bitmask().count_ones() as usize;
203                        i += N;
204                    }
205                    for j in i..data.len() {
206                        if unsafe { mb.get_unchecked(j) } {
207                            let x = data[j] as $acc;
208                            sum += x;
209                            sum2 += x * x;
210                            cnt += 1;
211                        }
212                    }
213                }
214                    return (sum as f64, sum2 as f64, cnt);
215                }
216                // Fall through to scalar path if alignment check failed
217            }
218
219            // Scalar fallback - alignment check failed
220            let mut sum: $acc = 0;
221            let mut sum2: $acc = 0;
222            let mut cnt = 0usize;
223            if !has_nulls {
224                for &x in data.iter() {
225                    let xi = x as $acc;
226                    sum += xi;
227                    sum2 += xi * xi;
228                    cnt += 1;
229                }
230            } else {
231                let mb = mask.expect("Mask must be Some if nulls are present or mask was supplied");
232                for (i, &x) in data.iter().enumerate() {
233                    if unsafe { mb.get_unchecked(i) } {
234                        let xi = x as $acc;
235                        sum += xi;
236                        sum2 += xi * xi;
237                        cnt += 1;
238                    }
239                }
240            }
241            (sum as f64, sum2 as f64, cnt)
242        }
243    };
244}
245
246/// Implements SIMD-enabled min/max reducers
247/// for numeric slices with or without nulls.
248macro_rules! impl_reduce_min_max {
249    ($name:ident, $ty:ty, $LANES:expr, $minval:expr, $maxval:expr) => {
250        /// Finds the minimum and maximum values in a numeric slice.
251        ///
252        /// Uses SIMD operations when available for optimal performance.
253        /// Handles null values through optional bitmask.
254        ///
255        /// # Arguments
256        ///
257        /// * `data` - The slice of numeric values to process
258        /// * `mask` - Optional bitmask indicating valid/invalid elements
259        /// * `null_count` - Optional count of null values for optimisation
260        ///
261        /// # Returns
262        ///
263        /// `Some((min, max))` if any valid values exist, `None` if all values are null
264        #[inline(always)]
265        pub fn $name(
266            data: &[$ty],
267            mask: Option<&Bitmask>,
268            null_count: Option<usize>,
269        ) -> Option<($ty, $ty)> {
270            let has_nulls = has_nulls(null_count, mask);
271            let len = data.len();
272            if len == 0 {
273                return None;
274            }
275
276            #[cfg(feature = "simd")]
277            {
278                // Check if both arrays are 64-byte aligned for SIMD
279                if is_simd_aligned(data) {
280                    type V<const N: usize> = Simd<$ty, N>;
281                    type M<const N: usize> = <$ty as SimdElement>::Mask;
282                    const N: usize = $LANES;
283
284                    let mut first_valid = None;
285                    if !has_nulls {
286                        // Find first value
287                        if len == 0 {
288                            return None;
289                        }
290                        first_valid = Some((0, data[0]));
291                    } else {
292                        let mb = mask.expect("Mask must be Some if nulls are present");
293                        for (idx, &x) in data.iter().enumerate() {
294                            if unsafe { mb.get_unchecked(idx) } {
295                                first_valid = Some((idx, x));
296                                break;
297                            }
298                        }
299                    }
300                    let (mut i, x0) = match first_valid {
301                        Some(v) => v,
302                        None => return None,
303                    };
304                    let mut min_s = x0;
305                    let mut max_s = x0;
306                    i += 1;
307
308                    // SIMD alignment: process until next SIMD-aligned index
309                    while i < len && (i % N != 0) {
310                        let x = data[i];
311                        if !has_nulls
312                            || (has_nulls && {
313                                let mb = mask.unwrap();
314                                unsafe { mb.get_unchecked(i) }
315                            })
316                        {
317                            min_s = min_s.min(x);
318                            max_s = max_s.max(x);
319                        }
320                        i += 1;
321                    }
322
323                    let mut min_v = V::<N>::splat(min_s);
324                    let mut max_v = V::<N>::splat(max_s);
325
326                    if !has_nulls {
327                        while i + N <= len {
328                            let v = V::<N>::from_slice(&data[i..i + N]);
329                            min_v = min_v.simd_min(v);
330                            max_v = max_v.simd_max(v);
331                            i += N;
332                        }
333                    } else {
334                        let mb = mask.unwrap();
335                        let mask_bytes = mb.as_bytes();
336                        while i + N <= len {
337                            let v = V::<N>::from_slice(&data[i..i + N]);
338                            let lane_m: Mask<M<N>, N> =
339                                bitmask_to_simd_mask::<N, M<N>>(mask_bytes, i, len);
340                            // Null entries are replaced by previous running min/max
341                            let v_min = lane_m.select(v, V::<N>::splat(min_s));
342                            let v_max = lane_m.select(v, V::<N>::splat(max_s));
343                            min_v = min_v.simd_min(v_min);
344                            max_v = max_v.simd_max(v_max);
345                            i += N;
346                        }
347                    }
348
349                    for idx in i..len {
350                        let x = data[idx];
351                        if !has_nulls
352                            || (has_nulls && {
353                                let mb = mask.unwrap();
354                                unsafe { mb.get_unchecked(idx) }
355                            })
356                        {
357                            min_s = min_s.min(x);
358                            max_s = max_s.max(x);
359                        }
360                    }
361
362                    min_s = min_s.min(min_v.reduce_min());
363                    max_s = max_s.max(max_v.reduce_max());
364                    return Some((min_s, max_s));
365                }
366                // Fall through to scalar path if alignment check failed
367            }
368
369            // Scalar fallback - alignment check failed
370            let mut min = None;
371            let mut max = None;
372            if !has_nulls {
373                for &x in data {
374                    match min {
375                        None => {
376                            min = Some(x);
377                            max = Some(x);
378                        }
379                        Some(m) => {
380                            min = Some(m.min(x));
381                            max = Some(max.unwrap().max(x));
382                        }
383                    }
384                }
385            } else {
386                let mb = mask.expect("Mask must be Some if nulls are present");
387                for (i, &x) in data.iter().enumerate() {
388                    if unsafe { mb.get_unchecked(i) } {
389                        match min {
390                            None => {
391                                min = Some(x);
392                                max = Some(x);
393                            }
394                            Some(m) => {
395                                min = Some(m.min(x));
396                                max = Some(max.unwrap().max(x));
397                            }
398                        }
399                    }
400                }
401            }
402            min.zip(max)
403        }
404    };
405}
406
407/// Implements SIMD-enabled floating-point min/max reducers
408/// for numeric slices with or without nulls, handling NaN values.
409macro_rules! impl_reduce_min_max_float {
410    ($fn_name:ident, $ty:ty, $LANES:ident, $SIMD:ident, $MASK:ident) => {
411        /// Finds the minimum and maximum values in a floating-point slice.
412        ///
413        /// Handles NaN values correctly by treating them as missing data.
414        /// Uses SIMD operations when available for optimal performance.
415        ///
416        /// # Arguments
417        ///
418        /// * `data` - The slice of floating-point values to process
419        /// * `mask` - Optional bitmask indicating valid/invalid elements
420        /// * `null_count` - Optional count of null values for optimisation
421        ///
422        /// # Returns
423        ///
424        /// `Some((min, max))` if any valid values exist, `None` if all values are null/NaN
425        #[inline(always)]
426        pub fn $fn_name(
427            data: &[$ty],
428            mask: Option<&Bitmask>,
429            null_count: Option<usize>,
430        ) -> Option<($ty, $ty)> {
431            let has_nulls = has_nulls(null_count, mask);
432            let len = data.len();
433            if len == 0 {
434                return None;
435            }
436
437            #[cfg(feature = "simd")]
438            {
439                // Check if both arrays are 64-byte aligned for SIMD
440                if is_simd_aligned(data) {
441                    const LANES: usize = $LANES;
442                    type V = $SIMD<$ty, LANES>;
443                    type M = <$ty as SimdElement>::Mask;
444
445                    // Find the first valid value (not null and not NaN)
446                    let mut first_valid = None;
447                    if !has_nulls {
448                        for (idx, &x) in data.iter().enumerate() {
449                            if !x.is_nan() {
450                                first_valid = Some((idx, x));
451                                break;
452                            }
453                        }
454                    } else {
455                        let mb = mask.expect("Bitmask must be Some if nulls are present");
456                        for (idx, &x) in data.iter().enumerate() {
457                            if unsafe { mb.get_unchecked(idx) } && !x.is_nan() {
458                                first_valid = Some((idx, x));
459                                break;
460                            }
461                        }
462                    }
463                    let (mut i, x0) = match first_valid {
464                        Some(v) => v,
465                        None => return None,
466                    };
467                    let mut min_s = x0;
468                    let mut max_s = x0;
469                    i += 1;
470
471                    // SIMD alignment for tail
472                    while i < len && (i % LANES != 0) {
473                        let x = data[i];
474                        if (!has_nulls && !x.is_nan())
475                            || (has_nulls && {
476                                let mb = mask.unwrap();
477                                unsafe { mb.get_unchecked(i) && !x.is_nan() }
478                            })
479                        {
480                            min_s = min_s.min(x);
481                            max_s = max_s.max(x);
482                        }
483                        i += 1;
484                    }
485
486                    let mut min_v = V::splat(min_s);
487                    let mut max_v = V::splat(max_s);
488
489                    if !has_nulls {
490                        while i + LANES <= len {
491                            let v = V::from_slice(&data[i..i + LANES]);
492                            let valid = !v.is_nan();
493                            let v_min = valid.select(v, V::splat(<$ty>::INFINITY));
494                            let v_max = valid.select(v, V::splat(<$ty>::NEG_INFINITY));
495                            min_v = min_v.simd_min(v_min);
496                            max_v = max_v.simd_max(v_max);
497                            i += LANES;
498                        }
499                    } else {
500                        let mb = mask.unwrap();
501                        let mask_bytes = mb.as_bytes();
502                        while i + LANES <= len {
503                            let v = V::from_slice(&data[i..i + LANES]);
504                            let valid_mask =
505                                bitmask_to_simd_mask::<LANES, M>(mask_bytes, i, len) & !v.is_nan();
506                            let v_min = valid_mask.select(v, V::splat(<$ty>::INFINITY));
507                            let v_max = valid_mask.select(v, V::splat(<$ty>::NEG_INFINITY));
508                            min_v = min_v.simd_min(v_min);
509                            max_v = max_v.simd_max(v_max);
510                            i += LANES;
511                        }
512                    }
513
514                    for idx in i..len {
515                        let x = data[idx];
516                        if (!has_nulls && !x.is_nan())
517                            || (has_nulls && {
518                                let mb = mask.unwrap();
519                                unsafe { mb.get_unchecked(idx) && !x.is_nan() }
520                            })
521                        {
522                            min_s = min_s.min(x);
523                            max_s = max_s.max(x);
524                        }
525                    }
526
527                    min_s = min_s.min(min_v.reduce_min());
528                    max_s = max_s.max(max_v.reduce_max());
529                    return Some((min_s, max_s));
530                }
531                // Fall through to scalar path if alignment check failed
532            }
533
534            // Scalar fallback - alignment check failed
535            let mut min = None;
536            let mut max = None;
537            if !has_nulls {
538                for &x in data {
539                    if !x.is_nan() {
540                        match min {
541                            None => {
542                                min = Some(x);
543                                max = Some(x);
544                            }
545                            Some(m) => {
546                                min = Some(m.min(x));
547                                max = Some(max.unwrap().max(x));
548                            }
549                        }
550                    }
551                }
552            } else {
553                let mb = mask.expect("Bitmask must be Some if nulls are present");
554                for (i, &x) in data.iter().enumerate() {
555                    if unsafe { mb.get_unchecked(i) } && !x.is_nan() {
556                        match min {
557                            None => {
558                                min = Some(x);
559                                max = Some(x);
560                            }
561                            Some(m) => {
562                                min = Some(m.min(x));
563                                max = Some(max.unwrap().max(x));
564                            }
565                        }
566                    }
567                }
568            }
569            min.zip(max)
570        }
571    };
572}
573
574/// Dispatches to the right stat-moment function by type.
575macro_rules! stat_moments {
576    (f64 => $d:expr, $m:expr, $hn:expr) => {
577        stat_moments_f64($d, $m, $hn)
578    };
579    (f32 => $d:expr, $m:expr, $hn:expr) => {
580        stat_moments_f32($d, $m, $hn)
581    };
582    (i64 => $d:expr, $m:expr, $hn:expr) => {
583        stat_moments_i64($d, $m, $hn)
584    };
585    (u64 => $d:expr, $m:expr, $hn:expr) => {
586        stat_moments_u64($d, $m, $hn)
587    };
588    (i32 => $d:expr, $m:expr, $hn:expr) => {
589        stat_moments_i32($d, $m, $hn)
590    };
591    (u32 => $d:expr, $m:expr, $hn:expr) => {
592        stat_moments_u32($d, $m, $hn)
593    };
594}
595
596/// Generates a variance function using stat moments.
597/// Generates both population and sample variance for a numeric type.
598macro_rules! impl_variance {
599    ($ty:ident, $fn_var:ident) => {
600        #[doc = concat!("Returns the variance for `", stringify!($ty), "`; if `sample` is true, uses Bessel's correction (n-1).")]
601        #[inline(always)]
602        pub fn $fn_var(
603            data: &[$ty],
604            mask: Option<&Bitmask>,
605            null_count: Option<usize>,
606            sample: bool,
607        ) -> Option<f64> {
608            let (s, s2, n) = stat_moments!($ty => data, mask, null_count);
609            if n == 0 || (sample && n < 2) {
610                None
611            } else if sample {
612                Some((s2 - s * s / n as f64) / (n as f64 - 1.0))
613            } else {
614                Some((s2 - s * s / n as f64) / n as f64)
615            }
616        }
617    };
618}
619
620/// Generates min, max, and (min, max) range extractors from a reduce.
621macro_rules! impl_min_max_range {
622    ($ty:ty, $reduce_fn:ident, $min_fn:ident, $max_fn:ident, $range_fn:ident) => {
623        #[doc = concat!("Returns the minimum of a `", stringify!($ty), "` slice.")]
624        #[inline(always)]
625        pub fn $min_fn(
626            data: &[$ty],
627            mask: Option<&Bitmask>,
628            null_count: Option<usize>,
629        ) -> Option<$ty> {
630            $reduce_fn(data, mask, null_count).map(|(min, _)| min)
631        }
632
633        #[doc = concat!("Returns the maximum of a `", stringify!($ty), "` slice.")]
634        #[inline(always)]
635        pub fn $max_fn(
636            data: &[$ty],
637            mask: Option<&Bitmask>,
638            null_count: Option<usize>,
639        ) -> Option<$ty> {
640            $reduce_fn(data, mask, null_count).map(|(_, max)| max)
641        }
642
643        #[doc = concat!("Returns the (min, max) of a `", stringify!($ty), "` slice.")]
644        #[inline(always)]
645        pub fn $range_fn(
646            data: &[$ty],
647            mask: Option<&Bitmask>,
648            null_count: Option<usize>,
649        ) -> Option<($ty, $ty)> {
650            $reduce_fn(data, mask, null_count)
651        }
652    };
653}
654
655// ---------- 1. SIMD helpers: SUM / COUNT / SUM² ------------------------
656
657/// Implements raw (sum, count) aggregation for float types.
658macro_rules! impl_sum_float {
659    ($name:ident, $ty:ty, $LANES:expr) => {
660        #[doc = concat!("Returns (sum, count) for non-null `", stringify!($ty), "` values.")]
661        #[doc = " Uses SIMD where available."]
662        #[inline(always)]
663        pub fn $name(
664            data: &[$ty],
665            mask: Option<&Bitmask>,
666            null_count: Option<usize>,
667        ) -> (f64, usize) {
668            let has_nulls = has_nulls(null_count, mask);
669
670            #[cfg(feature = "simd")]
671            {
672                // Check if both arrays are 64-byte aligned for SIMD
673                if is_simd_aligned(data) {
674                    const N: usize = $LANES;
675                    type V<const L: usize> = Simd<$ty, L>;
676                    type M<const L: usize> = <$ty as SimdElement>::Mask;
677
678                    let len = data.len();
679                    let mut i = 0;
680                    let mut sum = 0.0f64;
681                    let mut cnt = 0usize;
682
683                    if !has_nulls {
684                        // Dense path
685                        while i + N <= len {
686                            let v = V::<N>::from_slice(&data[i..i + N]);
687                            sum += v.cast::<f64>().reduce_sum();
688                            cnt += N;
689                            i += N;
690                        }
691                        for &x in &data[i..] {
692                            sum += x as f64;
693                            cnt += 1;
694                        }
695                    } else {
696                        // Null‐aware path
697                        let mb = mask.expect(
698                            "Bitmask must be Some if nulls are present or mask is supplied",
699                        );
700                        let bytes = mb.as_bytes();
701
702                        while i + N <= len {
703                            let v = V::<N>::from_slice(&data[i..i + N]);
704                            // extract a SIMD‐mask for these N lanes
705                            let lane_m: Mask<M<N>, N> =
706                                bitmask_to_simd_mask::<N, M<N>>(bytes, i, len);
707                            // zero out the null lanes
708                            let vv = lane_m.select(v, V::<N>::splat(0.0 as $ty));
709                            sum += vv.cast::<f64>().reduce_sum();
710                            // count the 1‐bits in lane_m
711                            cnt += lane_m.to_bitmask().count_ones() as usize;
712                            i += N;
713                        }
714                        for idx in i..len {
715                            if unsafe { mb.get_unchecked(idx) } {
716                                sum += data[idx] as f64;
717                                cnt += 1;
718                            }
719                        }
720                    }
721
722                    return (sum, cnt);
723                }
724                // Fall through to scalar path if alignment check failed
725            }
726
727            // Scalar fallback - alignment check failed
728            let mut sum = 0.0f64;
729            let mut cnt = 0usize;
730            let len = data.len();
731
732            if !has_nulls {
733                for &x in data {
734                    sum += x as f64;
735                    cnt += 1;
736                }
737            } else {
738                let mb =
739                    mask.expect("Bitmask must be Some if nulls are present or mask is supplied");
740                for i in 0..len {
741                    if unsafe { mb.get_unchecked(i) } {
742                        sum += data[i] as f64;
743                        cnt += 1;
744                    }
745                }
746            }
747
748            (sum, cnt)
749        }
750    };
751}
752
753/// Macro to implement raw sum/count aggregation for integer types.
754macro_rules! impl_sum_int {
755    ($name:ident, $ty:ty, $acc:ty, $LANES:expr) => {
756        #[doc = concat!("Returns (sum, count) for non-null `", stringify!($ty), "` values.")]
757        #[doc = concat!(" Accumulates using `", stringify!($acc), "`. Uses SIMD if available.")]
758        #[inline(always)]
759        pub fn $name(
760            data: &[$ty],
761            mask: Option<&Bitmask>,
762            null_count: Option<usize>,
763        ) -> ($acc, usize) {
764            let has_nulls = has_nulls(null_count, mask);
765
766            #[cfg(feature = "simd")]
767            {
768                // Check if both arrays are 64-byte aligned for SIMD
769                if is_simd_aligned(data) {
770                    const N: usize = $LANES;
771                    type V<const L: usize> = Simd<$ty, L>;
772                    type M<const L: usize> = <$ty as SimdElement>::Mask;
773
774                    let len = data.len();
775                    let mut i = 0;
776                    let mut sum: $acc = 0;
777                    let mut cnt = 0_usize;
778
779                    if !has_nulls {
780                        // dense path
781                        while i + N <= len {
782                            let v = V::<N>::from_slice(&data[i..i + N]);
783                            sum += v.reduce_sum() as $acc;
784                            cnt += N;
785                            i += N;
786                        }
787                        // tail
788                        for &x in &data[i..] {
789                            sum += x as $acc;
790                            cnt += 1;
791                        }
792                    } else {
793                        // null‐aware path
794                        let mb = mask.expect(
795                            "Bitmask must be Some if nulls are present or mask is supplied",
796                        );
797                        let bytes = mb.as_bytes();
798
799                        while i + N <= len {
800                            let v = V::<N>::from_slice(&data[i..i + N]);
801                            // extract the SIMD validity mask
802                            let lane_m: Mask<M<N>, N> =
803                                bitmask_to_simd_mask::<N, M<N>>(bytes, i, len);
804                            // zero‐out invalid lanes
805                            let vv = lane_m.select(v, V::<N>::splat(0 as $ty));
806                            sum += vv.reduce_sum() as $acc;
807                            // count ones in the mask
808                            cnt += lane_m.to_bitmask().count_ones() as usize;
809                            i += N;
810                        }
811                        // scalar tail
812                        for idx in i..len {
813                            if unsafe { mb.get_unchecked(idx) } {
814                                sum += data[idx] as $acc;
815                                cnt += 1;
816                            }
817                        }
818                    }
819
820                    return (sum, cnt);
821                }
822                // Fall through to scalar path if alignment check failed
823            }
824
825            // Scalar fallback - alignment check failed
826            let mut sum: $acc = 0;
827            let mut cnt = 0_usize;
828            let len = data.len();
829
830            if !has_nulls {
831                for &x in data {
832                    sum += x as $acc;
833                    cnt += 1;
834                }
835            } else {
836                let mb =
837                    mask.expect("Bitmask must be Some if nulls are present or mask is supplied");
838                for i in 0..len {
839                    if unsafe { mb.get_unchecked(i) } {
840                        sum += data[i] as $acc;
841                        cnt += 1;
842                    }
843                }
844            }
845
846            (sum, cnt)
847        }
848    };
849}
850
851/// Computes Σx² for `$ty` values, skipping nulls.  
852/// SIMD‐accelerated when available.
853macro_rules! impl_sum2_float {
854    ($fn_name:ident, $ty:ty, $LANES:expr) => {
855        #[doc = concat!("Computes sum-of-squares for `", stringify!($ty), "` (Σx²).")]
856        #[doc = " Skips nulls if present. SIMD‐enabled."]
857        #[inline(always)]
858        pub fn $fn_name(data: &[$ty], mask: Option<&Bitmask>, null_count: Option<usize>) -> f64 {
859            let has_nulls = has_nulls(null_count, mask);
860
861            #[cfg(feature = "simd")]
862            {
863                // Check if both arrays are 64-byte aligned for SIMD
864                if is_simd_aligned(data) {
865                    // Vector type for the original element type
866                    type V<const N: usize> = Simd<$ty, N>;
867                    // Mask type comes from the element’s SimdElement impl
868                    type M<const N: usize> = <$ty as SimdElement>::Mask;
869
870                    const N: usize = $LANES;
871                    let len = data.len();
872                    let mut i = 0;
873                    let mut s2 = 0.0f64;
874
875                    if !has_nulls {
876                        // Dense path: no nulls
877                        while i + N <= len {
878                            // load N lanes, cast to f64, square & sum
879                            let dv = V::<N>::from_slice(&data[i..i + N]).cast::<f64>();
880                            s2 += (dv * dv).reduce_sum();
881                            i += N;
882                        }
883                        // tail
884                        for &x in &data[i..] {
885                            let y = x as f64;
886                            s2 += y * y;
887                        }
888                    } else {
889                        // Null‐aware path
890                        let mb = mask.expect("mask must be Some if nulls present");
891                        let bytes = mb.as_bytes();
892                        while i + N <= len {
893                            // load original‐type vector
894                            let v = V::<N>::from_slice(&data[i..i + N]);
895                            // extract the same‐type mask
896                            let lane_m: Mask<M<N>, N> =
897                                bitmask_to_simd_mask::<N, M<N>>(bytes, i, len);
898                            // zero out null lanes *before* the cast
899                            let vv = lane_m.select(v, V::<N>::splat(0 as $ty));
900                            // now safe to cast to f64 and sum squares
901                            let dv = vv.cast::<f64>();
902                            s2 += (dv * dv).reduce_sum();
903                            i += N;
904                        }
905                        // tail
906                        for idx in i..len {
907                            if unsafe { mb.get_unchecked(idx) } {
908                                let y = data[idx] as f64;
909                                s2 += y * y;
910                            }
911                        }
912                    }
913                    return s2;
914                }
915                // Fall through to scalar path if alignment check failed
916            }
917
918            // Scalar fallback - alignment check failed
919            let mut s2 = 0.0f64;
920            if !has_nulls {
921                for &x in data {
922                    let y = x as f64;
923                    s2 += y * y;
924                }
925            } else {
926                let mb = mask.expect("mask must be Some if nulls present");
927                for (i, &x) in data.iter().enumerate() {
928                    if unsafe { mb.get_unchecked(i) } {
929                        let y = x as f64;
930                        s2 += y * y;
931                    }
932                }
933            }
934            s2
935        }
936    };
937}
938
939/// Macro to implement null-aware element counting.
940macro_rules! impl_count {
941    ($fn_name:ident) => {
942        #[doc = "Returns the number of valid (non-null) elements."]
943        #[inline(always)]
944        pub fn $fn_name(len: usize, mask: Option<&Bitmask>, null_count: Option<usize>) -> usize {
945            match null_count {
946                Some(n) => len - n,
947                None => match mask {
948                    Some(m) => m.count_ones(),
949                    None => len,
950                },
951            }
952        }
953    };
954}
955
956/// Macro to implement sum and average aggregations for float/int types.
957/// Also handles raw (sum, count) cases internally.
958macro_rules! impl_sum_avg {
959    // floats ---------------------------------------------------
960    (float $ty:ident, $sum_fn:ident, $avg_fn:ident, $raw:ident) => {
961        #[doc = concat!("Returns the sum of `", stringify!($ty), "` values, or None if all-null.")]
962        #[inline(always)]
963        pub fn $sum_fn(d: &[$ty], m: Option<&Bitmask>, null_count: Option<usize>) -> Option<f64> {
964            let (s, n) = $raw(d, m, null_count);
965            if n == 0 { None } else { Some(s) }
966        }
967
968        #[doc = concat!("Returns the average of `", stringify!($ty), "` values, or None if all-null.")]
969        #[inline(always)]
970        pub fn $avg_fn(d: &[$ty], m: Option<&Bitmask>, null_count: Option<usize>) -> Option<f64> {
971            let (s, n) = $raw(d, m, null_count);
972            if n == 0 { None } else { Some(s / n as f64) }
973        }
974    };
975
976    // ints -----------------------------------------------------
977    (int $ty:ident, $sum_fn:ident, $avg_fn:ident, $acc:ty, $raw:ident) => {
978        #[doc = concat!("Returns the sum of `", stringify!($ty), "` values, or None if all-null.")]
979        #[inline(always)]
980        pub fn $sum_fn(d: &[$ty], m: Option<&Bitmask>, null_count: Option<usize>) -> Option<$acc> {
981            let (s, n) = $raw(d, m, null_count);
982            if n == 0 { None } else { Some(s) }
983        }
984
985        #[doc = concat!("Returns the average of `", stringify!($ty), "` values, or None if all-null.")]
986        #[inline(always)]
987        pub fn $avg_fn(d: &[$ty], m: Option<&Bitmask>, null_count: Option<usize>) -> Option<f64> {
988            let (s, n) = $raw(d, m, null_count);
989            if n == 0 { None } else { Some(s as f64 / n as f64) }
990        }
991    };
992}
993
994macro_rules! impl_stat_moments4_float {
995    ($fn_name:ident, $ty:ty, $LANES:expr) => {
996        #[doc = concat!(
997                                    "SIMD-accelerated (∑x … ∑x⁴) for `", stringify!($ty), "`.\n",
998                                    "Returns `(s1,s2,s3,s4,n)` as `(f64,f64,f64,f64,usize)`."
999                                )]
1000        #[inline(always)]
1001        pub fn $fn_name(
1002            data: &[$ty],
1003            mask: Option<&Bitmask>,
1004            null_count: Option<usize>,
1005        ) -> (f64, f64, f64, f64, usize) {
1006            let has_nulls = has_nulls(null_count, mask);
1007
1008            #[cfg(feature = "simd")]
1009            {
1010                // Check if both arrays are 64-byte aligned for SIMD
1011                if is_simd_aligned(data) {
1012                    type V<const L: usize> = Simd<$ty, L>;
1013                    type M<const L: usize> = <$ty as SimdElement>::Mask;
1014
1015                    const N: usize = $LANES;
1016                    let len = data.len();
1017                    let mut i = 0usize;
1018
1019                    let mut s1 = 0.0;
1020                    let mut s2 = 0.0;
1021                    let mut s3 = 0.0;
1022                    let mut s4 = 0.0;
1023                    let mut n = 0usize;
1024
1025                    if !has_nulls {
1026                        // Dense, no nulls
1027                        while i + N <= len {
1028                            let v = V::<N>::from_slice(&data[i..i + N]).cast::<f64>();
1029                            let v2 = v * v;
1030                            s1 += v.reduce_sum();
1031                            s2 += v2.reduce_sum();
1032                            s3 += (v2 * v).reduce_sum();
1033                            s4 += (v2 * v2).reduce_sum();
1034                            n += N;
1035                            i += N;
1036                        }
1037                        for &x in &data[i..] {
1038                            let xf = x as f64;
1039                            s1 += xf;
1040                            let x2 = xf * xf;
1041                            s2 += x2;
1042                            s3 += x2 * xf;
1043                            s4 += x2 * x2;
1044                            n += 1;
1045                        }
1046                    } else {
1047                        // Null-aware SIMD
1048                        let mb = mask.expect("mask must be Some when nulls present");
1049                        let mask_bytes = mb.as_bytes();
1050                        while i + N <= len {
1051                            let v_raw = V::<N>::from_slice(&data[i..i + N]);
1052                            let lane_m: Mask<M<N>, N> =
1053                                bitmask_to_simd_mask::<N, M<N>>(mask_bytes, i, len);
1054                            if !lane_m.any() {
1055                                i += N;
1056                                continue;
1057                            }
1058                            // Zero out nulls
1059                            let v = lane_m.select(v_raw, V::<N>::splat(0 as $ty)).cast::<f64>();
1060                            let v2 = v * v;
1061                            s1 += v.reduce_sum();
1062                            s2 += v2.reduce_sum();
1063                            s3 += (v2 * v).reduce_sum();
1064                            s4 += (v2 * v2).reduce_sum();
1065                            n += lane_m.to_bitmask().count_ones() as usize;
1066                            i += N;
1067                        }
1068                        for j in i..len {
1069                            if unsafe { mb.get_unchecked(j) } {
1070                                let xf = data[j] as f64;
1071                                s1 += xf;
1072                                let x2 = xf * xf;
1073                                s2 += x2;
1074                                s3 += x2 * xf;
1075                                s4 += x2 * x2;
1076                                n += 1;
1077                            }
1078                        }
1079                    }
1080                    return (s1, s2, s3, s4, n);
1081                }
1082                // Fall through to scalar path if alignment check failed
1083            }
1084
1085            // Scalar fallback - alignment check failed
1086            let mut s1 = 0.0;
1087            let mut s2 = 0.0;
1088            let mut s3 = 0.0;
1089            let mut s4 = 0.0;
1090            let mut n = 0usize;
1091
1092            if !has_nulls {
1093                for &x in data {
1094                    let xf = x as f64;
1095                    s1 += xf;
1096                    let x2 = xf * xf;
1097                    s2 += x2;
1098                    s3 += x2 * xf;
1099                    s4 += x2 * x2;
1100                    n += 1;
1101                }
1102            } else {
1103                let mb = mask.expect("mask must be Some when nulls present");
1104                for (idx, &x) in data.iter().enumerate() {
1105                    if unsafe { mb.get_unchecked(idx) } {
1106                        let xf = x as f64;
1107                        s1 += xf;
1108                        let x2 = xf * xf;
1109                        s2 += x2;
1110                        s3 += x2 * xf;
1111                        s4 += x2 * x2;
1112                        n += 1;
1113                    }
1114                }
1115            }
1116            (s1, s2, s3, s4, n)
1117        }
1118    };
1119}
1120
1121macro_rules! impl_stat_moments4_int {
1122    ($fn_name:ident, $ty:ty, $acc:ty, $LANES:expr) => {
1123        #[doc = concat!(
1124                    "SIMD-accelerated (∑x … ∑x⁴) for `", stringify!($ty), "` (promoted to f64).\n",
1125                    "Accumulator type for scalar paths: `", stringify!($acc), "`."
1126                )]
1127        #[inline(always)]
1128        pub fn $fn_name(
1129            data: &[$ty],
1130            mask: Option<&Bitmask>,
1131            null_count: Option<usize>
1132        ) -> (f64, f64, f64, f64, usize) {
1133            let has_nulls = has_nulls(null_count, mask);
1134
1135            #[cfg(feature = "simd")]
1136            {
1137                // Check if both arrays are 64-byte aligned for SIMD
1138                if is_simd_aligned(data) {
1139                type V<const L: usize> = Simd<$ty, L>;
1140                type M<const L: usize> = <$ty as SimdElement>::Mask;
1141
1142                const N: usize = $LANES;
1143                let len = data.len();
1144                let mut i = 0usize;
1145
1146                let mut s1 = 0.0;
1147                let mut s2 = 0.0;
1148                let mut s3 = 0.0;
1149                let mut s4 = 0.0;
1150                let mut n = 0usize;
1151
1152                if !has_nulls {
1153                    while i + N <= len {
1154                        let v_i = V::<N>::from_slice(&data[i..i + N]);
1155                        let v_f = v_i.cast::<f64>();
1156                        let v2 = v_f * v_f;
1157                        s1 += v_f.reduce_sum();
1158                        s2 += v2.reduce_sum();
1159                        s3 += (v2 * v_f).reduce_sum();
1160                        s4 += (v2 * v2).reduce_sum();
1161                        n += N;
1162                        i += N;
1163                    }
1164                    for &x in &data[i..] {
1165                        let xf = x as f64;
1166                        s1 += xf;
1167                        let x2 = xf * xf;
1168                        s2 += x2;
1169                        s3 += x2 * xf;
1170                        s4 += x2 * x2;
1171                        n += 1;
1172                    }
1173                } else {
1174                    let mb = mask.expect("mask must be Some when nulls present");
1175                    let mask_bytes = mb.as_bytes();
1176                    while i + N <= len {
1177                        let v_i = V::<N>::from_slice(&data[i..i + N]);
1178                        let lane_m: Mask<M<N>, N> =
1179                            bitmask_to_simd_mask::<N, M<N>>(mask_bytes, i, len);
1180                        if !lane_m.any() {
1181                            i += N;
1182                            continue;
1183                        }
1184                        let v_masked = lane_m.select(v_i, V::<N>::splat(0));
1185                        let v_f = v_masked.cast::<f64>();
1186                        let v2 = v_f * v_f;
1187                        s1 += v_f.reduce_sum();
1188                        s2 += v2.reduce_sum();
1189                        s3 += (v2 * v_f).reduce_sum();
1190                        s4 += (v2 * v2).reduce_sum();
1191                        n += lane_m.to_bitmask().count_ones() as usize;
1192                        i += N;
1193                    }
1194                    for j in i..len {
1195                        if unsafe { mb.get_unchecked(j) } {
1196                            let xf = data[j] as f64;
1197                            let x2 = xf * xf;
1198                            s1 += xf;
1199                            s2 += x2;
1200                            s3 += x2 * xf;
1201                            s4 += x2 * x2;
1202                            n += 1;
1203                        }
1204                    }
1205                }
1206                return (s1, s2, s3, s4, n);
1207                }
1208                // Fall through to scalar path if alignment check failed
1209            }
1210
1211            // Scalar fallback - alignment check failed
1212            // For int types, accumulate in a wide accumulator, then cast
1213            let mut s1: $acc = 0;
1214            let mut s2: $acc = 0;
1215            let mut s3: $acc = 0;
1216            let mut s4: $acc = 0;
1217            let mut n = 0usize;
1218
1219            if !has_nulls {
1220                for &x in data {
1221                    let xi: $acc = x as $acc;
1222                    let x2 = xi * xi;
1223                    s1 += xi;
1224                    s2 += x2;
1225                    s3 += x2 * xi;
1226                    s4 += x2 * x2;
1227                    n += 1;
1228                }
1229            } else {
1230                let mb = mask.expect("mask must be Some when nulls present");
1231                for (idx, &x) in data.iter().enumerate() {
1232                    if unsafe { mb.get_unchecked(idx) } {
1233                        let xi: $acc = x as $acc;
1234                        let x2 = xi * xi;
1235                        s1 += xi;
1236                        s2 += x2;
1237                        s3 += x2 * xi;
1238                        s4 += x2 * x2;
1239                        n += 1;
1240                    }
1241                }
1242            }
1243            (s1 as f64, s2 as f64, s3 as f64, s4 as f64, n)
1244        }
1245    };
1246}
1247
1248// Weighted-Sum-2  &  Pair-Stats kernels
1249macro_rules! impl_weighted_sum2_float {
1250    ($fn:ident, $ty:ty, $LANES:expr) => {
1251        #[doc = concat!(
1252                                    "Returns `(∑w·x, ∑w, ∑w·x², rows)` for `", stringify!($ty),
1253                                    "` slices `vals`, `wts` with optional null mask."
1254                                )]
1255        #[inline(always)]
1256        pub fn $fn(
1257            vals: &[$ty],
1258            wts: &[$ty],
1259            mask: Option<&Bitmask>,
1260            null_count: Option<usize>,
1261        ) -> (f64, f64, f64, usize) {
1262            debug_assert_eq!(vals.len(), wts.len(), "weighted_sum2x: len mismatch");
1263            let has_nulls = has_nulls(null_count, mask);
1264
1265            #[cfg(feature = "simd")]
1266            {
1267                // Check if both arrays are 64-byte aligned for SIMD
1268                if is_simd_aligned(vals) && is_simd_aligned(wts) {
1269                    type V<const L: usize> = Simd<$ty, L>;
1270                    type M<const L: usize> = <$ty as SimdElement>::Mask;
1271                    const N: usize = $LANES;
1272
1273                    let len = vals.len();
1274                    let mut i = 0usize;
1275                    let mut swx = 0.0;
1276                    let mut sw = 0.0;
1277                    let mut swx2 = 0.0;
1278                    let mut n = 0usize;
1279
1280                    if !has_nulls {
1281                        while i + N <= len {
1282                            let v = V::<N>::from_slice(&vals[i..i + N]).cast::<f64>();
1283                            let w = V::<N>::from_slice(&wts[i..i + N]).cast::<f64>();
1284                            swx += (v * w).reduce_sum();
1285                            sw += w.reduce_sum();
1286                            swx2 += (w * v * v).reduce_sum();
1287                            n += N;
1288                            i += N;
1289                        }
1290                        for j in i..len {
1291                            let vf = vals[j] as f64;
1292                            let wf = wts[j] as f64;
1293                            swx += wf * vf;
1294                            sw += wf;
1295                            swx2 += wf * vf * vf;
1296                            n += 1;
1297                        }
1298                    } else {
1299                        let mb = mask.expect("mask = Some when nulls present");
1300                        let mask_bytes = mb.as_bytes();
1301                        while i + N <= len {
1302                            let v_raw = V::<N>::from_slice(&vals[i..i + N]);
1303                            let w_raw = V::<N>::from_slice(&wts[i..i + N]);
1304                            let lane_m: Mask<M<N>, N> =
1305                                bitmask_to_simd_mask::<N, M<N>>(mask_bytes, i, len);
1306                            if !lane_m.any() {
1307                                i += N;
1308                                continue;
1309                            }
1310                            let v = lane_m.select(v_raw, V::<N>::splat(0.0)).cast::<f64>();
1311                            let w = lane_m.select(w_raw, V::<N>::splat(0.0)).cast::<f64>();
1312                            swx += (v * w).reduce_sum();
1313                            sw += w.reduce_sum();
1314                            swx2 += (w * v * v).reduce_sum();
1315                            n += lane_m.to_bitmask().count_ones() as usize;
1316                            i += N;
1317                        }
1318                        for j in i..len {
1319                            if unsafe { mb.get_unchecked(j) } {
1320                                let vf = vals[j] as f64;
1321                                let wf = wts[j] as f64;
1322                                swx += wf * vf;
1323                                sw += wf;
1324                                swx2 += wf * vf * vf;
1325                                n += 1;
1326                            }
1327                        }
1328                    }
1329                    return (swx, sw, swx2, n);
1330                }
1331                // Fall through to scalar path if alignment check failed
1332            }
1333
1334            // Scalar fallback - alignment check failed
1335            let mut swx = 0.0;
1336            let mut sw = 0.0;
1337            let mut swx2 = 0.0;
1338            let mut n = 0usize;
1339
1340            if !has_nulls {
1341                for i in 0..vals.len() {
1342                    let vf = vals[i] as f64;
1343                    let wf = wts[i] as f64;
1344                    swx += wf * vf;
1345                    sw += wf;
1346                    swx2 += wf * vf * vf;
1347                    n += 1;
1348                }
1349            } else {
1350                let mb = mask.expect("mask = Some when nulls present");
1351                for i in 0..vals.len() {
1352                    if unsafe { mb.get_unchecked(i) } {
1353                        let vf = vals[i] as f64;
1354                        let wf = wts[i] as f64;
1355                        swx += wf * vf;
1356                        sw += wf;
1357                        swx2 += wf * vf * vf;
1358                        n += 1;
1359                    }
1360                }
1361            }
1362            (swx, sw, swx2, n)
1363        }
1364    };
1365}
1366
1367/// Statistical summary for paired data analysis.
1368///
1369/// Accumulates fundamental statistical quantities for bivariate data pairs,
1370/// providing the building blocks for correlation analysis, regression coefficients,
1371/// and covariance calculations with SIMD-accelerated computation.
1372///
1373/// # Fields
1374/// - **`n`**: Count of valid (non-null) data pairs
1375/// - **`sx`**: Sum of x-values (Σx)
1376/// - **`sy`**: Sum of y-values (Σy)
1377/// - **`sxy`**: Sum of products (Σxy) for covariance calculation
1378/// - **`sx2`**: Sum of x-squared values (Σx²) for variance calculation
1379/// - **`sy2`**: Sum of y-squared values (Σy²) for variance calculation
1380///
1381/// # Applications
1382/// These statistics enable efficient calculation of:
1383/// - **Pearson correlation coefficient**: r = (n⋅Σxy - Σx⋅Σy) / √[(n⋅Σx² - (Σx)²)(n⋅Σy² - (Σy)²)]
1384/// - **Linear regression slope**: β₁ = (n⋅Σxy - Σx⋅Σy) / (n⋅Σx² - (Σx)²)
1385/// - **Sample covariance**: cov(x,y) = (Σxy - n⋅x̄⋅ȳ) / (n-1)
1386/// - **Coefficient of determination**: R² for regression analysis
1387#[derive(Default)]
1388pub struct PairStats {
1389    /// Count of valid (non-null) paired observations
1390    pub n: usize,
1391    /// Sum of x-values (Σx)
1392    pub sx: f64,
1393    /// Sum of y-values (Σy)
1394    pub sy: f64,
1395    /// Sum of cross-products (Σxy) for covariance computation
1396    pub sxy: f64,
1397    /// Sum of squared x-values (Σx²) for variance computation
1398    pub sx2: f64,
1399    /// Sum of squared y-values (Σy²) for variance computation
1400    pub sy2: f64,
1401}
1402
1403// PAIR - Σ stats (x,y)  (float)
1404macro_rules! impl_pair_stats_float {
1405    ($fn:ident, $ty:ty, $LANES:expr) => {
1406        /// Returns PairStats (n, sum_x, sum_y, sum_xy, sum_x2, sum_y2) for floating point types.
1407        #[inline(always)]
1408        pub fn $fn(
1409            xs: &[$ty],
1410            ys: &[$ty],
1411            mask: Option<&Bitmask>,
1412            null_count: Option<usize>,
1413        ) -> PairStats {
1414            debug_assert_eq!(xs.len(), ys.len(), "pair_stats: len mismatch ");
1415            let has_nulls = has_nulls(null_count, mask);
1416
1417            #[cfg(feature = "simd")]
1418            {
1419                // Check if both arrays are 64-byte aligned for SIMD
1420                if is_simd_aligned(xs) && is_simd_aligned(ys) {
1421                    type V<const L: usize> = Simd<$ty, L>;
1422                    type M<const L: usize> = <$ty as SimdElement>::Mask;
1423                    const N: usize = $LANES;
1424                    let len = xs.len();
1425
1426                    let mut i = 0usize;
1427                    let mut sx = 0.0;
1428                    let mut sy = 0.0;
1429                    let mut sxy = 0.0;
1430                    let mut sx2 = 0.0;
1431                    let mut sy2 = 0.0;
1432                    let mut n = 0usize;
1433
1434                    if !has_nulls {
1435                        while i + N <= len {
1436                            let vx = V::<N>::from_slice(&xs[i..i + N]).cast::<f64>();
1437                            let vy = V::<N>::from_slice(&ys[i..i + N]).cast::<f64>();
1438
1439                            sx += vx.reduce_sum();
1440                            sy += vy.reduce_sum();
1441                            sxy += (vx * vy).reduce_sum();
1442                            sx2 += (vx * vx).reduce_sum();
1443                            sy2 += (vy * vy).reduce_sum();
1444                            n += N;
1445                            i += N;
1446                        }
1447                        for j in i..len {
1448                            let xf = xs[j] as f64;
1449                            let yf = ys[j] as f64;
1450                            sx += xf;
1451                            sy += yf;
1452                            sxy += xf * yf;
1453                            sx2 += xf * xf;
1454                            sy2 += yf * yf;
1455                            n += 1;
1456                        }
1457                    } else {
1458                        let mb = mask.expect("mask Some when nulls present");
1459                        let mask_bytes = mb.as_bytes();
1460
1461                        while i + N <= len {
1462                            let vx_raw = V::<N>::from_slice(&xs[i..i + N]);
1463                            let vy_raw = V::<N>::from_slice(&ys[i..i + N]);
1464                            let lane_m: Mask<M<N>, N> =
1465                                bitmask_to_simd_mask::<N, M<N>>(mask_bytes, i, len);
1466                            if !lane_m.any() {
1467                                i += N;
1468                                continue;
1469                            }
1470
1471                            let vx = lane_m.select(vx_raw, V::<N>::splat(0.0)).cast::<f64>();
1472                            let vy = lane_m.select(vy_raw, V::<N>::splat(0.0)).cast::<f64>();
1473
1474                            sx += vx.reduce_sum();
1475                            sy += vy.reduce_sum();
1476                            sxy += (vx * vy).reduce_sum();
1477                            sx2 += (vx * vx).reduce_sum();
1478                            sy2 += (vy * vy).reduce_sum();
1479                            n += lane_m.to_bitmask().count_ones() as usize;
1480                            i += N;
1481                        }
1482                        for j in i..len {
1483                            if unsafe { mb.get_unchecked(j) } {
1484                                let xf = xs[j] as f64;
1485                                let yf = ys[j] as f64;
1486                                sx += xf;
1487                                sy += yf;
1488                                sxy += xf * yf;
1489                                sx2 += xf * xf;
1490                                sy2 += yf * yf;
1491                                n += 1;
1492                            }
1493                        }
1494                    }
1495                    return PairStats {
1496                        n,
1497                        sx,
1498                        sy,
1499                        sxy,
1500                        sx2,
1501                        sy2,
1502                    };
1503                }
1504                // Fall through to scalar path if alignment check failed
1505            }
1506
1507            // Scalar fallback - alignment check failed
1508            let mut sx = 0.0;
1509            let mut sy = 0.0;
1510            let mut sxy = 0.0;
1511            let mut sx2 = 0.0;
1512            let mut sy2 = 0.0;
1513            let mut n = 0usize;
1514
1515            if !has_nulls {
1516                for i in 0..xs.len() {
1517                    let xf = xs[i] as f64;
1518                    let yf = ys[i] as f64;
1519                    sx += xf;
1520                    sy += yf;
1521                    sxy += xf * yf;
1522                    sx2 += xf * xf;
1523                    sy2 += yf * yf;
1524                    n += 1;
1525                }
1526            } else {
1527                let mb = mask.expect("mask Some when nulls present");
1528                for i in 0..xs.len() {
1529                    if unsafe { mb.get_unchecked(i) } {
1530                        let xf = xs[i] as f64;
1531                        let yf = ys[i] as f64;
1532                        sx += xf;
1533                        sy += yf;
1534                        sxy += xf * yf;
1535                        sx2 += xf * xf;
1536                        sy2 += yf * yf;
1537                        n += 1;
1538                    }
1539                }
1540            }
1541            PairStats {
1542                n,
1543                sx,
1544                sy,
1545                sxy,
1546                sx2,
1547                sy2,
1548            }
1549        }
1550    };
1551}
1552
1553// 4-moment running-update helper (scalar). Works for any Into<f64> value.
1554#[inline(always)]
1555fn moments4_scalar<I>(iter: I) -> (usize, f64, f64, f64, f64)
1556where
1557    I: IntoIterator<Item = f64>,
1558{
1559    let mut n = 0usize;
1560    let mut m1 = 0.0; // mean
1561    let mut m2 = 0.0; // Σ(x-μ)²
1562    let mut m3 = 0.0; // Σ(x-μ)³
1563    let mut m4 = 0.0; // Σ(x-μ)⁴
1564
1565    for x in iter {
1566        n += 1;
1567        let n_f = n as f64;
1568        let delta = x - m1;
1569        let delta_n = delta / n_f;
1570        let delta_n2 = delta_n * delta_n;
1571        let term1 = delta * delta_n * (n_f - 1.0);
1572
1573        m4 += term1 * delta_n2 * (n_f * n_f - 3.0 * n_f + 3.0) + 6.0 * delta_n2 * m2
1574            - 4.0 * delta_n * m3;
1575
1576        m3 += term1 * delta_n * (n_f - 2.0) - 3.0 * delta_n * m2;
1577        m2 += term1;
1578        m1 += delta_n;
1579    }
1580    (n, m1, m2, m3, m4)
1581}
1582
1583//  Skewness & Kurtosis — float
1584/// Implements skewness and kurtosis calculation functions for floating-point types.
1585macro_rules! impl_skew_kurt_float {
1586    ($ty:ident, $skew_fn:ident, $kurt_fn:ident) => {
1587        /// Calculates the skewness of a floating-point dataset.
1588        ///
1589        /// Skewness measures the asymmetry of the probability distribution.
1590        /// Positive skewness indicates a longer tail on the right side,
1591        /// negative skewness indicates a longer tail on the left side.
1592        ///
1593        /// # Arguments
1594        ///
1595        /// * `d` - The slice of floating-point values
1596        /// * `m` - Optional bitmask for null value handling
1597        /// * `null_count` - Optional count of null values
1598        /// * `adjust_sample_bias` - Whether to apply sample bias correction
1599        ///
1600        /// # Returns
1601        ///
1602        /// `Some(skewness)` if calculation is possible, `None` if insufficient data
1603        #[inline(always)]
1604        pub fn $skew_fn(
1605            d: &[$ty],
1606            m: Option<&Bitmask>,
1607            null_count: Option<usize>,
1608            adjust_sample_bias: bool,
1609        ) -> Option<f64> {
1610            let vals: Vec<f64> = if has_nulls(null_count, m) {
1611                collect_valid(d, m).into_iter().map(|x| x as f64).collect()
1612            } else {
1613                d.iter().map(|&x| x as f64).collect()
1614            };
1615
1616            let (n, _m1, m2, m3, _) = moments4_scalar(vals);
1617            if n < 3 || m2 == 0.0 {
1618                return None;
1619            }
1620            let n_f = n as f64;
1621
1622            // population skewness (fisher-style)
1623            let pop = n_f.sqrt() * m3 / m2.powf(1.5);
1624            if !adjust_sample_bias {
1625                return Some(pop);
1626            }
1627
1628            // unbiased sample skewness (Joanes & Gill)
1629            Some(n_f * (n_f - 1.0).sqrt() * m3 / ((n_f - 2.0) * m2.powf(1.5)))
1630        }
1631
1632        /// Calculates the kurtosis of a floating-point dataset.
1633        ///
1634        /// Kurtosis measures the "tailedness" of the probability distribution.
1635        /// Higher kurtosis indicates more extreme outliers, lower kurtosis
1636        /// indicates a distribution with lighter tails.
1637        ///
1638        /// # Arguments
1639        ///
1640        /// * `d` - The slice of floating-point values
1641        /// * `m` - Optional bitmask for null value handling
1642        /// * `null_count` - Optional count of null values
1643        /// * `adjust_sample_bias` - Whether to apply sample bias correction
1644        ///
1645        /// # Returns
1646        ///
1647        /// `Some(kurtosis)` if calculation is possible, `None` if insufficient data
1648        #[inline(always)]
1649        pub fn $kurt_fn(
1650            d: &[$ty],
1651            m: Option<&Bitmask>,
1652            null_count: Option<usize>,
1653            adjust_sample_bias: bool,
1654        ) -> Option<f64> {
1655            let vals: Vec<f64> = if has_nulls(null_count, m) {
1656                collect_valid(d, m).into_iter().map(|x| x as f64).collect()
1657            } else {
1658                d.iter().map(|&x| x as f64).collect()
1659            };
1660
1661            let (n, _m1, m2, _m3, m4) = moments4_scalar(vals);
1662            if n < 2 || m2 == 0.0 {
1663                return None;
1664            }
1665            let n_f = n as f64;
1666            let pop_excess = n_f * m4 / (m2 * m2) - 3.0;
1667            if !adjust_sample_bias {
1668                return Some(pop_excess);
1669            }
1670
1671            // n < 4 → unbiased formula undefined; fall back to population value
1672            if n < 4 {
1673                return Some(pop_excess);
1674            }
1675
1676            // unbiased sample excess kurtosis (Joanes & Gill)
1677            let numerator = (n_f + 1.0) * n_f * (m4 / (m2 * m2)) - 3.0 * (n_f - 1.0);
1678            let denominator = (n_f - 2.0) * (n_f - 3.0);
1679            Some(numerator * (n_f - 1.0) / denominator)
1680        }
1681    };
1682}
1683
1684//  Skewness & Kurtosis — Integer
1685/// Implements skewness and kurtosis calculation functions for integer types.
1686macro_rules! impl_skew_kurt_int {
1687    ($ty:ident, $skew_fn:ident, $kurt_fn:ident) => {
1688        /// Calculates the skewness of an integer dataset.
1689        ///
1690        /// Skewness measures the asymmetry of the probability distribution.
1691        /// Values are converted to f64 for calculation to maintain precision.
1692        ///
1693        /// # Arguments
1694        ///
1695        /// * `d` - The slice of integer values
1696        /// * `m` - Optional bitmask for null value handling
1697        /// * `null_count` - Optional count of null values
1698        /// * `adjust_sample_bias` - Whether to apply sample bias correction
1699        ///
1700        /// # Returns
1701        ///
1702        /// `Some(skewness)` if calculation is possible, `None` if insufficient data
1703        #[inline(always)]
1704        pub fn $skew_fn(
1705            d: &[$ty],
1706            m: Option<&Bitmask>,
1707            null_count: Option<usize>,
1708            adjust_sample_bias: bool,
1709        ) -> Option<f64> {
1710            let vals: Vec<f64> = if has_nulls(null_count, m) {
1711                collect_valid(d, m).into_iter().map(|x| x as f64).collect()
1712            } else {
1713                d.iter().map(|&x| x as f64).collect()
1714            };
1715
1716            let (n, _m1, m2, m3, _) = moments4_scalar(vals);
1717            if n < 3 || m2 == 0.0 {
1718                return None;
1719            }
1720            let n_f = n as f64;
1721
1722            let pop = n_f.sqrt() * m3 / m2.powf(1.5);
1723            if !adjust_sample_bias {
1724                return Some(pop);
1725            }
1726
1727            Some(n_f * (n_f - 1.0).sqrt() * m3 / ((n_f - 2.0) * m2.powf(1.5)))
1728        }
1729
1730        /// Calculates the kurtosis of an integer dataset.
1731        ///
1732        /// Kurtosis measures the "tailedness" of the probability distribution.
1733        /// Values are converted to f64 for calculation to maintain precision.
1734        ///
1735        /// # Arguments
1736        ///
1737        /// * `d` - The slice of integer values
1738        /// * `m` - Optional bitmask for null value handling
1739        /// * `null_count` - Optional count of null values
1740        /// * `adjust_sample_bias` - Whether to apply sample bias correction
1741        ///
1742        /// # Returns
1743        ///
1744        /// `Some(kurtosis)` if calculation is possible, `None` if insufficient data
1745        #[inline(always)]
1746        pub fn $kurt_fn(
1747            d: &[$ty],
1748            m: Option<&Bitmask>,
1749            null_count: Option<usize>,
1750            adjust_sample_bias: bool,
1751        ) -> Option<f64> {
1752            let vals: Vec<f64> = if has_nulls(null_count, m) {
1753                collect_valid(d, m).into_iter().map(|x| x as f64).collect()
1754            } else {
1755                d.iter().map(|&x| x as f64).collect()
1756            };
1757
1758            let (n, _m1, m2, _m3, m4) = moments4_scalar(vals);
1759            if n < 2 || m2 == 0.0 {
1760                return None;
1761            }
1762            let n_f = n as f64;
1763
1764            let pop_excess = n_f * m4 / (m2 * m2) - 3.0;
1765            if !adjust_sample_bias {
1766                return Some(pop_excess);
1767            }
1768            if n < 4 {
1769                return Some(pop_excess);
1770            }
1771
1772            let numerator = (n_f + 1.0) * n_f * (m4 / (m2 * m2)) - 3.0 * (n_f - 1.0);
1773            let denominator = (n_f - 2.0) * (n_f - 3.0);
1774            Some(numerator * (n_f - 1.0) / denominator)
1775        }
1776    };
1777}
1778
1779/// Computes the sum of squares (Σx²).
1780/// Kernel: SumP2 (L1-2)
1781#[inline(always)]
1782pub fn sum_squares(v: &[f64], mask: Option<&Bitmask>, null_count: Option<usize>) -> f64 {
1783    let has_nulls = has_nulls(null_count, mask);
1784
1785    #[cfg(feature = "simd")]
1786    if is_simd_aligned(v) {
1787        type V<const L: usize> = Simd<f64, L>;
1788        type M<const L: usize> = <f64 as SimdElement>::Mask;
1789        const N: usize = W64;
1790        let len = v.len();
1791        let mut i = 0;
1792        let mut acc = 0.0f64;
1793        if !has_nulls {
1794            while i + N <= len {
1795                let simd_v = V::<N>::from_slice(&v[i..i + N]);
1796                acc += (simd_v * simd_v).reduce_sum();
1797                i += N;
1798            }
1799            for &x in &v[i..] {
1800                acc += x * x;
1801            }
1802        } else {
1803            let mb = mask.expect("sum_squares: mask must be Some when nulls are present");
1804            let mask_bytes = mb.as_bytes();
1805            while i + N <= len {
1806                let simd_v = V::<N>::from_slice(&v[i..i + N]);
1807                let lane_m: Mask<M<N>, N> = bitmask_to_simd_mask::<N, M<N>>(mask_bytes, i, len);
1808                let simd_v_masked = lane_m.select(simd_v, V::<N>::splat(0.0));
1809                acc += (simd_v_masked * simd_v_masked).reduce_sum();
1810                i += N;
1811            }
1812            for j in i..len {
1813                if unsafe { mb.get_unchecked(j) } {
1814                    acc += v[j] * v[j];
1815                }
1816            }
1817        }
1818        return acc;
1819    }
1820
1821    // Scalar fallback - alignment check failed or no simd flag
1822    let mut acc = 0.0f64;
1823    if !has_nulls {
1824        for &x in v {
1825            acc += x * x;
1826        }
1827    } else {
1828        let mb = mask.expect("sum_squares: mask must be Some when nulls are present");
1829        for (j, &x) in v.iter().enumerate() {
1830            if unsafe { mb.get_unchecked(j) } {
1831                acc += x * x;
1832            }
1833        }
1834    }
1835    acc
1836}
1837
1838/// Computes the sum of cubes (Σx³).
1839/// Kernel: SumP3 (L1-3)
1840#[inline(always)]
1841pub fn sum_cubes(v: &[f64], mask: Option<&Bitmask>, null_count: Option<usize>) -> f64 {
1842    let has_nulls = has_nulls(null_count, mask);
1843
1844    #[cfg(feature = "simd")]
1845    if is_simd_aligned(v) {
1846        type V<const L: usize> = Simd<f64, L>;
1847        type M<const L: usize> = <f64 as SimdElement>::Mask;
1848        const N: usize = W64;
1849        let len = v.len();
1850        let mut i = 0;
1851        let mut acc = 0.0f64;
1852        let mut found_infinite = false;
1853
1854        if !has_nulls {
1855            while i + N <= len {
1856                let simd_v = V::<N>::from_slice(&v[i..i + N]);
1857                let simd_v2 = simd_v * simd_v;
1858                let simd_cubes = simd_v2 * simd_v;
1859                // SIMD horizontal check for infinities
1860                let infinite_mask = simd_cubes.is_infinite();
1861                if infinite_mask.any() {
1862                    found_infinite = true;
1863                }
1864                acc += simd_cubes.reduce_sum();
1865                i += N;
1866            }
1867            for &x in &v[i..] {
1868                let cube = x * x * x;
1869                if cube.is_infinite() {
1870                    found_infinite = true;
1871                }
1872                acc += cube;
1873            }
1874        } else {
1875            let mb = mask.expect("sum_cubes: mask must be Some when nulls are present");
1876            let mask_bytes = mb.as_bytes();
1877            while i + N <= len {
1878                let simd_v = V::<N>::from_slice(&v[i..i + N]);
1879                let lane_m: Mask<M<N>, N> = bitmask_to_simd_mask::<N, M<N>>(mask_bytes, i, len);
1880                let simd_v_masked = lane_m.select(simd_v, V::<N>::splat(0.0));
1881                let simd_v2 = simd_v_masked * simd_v_masked;
1882                let simd_cubes = simd_v2 * simd_v_masked;
1883                let infinite_mask = simd_cubes.is_infinite();
1884                if infinite_mask.any() {
1885                    found_infinite = true;
1886                }
1887                acc += simd_cubes.reduce_sum();
1888                i += N;
1889            }
1890            for j in i..len {
1891                if unsafe { mb.get_unchecked(j) } {
1892                    let cube = v[j] * v[j] * v[j];
1893                    if cube.is_infinite() {
1894                        found_infinite = true;
1895                    }
1896                    acc += cube;
1897                }
1898            }
1899        }
1900        return if acc.is_nan() && found_infinite {
1901            f64::INFINITY
1902        } else {
1903            acc
1904        };
1905    }
1906
1907    // Scalar fallback - alignment check failed or no simd flag
1908    let mut acc = 0.0f64;
1909    let mut found_infinite = false;
1910    if !has_nulls {
1911        for &x in v {
1912            let cube = x * x * x;
1913            if cube.is_infinite() {
1914                found_infinite = true;
1915            }
1916            acc += cube;
1917        }
1918    } else {
1919        let mb = mask.expect("sum_cubes: mask must be Some when nulls are present");
1920        for (j, &x) in v.iter().enumerate() {
1921            if unsafe { mb.get_unchecked(j) } {
1922                let cube = x * x * x;
1923                if cube.is_infinite() {
1924                    found_infinite = true;
1925                }
1926                acc += cube;
1927            }
1928        }
1929    }
1930    if acc.is_nan() && found_infinite {
1931        f64::INFINITY
1932    } else {
1933        acc
1934    }
1935}
1936
1937/// Computes the sum of fourth powers (Σx⁴).
1938/// Kernel: SumP4 (L1-4)
1939#[inline(always)]
1940pub fn sum_quartics(v: &[f64], mask: Option<&Bitmask>, null_count: Option<usize>) -> f64 {
1941    let has_nulls = has_nulls(null_count, mask);
1942
1943    #[cfg(feature = "simd")]
1944    if is_simd_aligned(v) {
1945        type V<const L: usize> = Simd<f64, L>;
1946        type M<const L: usize> = <f64 as SimdElement>::Mask;
1947        const N: usize = W64;
1948        let len = v.len();
1949        let mut i = 0;
1950        let mut acc = 0.0f64;
1951        if !has_nulls {
1952            while i + N <= len {
1953                let simd_v = V::<N>::from_slice(&v[i..i + N]);
1954                let simd_v2 = simd_v * simd_v;
1955                acc += (simd_v2 * simd_v2).reduce_sum();
1956                i += N;
1957            }
1958            for &x in &v[i..] {
1959                let x2 = x * x;
1960                acc += x2 * x2;
1961            }
1962        } else {
1963            let mb = mask.expect("sum_quartics: mask must be Some when nulls are present");
1964            let mask_bytes = mb.as_bytes();
1965            while i + N <= len {
1966                let simd_v = V::<N>::from_slice(&v[i..i + N]);
1967                let lane_m: Mask<M<N>, N> = bitmask_to_simd_mask::<N, M<N>>(mask_bytes, i, len);
1968                let simd_v_masked = lane_m.select(simd_v, V::<N>::splat(0.0));
1969                let simd_v2 = simd_v_masked * simd_v_masked;
1970                acc += (simd_v2 * simd_v2).reduce_sum();
1971                i += N;
1972            }
1973            for j in i..len {
1974                if unsafe { mb.get_unchecked(j) } {
1975                    let x2 = v[j] * v[j];
1976                    acc += x2 * x2;
1977                }
1978            }
1979        }
1980        return acc;
1981    }
1982
1983    // Scalar fallback - alignment check failed or no simd flag
1984    let mut acc = 0.0f64;
1985    if !has_nulls {
1986        for &x in v {
1987            let x2 = x * x;
1988            acc += x2 * x2;
1989        }
1990    } else {
1991        let mb = mask.expect("sum_quartics: mask must be Some when nulls are present");
1992        for (j, &x) in v.iter().enumerate() {
1993            if unsafe { mb.get_unchecked(j) } {
1994                let x2 = x * x;
1995                acc += x2 * x2;
1996            }
1997        }
1998    }
1999    acc
2000}
2001
2002// Macro impls per type
2003
2004impl_weighted_sum2_float!(weighted_sum2_f64, f64, W64);
2005impl_weighted_sum2_float!(weighted_sum2_f32, f32, W32);
2006
2007impl_pair_stats_float!(pair_stats_f64, f64, W64);
2008impl_pair_stats_float!(pair_stats_f32, f32, W32);
2009
2010impl_stat_moments4_float!(stat_moments4_f64, f64, W64);
2011impl_stat_moments4_float!(stat_moments4_f32, f32, W32);
2012
2013impl_stat_moments4_int!(stat_moments4_i64, i64, i128, W64);
2014impl_stat_moments4_int!(stat_moments4_u64, u64, u128, W64);
2015impl_stat_moments4_int!(stat_moments4_i32, i32, i128, W32);
2016impl_stat_moments4_int!(stat_moments4_u32, u32, u128, W32);
2017
2018impl_stat_moments_float!(stat_moments_f64, f64, W64);
2019impl_stat_moments_float!(stat_moments_f32, f32, W32);
2020impl_stat_moments_int!(stat_moments_i64, i64, i64, W64, i64);
2021impl_stat_moments_int!(stat_moments_u64, u64, u64, W64, i64);
2022impl_stat_moments_int!(stat_moments_i32, i32, i64, W32, i32);
2023impl_stat_moments_int!(stat_moments_u32, u32, u64, W32, i32);
2024impl_reduce_min_max!(reduce_min_max_i64, i64, W64, i64::MAX, i64::MIN);
2025impl_reduce_min_max!(reduce_min_max_u64, u64, W64, u64::MAX, u64::MIN);
2026impl_reduce_min_max!(reduce_min_max_i32, i32, W32, i32::MAX, i32::MIN);
2027impl_reduce_min_max!(reduce_min_max_u32, u32, W32, u32::MAX, u32::MIN);
2028impl_reduce_min_max_float!(reduce_min_max_f64, f64, W64, Simd, Mask);
2029impl_reduce_min_max_float!(reduce_min_max_f32, f32, W32, Simd, Mask);
2030
2031impl_min_max_range!(f64, reduce_min_max_f64, min_f64, max_f64, range_f64);
2032impl_min_max_range!(f32, reduce_min_max_f32, min_f32, max_f32, range_f32);
2033impl_min_max_range!(i64, reduce_min_max_i64, min_i64, max_i64, range_i64);
2034impl_min_max_range!(u64, reduce_min_max_u64, min_u64, max_u64, range_u64);
2035impl_min_max_range!(i32, reduce_min_max_i32, min_i32, max_i32, range_i32);
2036impl_min_max_range!(u32, reduce_min_max_u32, min_u32, max_u32, range_u32);
2037
2038impl_sum_avg!(float f64, sum_f64, average_f64, sum_f64_raw);
2039impl_sum_avg!(float f32, sum_f32, average_f32, sum_f32_raw);
2040
2041impl_sum_avg!(int  i64, sum_i64, average_i64, i64, sum_i64_raw);
2042impl_sum_avg!(int  u64, sum_u64, average_u64, u64, sum_u64_raw);
2043impl_sum_avg!(int  i32, sum_i32, average_i32, i64 , sum_i32_raw);
2044impl_sum_avg!(int  u32, sum_u32, average_u32, u64 , sum_u32_raw);
2045
2046impl_variance!(f64, variance_f64);
2047impl_variance!(f32, variance_f32);
2048impl_variance!(i64, variance_i64);
2049impl_variance!(u64, variance_u64);
2050impl_variance!(i32, variance_i32);
2051impl_variance!(u32, variance_u32);
2052
2053impl_sum_float!(sum_f64_raw, f64, W64);
2054impl_sum_float!(sum_f32_raw, f32, W32);
2055impl_sum_int!(sum_i64_raw, i64, i64, W64);
2056impl_sum_int!(sum_u64_raw, u64, u64, W64);
2057impl_sum_int!(sum_i32_raw, i32, i64, W32);
2058impl_sum_int!(sum_u32_raw, u32, u64, W32);
2059
2060impl_sum2_float!(sum2_f64_raw, f64, W64);
2061impl_sum2_float!(sum2_f32_raw, f32, W32);
2062
2063impl_count!(count_generic_raw);
2064
2065impl_skew_kurt_float!(f64, skewness_f64, kurtosis_f64);
2066impl_skew_kurt_float!(f32, skewness_f32, kurtosis_f32);
2067
2068impl_skew_kurt_int!(i64, skewness_i64, kurtosis_i64);
2069impl_skew_kurt_int!(u64, skewness_u64, kurtosis_u64);
2070impl_skew_kurt_int!(i32, skewness_i32, kurtosis_i32);
2071impl_skew_kurt_int!(u32, skewness_u32, kurtosis_u32);
2072
2073/// Computes median of a sorted/unsorted slice with optional nulls.
2074#[inline(always)]
2075pub fn median<T: Ord + Copy>(
2076    d: &[T],
2077    m: Option<&Bitmask>,
2078    null_count: Option<usize>,
2079    sort: bool,
2080) -> Option<T> {
2081    let has_nulls = match null_count {
2082        Some(n) => n > 0,
2083        None => m.is_some(),
2084    };
2085
2086    // collect valid values
2087    let mut v = if !has_nulls {
2088        Vec64::from_slice(d)
2089    } else {
2090        collect_valid(d, m)
2091    };
2092
2093    if v.is_empty() {
2094        return None;
2095    }
2096
2097    let len = v.len();
2098    let mid = len / 2;
2099
2100    if len % 2 == 1 {
2101        // odd: middle element
2102        if sort {
2103            let (_, nth, _) = v.select_nth_unstable(mid);
2104            Some(*nth)
2105        } else {
2106            Some(v[mid])
2107        }
2108    } else {
2109        // even: keep the *lower* median when sorted,
2110        // but for the unsorted case pick the element at index mid
2111        if sort {
2112            let (_, nth, _) = v.select_nth_unstable(mid - 1);
2113            Some(*nth)
2114        } else {
2115            Some(v[mid])
2116        }
2117    }
2118}
2119
2120/// Computes median for float types, with optional nulls and sorting.
2121#[inline(always)]
2122pub fn median_f<T: Float + Copy>(
2123    d: &[T],
2124    m: Option<&Bitmask>,
2125    null_count: Option<usize>,
2126    sort: bool,
2127) -> Option<T> {
2128    let has_nulls = match null_count {
2129        Some(n) => n > 0,
2130        None => m.is_some(),
2131    };
2132
2133    let mut v = if !has_nulls {
2134        Vec64::from_slice(d)
2135    } else {
2136        collect_valid(d, m)
2137    };
2138
2139    if v.is_empty() {
2140        return None;
2141    }
2142
2143    let len = v.len();
2144    let mid = len / 2;
2145
2146    if !sort {
2147        return Some(if len & 1 == 0 {
2148            (v[mid - 1] + v[mid]) / T::from(2.0).unwrap()
2149        } else {
2150            v[mid]
2151        });
2152    }
2153
2154    if len & 1 == 1 {
2155        let (_, nth, _) = v.select_nth_unstable_by(mid, total_cmp_f);
2156        Some(*nth)
2157    } else {
2158        let (_, nth_hi, _) = v.select_nth_unstable_by(mid, total_cmp_f);
2159        let hi = *nth_hi;
2160        let (_, nth_lo, _) = v.select_nth_unstable_by(mid - 1, total_cmp_f);
2161        let lo = *nth_lo;
2162        Some((lo + hi) / T::from(2.0).unwrap())
2163    }
2164}
2165/// Computes percentile for ordinal types, optionally sorted.
2166#[inline(always)]
2167pub fn percentile_ord<T: Ord + Copy>(
2168    d: &[T],
2169    m: Option<&Bitmask>,
2170    null_count: Option<usize>,
2171    p: f64,
2172    sort: bool,
2173) -> Option<T> {
2174    let has_nulls = match null_count {
2175        Some(n) => n > 0,
2176        None => m.is_some(),
2177    };
2178
2179    let mut v = if !has_nulls {
2180        Vec64::from_slice(d)
2181    } else {
2182        collect_valid(d, m)
2183    };
2184
2185    if v.is_empty() {
2186        return None;
2187    }
2188
2189    let idx = ((p / 100.0) * (v.len() as f64 - 1.0)).round() as usize;
2190
2191    if !sort {
2192        return v.get(idx).copied();
2193    }
2194
2195    let (_, nth, _) = v.select_nth_unstable(idx);
2196    Some(*nth)
2197}
2198
2199/// Computes percentile for float types, optionally sorted.
2200#[inline(always)]
2201pub fn percentile_f<T: Float + Copy>(
2202    d: &[T],
2203    m: Option<&Bitmask>,
2204    null_count: Option<usize>,
2205    p: f64,
2206    sort: bool,
2207) -> Option<T> {
2208    let has_nulls = match null_count {
2209        Some(n) => n > 0,
2210        None => m.is_some(),
2211    };
2212
2213    let mut v = if !has_nulls {
2214        Vec64::from_slice(d)
2215    } else {
2216        collect_valid(d, m)
2217    };
2218
2219    if v.is_empty() {
2220        return None;
2221    }
2222
2223    let idx = (p.clamp(0.0, 1.0) * (v.len() - 1) as f64).round() as usize;
2224
2225    if !sort {
2226        return v.get(idx).copied();
2227    }
2228
2229    let (_, nth, _) = v.select_nth_unstable_by(idx, total_cmp_f);
2230    Some(*nth)
2231}
2232
2233/// Computes `q` quantiles for ordinal types, optionally sorted.
2234#[inline(always)]
2235pub fn quantile<T: Ord + Copy>(
2236    d: &[T],
2237    m: Option<&Bitmask>,
2238    null_count: Option<usize>,
2239    q: usize,
2240    sort: bool,
2241) -> Option<Vec64<T>> {
2242    if q < 2 {
2243        return None;
2244    }
2245
2246    let has_nulls = match null_count {
2247        Some(n) => n > 0,
2248        None => m.is_some(),
2249    };
2250
2251    let mut v = if !has_nulls {
2252        Vec64::from_slice(d)
2253    } else {
2254        collect_valid(d, m)
2255    };
2256
2257    let n = v.len();
2258    if n == 0 {
2259        return None;
2260    }
2261
2262    if sort {
2263        v.sort_unstable();
2264    }
2265
2266    let mut out = Vec64::with_capacity(q - 1);
2267    unsafe {
2268        out.set_len(q - 1);
2269    }
2270
2271    for k in 1..q {
2272        let p = k as f64 / q as f64;
2273        let h = 1.0 + (n as f64 - 1.0) * p;
2274        let idx = h.floor() as usize;
2275        let idx = idx.saturating_sub(1).min(n - 1);
2276        unsafe {
2277            *out.get_unchecked_mut(k - 1) = v[idx];
2278        }
2279    }
2280
2281    Some(out)
2282}
2283
2284/// Computes `q` quantiles for float types, optionally sorted.
2285#[inline(always)]
2286pub fn quantile_f<T: Float + Copy>(
2287    d: &[T],
2288    m: Option<&Bitmask>,
2289    null_count: Option<usize>,
2290    q: usize,
2291    sort: bool,
2292) -> Option<Vec64<T>> {
2293    if q < 2 {
2294        return None;
2295    }
2296
2297    let has_nulls = match null_count {
2298        Some(n) => n > 0,
2299        None => m.is_some(),
2300    };
2301
2302    let mut v = if !has_nulls {
2303        Vec64::from_slice(d)
2304    } else {
2305        collect_valid(d, m)
2306    };
2307
2308    let n = v.len();
2309    if n == 0 {
2310        return None;
2311    }
2312
2313    if sort {
2314        sort_float(&mut v);
2315    }
2316
2317    let mut out = Vec64::with_capacity(q - 1);
2318    unsafe {
2319        out.set_len(q - 1);
2320    }
2321
2322    for k in 1..q {
2323        let p = k as f64 / q as f64;
2324        let h = 1.0 + (n as f64 - 1.0) * p;
2325        let hf = h.floor();
2326        let hc = h.ceil();
2327
2328        let idx_lo = (hf as usize).saturating_sub(1).min(n - 1);
2329        let idx_hi = (hc as usize).saturating_sub(1).min(n - 1);
2330
2331        let value = if idx_lo == idx_hi {
2332            v[idx_lo]
2333        } else {
2334            let weight = T::from(h - hf).unwrap();
2335            let v_lo = v[idx_lo];
2336            let v_hi = v[idx_hi];
2337            v_lo + (v_hi - v_lo) * weight
2338        };
2339
2340        unsafe {
2341            *out.get_unchecked_mut(k - 1) = value;
2342        }
2343    }
2344
2345    Some(out)
2346}
2347
2348/// Computes interquartile range for ordinal types.
2349#[inline(always)]
2350pub fn iqr<T>(d: &[T], m: Option<&Bitmask>, null_count: Option<usize>, s: bool) -> Option<T>
2351where
2352    T: Ord + Copy + Sub<Output = T>,
2353{
2354    Some(percentile_ord(d, m, null_count, 75.0, s)? - percentile_ord(d, m, null_count, 25.0, s)?)
2355}
2356
2357/// Computes interquartile range for float types.
2358#[inline(always)]
2359pub fn iqr_f<T: Float + Copy>(
2360    d: &[T],
2361    m: Option<&Bitmask>,
2362    null_count: Option<usize>,
2363    s: bool,
2364) -> Option<T> {
2365    Some(percentile_f(d, m, null_count, 0.75, s)? - percentile_f(d, m, null_count, 0.25, s)?)
2366}
2367
2368/// Computes mode (most frequent value) for ordinal types.
2369#[inline(always)]
2370pub fn mode<T: Eq + Hash + Copy>(
2371    d: &[T],
2372    m: Option<&Bitmask>,
2373    null_count: Option<usize>,
2374) -> Option<T> {
2375    let has_nulls = match null_count {
2376        Some(n) => n > 0,
2377        None => m.is_some(),
2378    };
2379
2380    #[cfg(feature = "fast_hash")]
2381    let mut f = AHashMap::with_capacity(d.len());
2382    #[cfg(not(feature = "fast_hash"))]
2383    let mut f = HashMap::with_capacity(d.len());
2384
2385    if !has_nulls {
2386        for &v in d {
2387            *f.entry(v).or_insert(0usize) += 1;
2388        }
2389    } else {
2390        let mb = m.expect("mode: nulls present but mask is None");
2391        for (i, &v) in d.iter().enumerate() {
2392            if unsafe { mb.get_unchecked(i) } {
2393                *f.entry(v).or_insert(0usize) += 1;
2394            }
2395        }
2396    }
2397
2398    f.into_iter().max_by_key(|e| e.1).map(|e| e.0)
2399}
2400
2401/// Computes mode for float types via bit-level equivalence.
2402#[inline(always)]
2403pub fn mode_f<T: ToBits + Copy>(
2404    d: &[T],
2405    m: Option<&Bitmask>,
2406    null_count: Option<usize>,
2407) -> Option<T> {
2408    let has_nulls = match null_count {
2409        Some(n) => n > 0,
2410        None => m.is_some(),
2411    };
2412
2413    #[cfg(feature = "fast_hash")]
2414    let mut f: AHashMap<T::Bits, (usize, T)> = AHashMap::with_capacity(d.len());
2415    #[cfg(not(feature = "fast_hash"))]
2416    let mut f: HashMap<T::Bits, (usize, T)> = HashMap::with_capacity(d.len());
2417
2418    if !has_nulls {
2419        for &v in d {
2420            let e = f.entry(v.to_bits()).or_insert((0, v));
2421            e.0 += 1;
2422        }
2423    } else {
2424        let mb = m.expect("mode_f: nulls present but mask is None");
2425        for (i, &v) in d.iter().enumerate() {
2426            if unsafe { mb.get_unchecked(i) } {
2427                let e = f.entry(v.to_bits()).or_insert((0, v));
2428                e.0 += 1;
2429            }
2430        }
2431    }
2432
2433    f.into_values().max_by_key(|x| x.0).map(|x| x.1)
2434}
2435
2436/// Counts distinct values in an ordinal slice, with optional nulls.
2437#[inline(always)]
2438pub fn count_distinct<T: Eq + Hash + Copy>(
2439    d: &[T],
2440    m: Option<&Bitmask>,
2441    null_count: Option<usize>,
2442) -> usize {
2443    let has_nulls = match null_count {
2444        Some(n) => n > 0,
2445        None => m.is_some(),
2446    };
2447
2448    if !has_nulls {
2449        d.iter().collect::<HashSet<_>>().len()
2450    } else {
2451        let mb = m.expect("count_distinct: nulls present but mask is None");
2452        d.iter()
2453            .enumerate()
2454            .filter(|(i, _)| unsafe { mb.get_unchecked(*i) })
2455            .map(|(_, v)| v)
2456            .collect::<HashSet<_>>()
2457            .len()
2458    }
2459}
2460
2461/// Counts distinct values in a float slice using bitwise equality.
2462#[inline(always)]
2463pub fn count_distinct_f<T: Float + ToBits + Copy>(
2464    d: &[T],
2465    m: Option<&Bitmask>,
2466    null_count: Option<usize>,
2467) -> usize {
2468    let has_nulls = match null_count {
2469        Some(n) => n > 0,
2470        None => m.is_some(),
2471    };
2472
2473    if !has_nulls {
2474        d.iter().map(|v| v.to_bits()).collect::<HashSet<_>>().len()
2475    } else {
2476        let mb = m.expect("count_distinct_f: nulls present but mask is None");
2477        d.iter()
2478            .enumerate()
2479            .filter(|(i, _)| unsafe { mb.get_unchecked(*i) })
2480            .map(|(_, v)| v.to_bits())
2481            .collect::<HashSet<_>>()
2482            .len()
2483    }
2484}
2485
2486/// Computes harmonic mean for integer types.
2487#[inline(always)]
2488pub fn harmonic_mean_int<T: NumCast + Copy + PartialOrd>(
2489    d: &[T],
2490    m: Option<&Bitmask>,
2491    null_count: Option<usize>,
2492) -> f64 {
2493    let has_nulls = match null_count {
2494        Some(n) => n > 0,
2495        None => m.is_some(),
2496    };
2497
2498    let v: Vec<f64> = if !has_nulls {
2499        d.iter().map(|&x| NumCast::from(x).unwrap()).collect()
2500    } else {
2501        collect_valid(d, m)
2502            .into_iter()
2503            .map(|x| NumCast::from(x).unwrap())
2504            .collect()
2505    };
2506
2507    if v.is_empty() {
2508        panic!("harmonic_mean_int: input data is empty");
2509    }
2510    if v.iter().any(|&x| x <= 0.0) {
2511        panic!("harmonic_mean_int: non-positive values are invalid");
2512    }
2513
2514    let n = v.len() as f64;
2515    let denom: f64 = v.iter().map(|&x| 1.0 / x).sum();
2516
2517    if denom == 0.0 {
2518        panic!("harmonic_mean_int: denominator is zero");
2519    }
2520
2521    n / denom
2522}
2523
2524/// Computes harmonic mean for unsigned integers.
2525#[inline(always)]
2526pub fn harmonic_mean_uint<T: NumCast + Copy + PartialOrd>(
2527    d: &[T],
2528    m: Option<&Bitmask>,
2529    null_count: Option<usize>,
2530) -> f64 {
2531    harmonic_mean_int(d, m, null_count)
2532}
2533
2534/// Computes harmonic mean for float types.
2535#[inline(always)]
2536pub fn harmonic_mean_f<T: Float + Into<f64> + Copy>(
2537    d: &[T],
2538    m: Option<&Bitmask>,
2539    null_count: Option<usize>,
2540) -> f64 {
2541    let has_nulls = match null_count {
2542        Some(n) => n > 0,
2543        None => m.is_some(),
2544    };
2545
2546    let (mut s, mut n) = (0.0, 0usize);
2547
2548    if !has_nulls {
2549        for &v in d {
2550            let x = v.into();
2551            if x <= 0.0 {
2552                panic!("harmonic_mean_f: non-positive values are invalid");
2553            }
2554            s += 1.0 / x;
2555            n += 1;
2556        }
2557    } else {
2558        let mb = m.expect("harmonic_mean_f: nulls present but mask is None");
2559        for (i, &v) in d.iter().enumerate() {
2560            if unsafe { mb.get_unchecked(i) } {
2561                let x = v.into();
2562                if x <= 0.0 {
2563                    panic!("harmonic_mean_f: non-positive values are invalid");
2564                }
2565                s += 1.0 / x;
2566                n += 1;
2567            }
2568        }
2569    }
2570
2571    if n == 0 {
2572        panic!("harmonic_mean_f: input data is empty");
2573    }
2574    if s == 0.0 {
2575        panic!("harmonic_mean_f: denominator is zero");
2576    }
2577
2578    n as f64 / s
2579}
2580
2581/// Computes geometric mean for integer types.
2582#[inline(always)]
2583pub fn geometric_mean_int<T: ToPrimitive + Copy>(
2584    d: &[T],
2585    m: Option<&Bitmask>,
2586    null_count: Option<usize>,
2587) -> f64 {
2588    let has_nulls = match null_count {
2589        Some(n) => n > 0,
2590        None => m.is_some(),
2591    };
2592
2593    let (mut s, mut n) = (0.0, 0usize);
2594    if !has_nulls {
2595        for &v in d.iter() {
2596            let val = v.to_f64().unwrap();
2597            if val <= 0.0 {
2598                panic!("geometric_mean_int: non-positive values are invalid");
2599            }
2600            s += val.ln();
2601            n += 1;
2602        }
2603    } else {
2604        let mb = m.expect("geometric_mean_int: nulls present but mask is None");
2605        for (i, &v) in d.iter().enumerate() {
2606            if unsafe { mb.get_unchecked(i) } {
2607                let val = v.to_f64().unwrap();
2608                if val <= 0.0 {
2609                    panic!("geometric_mean_int: non-positive values are invalid");
2610                }
2611                s += val.ln();
2612                n += 1;
2613            }
2614        }
2615    }
2616
2617    if n == 0 {
2618        panic!("geometric_mean_int: input data is empty");
2619    }
2620
2621    (s / n as f64).exp()
2622}
2623
2624/// Computes geometric mean for unsigned integer types.
2625#[inline(always)]
2626pub fn geometric_mean_uint<T: ToPrimitive + Copy>(
2627    d: &[T],
2628    m: Option<&Bitmask>,
2629    null_count: Option<usize>,
2630) -> f64 {
2631    geometric_mean_int(d, m, null_count)
2632}
2633
2634/// Computes geometric mean for float types.
2635#[inline(always)]
2636pub fn geometric_mean_f<T: Float + Into<f64> + Copy>(
2637    d: &[T],
2638    m: Option<&Bitmask>,
2639    null_count: Option<usize>,
2640) -> f64 {
2641    let has_nulls = match null_count {
2642        Some(n) => n > 0,
2643        None => m.is_some(),
2644    };
2645
2646    let (mut s, mut n) = (0.0, 0usize);
2647    if !has_nulls {
2648        for &v in d.iter() {
2649            let val = v.into();
2650            if val <= 0.0 {
2651                panic!("geometric_mean_f: non-positive values are invalid");
2652            }
2653            s += val.ln();
2654            n += 1;
2655        }
2656    } else {
2657        let mb = m.expect("geometric_mean_f: nulls present but mask is None");
2658        for (i, &v) in d.iter().enumerate() {
2659            if unsafe { mb.get_unchecked(i) } {
2660                let val = v.into();
2661                if val <= 0.0 {
2662                    panic!("geometric_mean_f: non-positive values are invalid");
2663                }
2664                s += val.ln();
2665                n += 1;
2666            }
2667        }
2668    }
2669
2670    if n == 0 {
2671        panic!("geometric_mean_f: input data is empty");
2672    }
2673
2674    (s / n as f64).exp()
2675}
2676
2677/// Returns the accumulating product of all values
2678#[inline(always)]
2679pub fn agg_product<T: Copy + One + Mul<Output = T> + Zero + PartialEq + 'static>(
2680    data: &[T],
2681    mask: Option<&Bitmask>,
2682    offset: usize,
2683    len: usize,
2684) -> T {
2685    let mut prod = T::one();
2686    match mask {
2687        Some(mask) => {
2688            let mask = mask.slice_clone(offset, len);
2689            for (i, &x) in data[offset..offset + len].iter().enumerate() {
2690                if unsafe { mask.get_unchecked(i) } {
2691                    prod = prod * x;
2692                    if prod == T::zero() {
2693                        break;
2694                    }
2695                }
2696            }
2697        }
2698        None => {
2699            for &x in &data[offset..offset + len] {
2700                prod = prod * x;
2701                if prod == T::zero() {
2702                    break;
2703                }
2704            }
2705        }
2706    }
2707    prod
2708}
2709
2710#[cfg(test)]
2711mod tests {
2712    use minarrow::{Vec64, vec64};
2713    use num_traits::Float;
2714
2715    use super::*;
2716
2717    // Build Arrow-style validity mask from bools (least significant bit is index 0)
2718    fn mask_from_bools(bits: &[bool]) -> Bitmask {
2719        Bitmask::from_bools(bits)
2720    }
2721
2722    // Compare floats robustly (for variance etc)
2723    fn approx_eq(a: f64, b: f64, eps: f64) -> bool {
2724        (a - b).abs() <= eps
2725    }
2726
2727    #[test]
2728    fn test_stat_moments_f64_dense() {
2729        let data = vec64![1.0, 2.0, 3.0, 4.0];
2730        let (sum, sum2, count) = stat_moments_f64(&data, None, None);
2731        assert_eq!(sum, 10.0);
2732        assert_eq!(sum2, 30.0);
2733        assert_eq!(count, 4);
2734    }
2735
2736    #[test]
2737    fn test_stat_moments_f64_masked() {
2738        let data = vec64![1.0, 2.0, 3.0, 4.0];
2739        let mask = mask_from_bools(&[true, false, true, false]);
2740        let (sum, sum2, count) = stat_moments_f64(&data, Some(&mask), Some(2));
2741        assert_eq!(sum, 4.0);
2742        assert_eq!(sum2, 10.0);
2743        assert_eq!(count, 2);
2744    }
2745
2746    #[test]
2747    fn test_stat_moments_i32_dense() {
2748        let data = vec64![1i32, 2, 3, 4];
2749        let (sum, sum2, count) = stat_moments_i32(&data, None, None);
2750        assert_eq!(sum, 10.0);
2751        assert_eq!(sum2, 30.0);
2752        assert_eq!(count, 4);
2753    }
2754
2755    #[test]
2756    fn test_stat_moments_i32_masked() {
2757        let data = vec64![1i32, 2, 3, 4];
2758        let mask = mask_from_bools(&[true, false, false, true]);
2759        let (sum, sum2, count) = stat_moments_i32(&data, Some(&mask), Some(2));
2760        assert_eq!(sum, 5.0);
2761        assert_eq!(sum2, 17.0);
2762        assert_eq!(count, 2);
2763    }
2764
2765    #[test]
2766    fn test_min_max_range_f64_dense() {
2767        let data = vec64![5.0, 3.0, 1.0, 7.0];
2768        assert_eq!(min_f64(&data, None, None), Some(1.0));
2769        assert_eq!(max_f64(&data, None, None), Some(7.0));
2770        assert_eq!(range_f64(&data, None, None), Some((1.0, 7.0)));
2771    }
2772
2773    #[test]
2774    fn test_min_max_range_f64_masked() {
2775        let data = vec64![5.0, 3.0, 1.0, 7.0];
2776        let mask = mask_from_bools(&[false, true, true, false]);
2777        assert_eq!(min_f64(&data, Some(&mask), Some(2)), Some(1.0));
2778        assert_eq!(max_f64(&data, Some(&mask), Some(2)), Some(3.0));
2779        assert_eq!(range_f64(&data, Some(&mask), Some(2)), Some((1.0, 3.0)));
2780    }
2781
2782    #[test]
2783    fn test_sum_and_avg_i32() {
2784        let data = vec64![1i32, 2, 3, 4];
2785        assert_eq!(sum_i32(&data, None, None), Some(10));
2786        assert_eq!(average_i32(&data, None, None), Some(2.5));
2787        let mask = mask_from_bools(&[true, false, true, true]);
2788        assert_eq!(sum_i32(&data, Some(&mask), Some(1)), Some(8));
2789        assert_eq!(average_i32(&data, Some(&mask), Some(1)), Some(8.0 / 3.0));
2790    }
2791
2792    #[test]
2793    fn test_sum_and_avg_f32() {
2794        let data = vec64![1.0f32, 2.0, 3.0, 4.0];
2795        assert_eq!(sum_f32(&data, None, None), Some(10.0));
2796        assert_eq!(average_f32(&data, None, None), Some(2.5));
2797        let mask = mask_from_bools(&[false, true, true, false]);
2798        assert_eq!(sum_f32(&data, Some(&mask), Some(2)), Some(5.0));
2799        assert_eq!(average_f32(&data, Some(&mask), Some(2)), Some(2.5));
2800    }
2801
2802    #[test]
2803    fn test_variance_f64() {
2804        let data = vec64![1.0, 2.0, 3.0, 4.0];
2805        let v = variance_f64(&data, None, None, false);
2806        assert!(approx_eq(v.unwrap(), 1.25, 1e-9));
2807        let mask = mask_from_bools(&[true, true, false, false]);
2808        let v2 = variance_f64(&data, Some(&mask), Some(2), false);
2809        assert!(approx_eq(v2.unwrap(), 0.25, 1e-9));
2810    }
2811
2812    #[test]
2813    fn test_count_generic_raw() {
2814        let n = 100;
2815        let mask = mask_from_bools(&[true; 100]);
2816        assert_eq!(count_generic_raw(n, None, None), 100);
2817        assert_eq!(count_generic_raw(n, Some(&mask), Some(0)), 100);
2818
2819        let mask = mask_from_bools(&(0..n).map(|i| i % 2 == 0).collect::<Vec<_>>());
2820        assert_eq!(count_generic_raw(n, Some(&mask), Some(50)), 50);
2821    }
2822
2823    #[test]
2824    fn test_median_ord_sorted() {
2825        let data = vec64![1i32, 4, 2, 3];
2826        assert_eq!(median(&data, None, None, true), Some(2));
2827    }
2828
2829    #[test]
2830    fn test_median_ord_unsorted() {
2831        let data = vec64![1i32, 4, 2, 3];
2832        // unsorted, even-length ⇒ index mid = 2 ⇒ value 2
2833        assert_eq!(median(&data, None, None, false), Some(2));
2834    }
2835
2836    #[test]
2837    fn test_median_ord_masked() {
2838        let data = vec64![1i32, 4, 2, 3];
2839        let mask = mask_from_bools(&[true, true, false, false]);
2840        assert_eq!(median(&data, Some(&mask), Some(2), true), Some(1));
2841    }
2842
2843    #[test]
2844    fn test_median_f_sorted() {
2845        let dataf = vec64![1.0f64, 2.0, 3.0, 4.0];
2846        assert_eq!(median_f(&dataf, None, None, true), Some(2.5));
2847    }
2848
2849    #[test]
2850    fn test_median_f_masked() {
2851        let dataf = vec64![1.0f64, 2.0, 3.0, 4.0];
2852        let mask = mask_from_bools(&[true, false, true, false]);
2853        assert_eq!(median_f(&dataf, Some(&mask), Some(2), true), Some(2.0));
2854    }
2855
2856    #[test]
2857    fn test_percentile_ord_and_f() {
2858        let data = vec64![10i32, 20, 30, 40, 50];
2859        assert_eq!(percentile_ord(&data, None, None, 0.0, true), Some(10));
2860        assert_eq!(percentile_ord(&data, None, None, 100.0, true), Some(50));
2861        assert_eq!(percentile_ord(&data, None, None, 50.0, true), Some(30));
2862        let mask = mask_from_bools(&[false, true, true, false, true]);
2863        assert_eq!(
2864            percentile_ord(&data, Some(&mask), Some(2), 50.0, true),
2865            Some(30)
2866        );
2867
2868        let dataf = vec64![1.0f64, 2.0, 3.0, 4.0, 5.0];
2869        assert_eq!(percentile_f(&dataf, None, None, 0.0, true), Some(1.0));
2870        assert_eq!(percentile_f(&dataf, None, None, 1.0, true), Some(5.0));
2871        assert_eq!(percentile_f(&dataf, None, None, 0.5, true), Some(3.0));
2872    }
2873
2874    #[test]
2875    fn test_quantile_ord_and_f() {
2876        let data = vec64![1i32, 2, 3, 4, 5];
2877        let out = quantile(&data, None, None, 4, true).unwrap();
2878        assert_eq!(out.len(), 3);
2879        assert_eq!(out, Vec64::from_slice(&[2, 3, 4]));
2880
2881        let mask = mask_from_bools(&[true, false, true, true, false]);
2882        let out2 = quantile(&data, Some(&mask), Some(2), 3, true).unwrap();
2883        assert_eq!(out2, Vec64::from_slice(&[1, 3]));
2884
2885        let dataf = vec64![10.0f64, 20.0, 30.0, 40.0, 50.0];
2886        let out = quantile_f(&dataf, None, None, 4, true).unwrap();
2887        assert_eq!(out.len(), 3);
2888        assert!(approx_eq(out[1], 30.0, 1e-9));
2889    }
2890
2891    #[test]
2892    fn test_iqr_ord() {
2893        let data = vec64![1i32, 2, 3, 4, 5];
2894        assert_eq!(iqr(&data, None, None, true), Some(2));
2895    }
2896
2897    #[test]
2898    fn test_iqr_f64() {
2899        let dataf = vec64![10.0f64, 20.0, 30.0, 40.0, 50.0];
2900        // p*(n−1) rounding definition, Q3=40, Q1=20 → IQR=20
2901        assert_eq!(iqr_f(&dataf, None, None, true), Some(20.0));
2902    }
2903    #[test]
2904    fn test_mode_ord_and_f() {
2905        let data = vec64![1i32, 2, 3, 2, 2, 4, 5, 1];
2906        assert_eq!(mode(&data, None, None), Some(2));
2907        let dataf = vec64![1.0f32, 2.0, 2.0, 3.0, 3.0, 3.0];
2908        assert_eq!(mode_f(&dataf, None, None), Some(3.0));
2909        let data = vec64![1i32, 2, 3, 3, 2, 1];
2910        let m = mode(&data, None, None);
2911        assert!(m == Some(1) || m == Some(2) || m == Some(3));
2912    }
2913
2914    #[test]
2915    fn test_count_distinct_and_f() {
2916        let data = vec64![1i32, 2, 2, 3, 3, 4];
2917        assert_eq!(count_distinct(&data, None, None), 4);
2918        let dataf = vec64![1.0f32, 2.0, 2.0, 3.0, 3.0, 4.0];
2919        assert_eq!(count_distinct_f(&dataf, None, None), 4);
2920    }
2921
2922    #[test]
2923    fn test_harmonic_and_geometric_mean() {
2924        let data = vec64![1.0f64, 2.0, 4.0];
2925        let hm = harmonic_mean_f(&data, None, None);
2926        assert!(approx_eq(hm, 1.714285714, 1e-6));
2927
2928        let gm = geometric_mean_f(&data, None, None);
2929        assert!(approx_eq(gm, 2.0, 1e-6));
2930
2931        let data = vec64![1i32, 2, 4];
2932        let hm = harmonic_mean_int(&data, None, None);
2933        assert!(approx_eq(hm, 1.714285714, 1e-6));
2934
2935        let gm = geometric_mean_int(&data, None, None);
2936        assert!(approx_eq(gm, 2.0, 1e-6));
2937    }
2938
2939    #[test]
2940    fn test_empty_arrays() {
2941        assert_eq!(stat_moments_f64(&[], None, None), (0.0, 0.0, 0));
2942        assert_eq!(stat_moments_i32(&[], None, None), (0.0, 0.0, 0));
2943        assert_eq!(sum_f64(&[], None, None), None);
2944        assert_eq!(average_i32(&[], None, None), None);
2945        assert_eq!(variance_f64(&[], None, None, false), None);
2946        assert_eq!(median::<i32>(&[], None, None, true), None);
2947        assert_eq!(median_f::<f64>(&[], None, None, true), None);
2948        assert_eq!(mode::<i32>(&[], None, None), None);
2949        assert_eq!(mode_f::<f32>(&[], None, None), None);
2950        assert_eq!(quantile::<i32>(&[], None, None, 3, true), None);
2951        assert_eq!(quantile_f::<f64>(&[], None, None, 3, true), None);
2952        assert_eq!(iqr::<i32>(&[], None, None, true), None);
2953        assert_eq!(iqr_f::<f64>(&[], None, None, true), None);
2954        assert_eq!(count_distinct::<i32>(&[], None, None), 0);
2955        assert_eq!(count_distinct_f::<f32>(&[], None, None), 0);
2956    }
2957
2958    // --- i64, u64, f32, u32: moments and basic stats (dense/masked) ---
2959    #[test]
2960    fn test_stat_moments_all_types() {
2961        let i64_data = vec64![-8i64, 2, 8, -2, 3];
2962        let mask = mask_from_bools(&[true, false, true, true, true]);
2963        let (sum, sum2, count) = stat_moments_i64(&i64_data, Some(&mask), Some(1));
2964        assert_eq!(sum, (-8.0) + 8.0 + (-2.0) + 3.0);
2965        assert_eq!(sum2, 64.0 + 64.0 + 4.0 + 9.0);
2966        assert_eq!(count, 4);
2967
2968        let u64_data = vec64![1u64, 2, 3, 4, 5];
2969        let mask = mask_from_bools(&[false, true, true, true, false]);
2970        let (sum, sum2, count) = stat_moments_u64(&u64_data, Some(&mask), Some(2));
2971        assert_eq!(sum, 2.0 + 3.0 + 4.0);
2972        assert_eq!(sum2, 4.0 + 9.0 + 16.0);
2973        assert_eq!(count, 3);
2974
2975        let f32_data = vec64![0.5f32, 2.0, 4.5, -2.0, 3.0];
2976        let mask = mask_from_bools(&[true, true, false, false, true]);
2977        let (sum, sum2, count) = stat_moments_f32(&f32_data, Some(&mask), Some(2));
2978        assert!(approx_eq(sum, 0.5 + 2.0 + 3.0, 1e-6));
2979        assert!(approx_eq(sum2, 0.25 + 4.0 + 9.0, 1e-6));
2980        assert_eq!(count, 3);
2981
2982        let u32_data = vec64![100u32, 200, 300, 400, 500];
2983        let mask = mask_from_bools(&[true, false, false, true, true]);
2984        let (sum, sum2, count) = stat_moments_u32(&u32_data, Some(&mask), Some(2));
2985        assert_eq!(sum, 100.0 + 400.0 + 500.0);
2986        assert_eq!(sum2, 10000.0 + 160000.0 + 250000.0);
2987        assert_eq!(count, 3);
2988    }
2989
2990    #[test]
2991    fn test_reduce_min_max_for_all_types() {
2992        let i64_data = vec64![-8i64, 2, 8, -2, 3];
2993        assert_eq!(reduce_min_max_i64(&i64_data, None, None), Some((-8, 8)));
2994        let mask = mask_from_bools(&[true, false, false, false, true]);
2995        assert_eq!(
2996            reduce_min_max_i64(&i64_data, Some(&mask), Some(3)),
2997            Some((-8, 3))
2998        );
2999
3000        let u64_data = vec64![1u64, 2, 3, 4, 5];
3001        assert_eq!(reduce_min_max_u64(&u64_data, None, None), Some((1, 5)));
3002        let mask = mask_from_bools(&[false, false, true, true, false]);
3003        assert_eq!(
3004            reduce_min_max_u64(&u64_data, Some(&mask), Some(3)),
3005            Some((3, 4))
3006        );
3007
3008        let i32_data = vec64![9i32, -2, 5, -2];
3009        assert_eq!(reduce_min_max_i32(&i32_data, None, None), Some((-2, 9)));
3010        let mask = mask_from_bools(&[false, false, true, false]);
3011        assert_eq!(
3012            reduce_min_max_i32(&i32_data, Some(&mask), Some(3)),
3013            Some((5, 5))
3014        );
3015
3016        let u32_data = vec64![7u32, 2, 5, 11];
3017        assert_eq!(reduce_min_max_u32(&u32_data, None, None), Some((2, 11)));
3018        let mask = mask_from_bools(&[true, false, false, true]);
3019        assert_eq!(
3020            reduce_min_max_u32(&u32_data, Some(&mask), Some(2)),
3021            Some((7, 11))
3022        );
3023
3024        let f64_data = vec64![f64::NAN, 3.0, 1.0, 7.0];
3025        assert_eq!(reduce_min_max_f64(&f64_data, None, None), Some((1.0, 7.0))); // NaN ignored
3026    }
3027
3028    #[test]
3029    fn test_empty_and_all_null_min_max() {
3030        let empty: [i32; 0] = [];
3031        assert_eq!(reduce_min_max_i32(&empty, None, None), None);
3032
3033        let all_nulls = vec64![5i32, 10, 15];
3034        let mask = mask_from_bools(&[false, false, false]);
3035        assert_eq!(reduce_min_max_i32(&all_nulls, Some(&mask), Some(3)), None);
3036    }
3037
3038    // --- sum/avg: all types, empty and all-null ---
3039    #[test]
3040    fn test_sum_avg_and_variance_all_types() {
3041        // u32
3042        let data = vec64![100u32, 200, 0, 400];
3043        assert_eq!(sum_u32(&data, None, None), Some(700));
3044        assert_eq!(average_u32(&data, None, None), Some(175.0));
3045        let mask = mask_from_bools(&[false, true, false, true]);
3046        assert_eq!(sum_u32(&data, Some(&mask), Some(2)), Some(600));
3047        assert_eq!(average_u32(&data, Some(&mask), Some(2)), Some(300.0));
3048        assert_eq!(
3049            variance_u32(&data, None, None, false).unwrap(),
3050            ((100f64.powi(2) + 200f64.powi(2) + 400f64.powi(2)) / 4.0 - (700.0 / 4.0).powi(2))
3051        );
3052
3053        // i64
3054        let data = vec64![5i64, -10, 25, 5];
3055        assert_eq!(sum_i64(&data, None, None), Some(25));
3056        assert_eq!(average_i64(&data, None, None), Some(6.25));
3057        let mask = mask_from_bools(&[true, false, false, false]);
3058        assert_eq!(sum_i64(&data, Some(&mask), Some(3)), Some(5));
3059        assert_eq!(average_i64(&data, Some(&mask), Some(3)), Some(5.0));
3060        assert!(variance_i64(&data, None, None, false).unwrap() > 0.0);
3061
3062        // u64
3063        let data = vec64![10u64, 0, 10, 10];
3064        assert_eq!(sum_u64(&data, None, None), Some(30));
3065        assert_eq!(average_u64(&data, None, None), Some(7.5));
3066        let mask = mask_from_bools(&[false, false, false, false]);
3067        assert_eq!(sum_u64(&data, Some(&mask), Some(4)), None);
3068        assert_eq!(average_u64(&data, Some(&mask), Some(4)), None);
3069
3070        // f32
3071        let data = vec64![2.0f32, 4.0, 6.0];
3072        assert_eq!(sum_f32(&data, None, None), Some(12.0));
3073        assert_eq!(average_f32(&data, None, None), Some(4.0));
3074        let mask = mask_from_bools(&[true, false, true]);
3075        assert_eq!(sum_f32(&data, Some(&mask), Some(1)), Some(8.0));
3076        assert_eq!(average_f32(&data, Some(&mask), Some(1)), Some(4.0));
3077        assert!(variance_f32(&data, None, None, false).unwrap() > 0.0);
3078
3079        // f64
3080        let data = vec64![1.0f64, f64::INFINITY, f64::NAN];
3081        assert!(
3082            sum_f64(&data, None, None).unwrap().is_nan()
3083                || sum_f64(&data, None, None).unwrap().is_infinite()
3084        );
3085    }
3086
3087    // --- min/max/range: singleton, duplicates, negative, NaN, INF ---
3088    #[test]
3089    fn test_min_max_range_i32_singleton() {
3090        let d = vec64![1i32];
3091        assert_eq!(min_i32(&d, None, None), Some(1));
3092        assert_eq!(max_i32(&d, None, None), Some(1));
3093        assert_eq!(range_i32(&d, None, None), Some((1, 1)));
3094    }
3095
3096    #[test]
3097    fn test_min_f64_nan() {
3098        let d = vec64![5.0f64, f64::NAN];
3099        // we ignore NaNs, so the min of [5.0, NaN] is 5.0
3100        assert_eq!(min_f64(&d, None, None), Some(5.0));
3101    }
3102
3103    #[test]
3104    fn test_max_f64_nan() {
3105        let d = vec64![5.0f64, f64::NAN];
3106        // we ignore NaNs, so the max of [5.0, NaN] is 5.0
3107        assert_eq!(max_f64(&d, None, None), Some(5.0));
3108    }
3109
3110    #[test]
3111    fn test_min_f32_neg_inf() {
3112        let d = vec64![f32::NEG_INFINITY, 0.0f32, 5.0f32];
3113        assert_eq!(min_f32(&d, None, None), Some(f32::NEG_INFINITY));
3114    }
3115
3116    #[test]
3117    fn test_max_f32_regular() {
3118        let d = vec64![f32::NEG_INFINITY, 0.0f32, 5.0f32];
3119        assert_eq!(max_f32(&d, None, None), Some(5.0));
3120    }
3121
3122    #[test]
3123    fn test_median_ord_even() {
3124        let d = vec64![2i64, 4, 6, 8];
3125        // sorted ⇒ [2,4,6,8], lower median is 4
3126        // Keep integers as integers - with users able
3127        // to cast to floats as needed
3128        assert_eq!(median(&d, None, None, true), Some(4));
3129    }
3130
3131    #[test]
3132    fn test_median_ord_odd() {
3133        let d = vec64![7i32, 3, 5];
3134        assert_eq!(median(&d, None, None, true), Some(5));
3135    }
3136
3137    #[test]
3138    fn test_median_ord_with_mask() {
3139        let d = vec64![7i32, 3, 5];
3140        let mask = mask_from_bools(&[false, true, false]);
3141        assert_eq!(median(&d, Some(&mask), Some(2), true), Some(3));
3142    }
3143
3144    #[test]
3145    fn test_quantile_ord() {
3146        let d = vec64![10u32, 20, 30, 40, 50, 60];
3147        let q = quantile(&d, None, None, 3, true).unwrap();
3148        assert_eq!(q.len(), 2);
3149        assert!(q[0] >= 20 && q[1] >= 40);
3150    }
3151
3152    #[test]
3153    fn test_median_f64() {
3154        let df = vec64![1.0f64, 5.0, 3.0, 2.0];
3155        assert!(approx_eq(
3156            median_f(&df, None, None, true).unwrap(),
3157            2.5,
3158            1e-9
3159        ));
3160    }
3161
3162    #[test]
3163    fn test_quantile_f64() {
3164        let df = vec64![1.0f64, 5.0, 3.0, 2.0];
3165        let qf = quantile_f(&df, None, None, 3, true).unwrap();
3166        // we produce exactly q−1 = 2 interior tertile values
3167        assert_eq!(qf.len(), 2);
3168        assert!(qf.iter().all(|&v| v >= 1.0 && v <= 5.0));
3169    }
3170
3171    #[test]
3172    fn test_iqr_i32() {
3173        let d = vec64![1i32, 3, 2, 8, 4, 5];
3174        assert!(iqr(&d, None, None, true).unwrap() >= 2);
3175    }
3176
3177    #[test]
3178    fn test_iqr_f32() {
3179        let d = vec64![10.0f32, 100.0, 30.0, 50.0];
3180        assert!(iqr_f(&d, None, None, true).unwrap() > 0.0);
3181    }
3182
3183    // --- mode & count_distinct: ties, all unique, all equal, NaN/Inf ---
3184    #[test]
3185    fn test_mode_and_count_distinct_exhaustive() {
3186        // Mode: all unique
3187        let d = vec64![7u64, 5, 3, 1];
3188        assert!(d.contains(&mode(&d, None, None).unwrap()));
3189
3190        // Mode: all equal
3191        let d = vec64![2i64; 6];
3192        assert_eq!(mode(&d, None, None), Some(2));
3193
3194        // Mode: tie
3195        let d = vec64![3i32, 5, 3, 5];
3196        let m = mode(&d, None, None);
3197        assert!(m == Some(3) || m == Some(5));
3198
3199        // Mode: NaN in floats, inf, repeated
3200        let d = vec64![f32::NAN, f32::INFINITY, 5.0, 5.0, f32::NAN];
3201        let m = mode_f(&d, None, None);
3202        assert!(m == Some(5.0) || m.unwrap().is_nan() || m.unwrap().is_infinite());
3203
3204        // Count distinct: all equal, all unique, mask
3205        let d = vec64![1u64, 1, 1, 1];
3206        assert_eq!(count_distinct(&d, None, None), 1);
3207
3208        let d = vec64![1i32, 2, 3, 4, 5];
3209        assert_eq!(count_distinct(&d, None, None), 5);
3210
3211        let mask = mask_from_bools(&[true, false, false, true, true]);
3212        assert_eq!(count_distinct(&d, Some(&mask), Some(2)), 3);
3213
3214        let d = vec64![f64::NAN, f64::INFINITY, 0.0];
3215        assert_eq!(count_distinct_f(&d, None, None), 3);
3216    }
3217
3218    // --- harmonic & geometric means: all types, zero/negative/NaN ---
3219    #[test]
3220    fn test_harmonic_mean_f64() {
3221        let data = vec64![0.5f64, 2.0, 1.0];
3222        // the true harmonic mean is 3/(2 + 0.5 + 1) ≈ 0.8571
3223        let hm = harmonic_mean_f(&data, None, None);
3224        assert!(approx_eq(hm, 0.857142857, 1e-2));
3225    }
3226
3227    #[test]
3228    fn test_geometric_mean_f64() {
3229        let data = vec64![0.5f64, 2.0, 1.0];
3230        let gm = geometric_mean_f(&data, None, None);
3231        assert!(approx_eq(gm, 1.0, 1e-9));
3232    }
3233
3234    #[test]
3235    #[should_panic(expected = "harmonic_mean_int: non-positive values are invalid")]
3236    fn test_harmonic_mean_int_all_zeros() {
3237        let d = vec64![0i32, 0, 0];
3238        // Panics due to non-positive values.
3239        let _ = harmonic_mean_int(&d, None, None);
3240    }
3241
3242    #[test]
3243    fn test_geometric_mean_uint() {
3244        let data = vec64![2u64, 8, 4];
3245        let gm = geometric_mean_uint(&data, None, None);
3246        assert!(approx_eq(gm, 4.0, 1e-9));
3247    }
3248
3249    #[test]
3250    #[should_panic(expected = "geometric_mean_int: non-positive values are invalid")]
3251    fn test_geometric_mean_int_negative() {
3252        // Should panic on negative input, even if result is complex
3253        let d = vec64![-2i64, -4, -8];
3254        let _ = geometric_mean_int(&d, None, None);
3255    }
3256
3257    // --- percentile: boundary and empty, floats with mask, p out of range ---
3258    #[test]
3259    fn test_percentile_ord_and_f_all_cases() {
3260        let d = vec64![1i32, 4, 6, 8, 10];
3261        assert_eq!(percentile_ord(&d, None, None, 0.0, true), Some(1));
3262        assert_eq!(percentile_ord(&d, None, None, 100.0, true), Some(10));
3263        let mask = mask_from_bools(&[false, false, false, false, false]);
3264        assert_eq!(percentile_ord(&d, Some(&mask), Some(5), 50.0, true), None);
3265
3266        let df = vec64![1.0f64, 2.0, 3.0, 4.0, 5.0];
3267        assert_eq!(percentile_f(&df, None, None, 0.0, true), Some(1.0));
3268        assert_eq!(percentile_f(&df, None, None, 1.0, true), Some(5.0));
3269        let mask = mask_from_bools(&[false, false, false, false, false]);
3270        assert_eq!(percentile_f(&df, Some(&mask), Some(5), 0.5, true), None);
3271
3272        // Out of range p (should clamp)
3273        assert_eq!(percentile_f(&df, None, None, 10.0, true), Some(5.0));
3274        assert_eq!(percentile_f(&df, None, None, -5.0, true), Some(1.0));
3275    }
3276
3277    // --- quantile: small q, empty, all-null, mask with only one valid ---
3278    #[test]
3279    fn test_quantile_f_and_ord_special_cases() {
3280        let d = vec64![2.0f32, 4.0, 6.0, 8.0];
3281        // q < 2 (should return None)
3282        assert_eq!(quantile_f(&d, None, None, 1, true), None);
3283        // all nulls
3284        let mask = mask_from_bools(&[false, false, false, false]);
3285        assert_eq!(quantile_f(&d, Some(&mask), Some(4), 2, true), None);
3286
3287        // mask: only one valid
3288        let mask = mask_from_bools(&[false, false, true, false]);
3289        let out = quantile_f(&d, Some(&mask), Some(3), 2, true).unwrap();
3290        assert_eq!(out.len(), 1);
3291        assert!(out.iter().all(|&x| x == 6.0));
3292    }
3293
3294    // --- Test for proper handling of singletons, all-nulls, degenerate input for all aggregation APIs ---
3295    #[test]
3296    fn test_all_apis_singleton_and_nulls() {
3297        let d = vec64![7u32];
3298        assert_eq!(sum_u32(&d, None, None), Some(7));
3299        assert_eq!(average_u32(&d, None, None), Some(7.0));
3300        assert_eq!(variance_u32(&d, None, None, false).unwrap(), 0.0);
3301        assert_eq!(mode(&d, None, None), Some(7));
3302        assert_eq!(median(&d, None, None, true), Some(7));
3303        assert_eq!(
3304            quantile(&d, None, None, 3, true).unwrap(),
3305            Vec64::from_slice(&[7, 7])
3306        );
3307        assert_eq!(iqr(&d, None, None, true), Some(0));
3308
3309        let mask = mask_from_bools(&[false]);
3310        assert_eq!(sum_u32(&d, Some(&mask), Some(1)), None);
3311        assert_eq!(median(&d, Some(&mask), Some(1), true), None);
3312        assert_eq!(quantile(&d, Some(&mask), Some(1), 3, true), None);
3313        assert_eq!(mode(&d, Some(&mask), Some(1)), None);
3314        assert_eq!(iqr(&d, Some(&mask), Some(1), true), None);
3315    }
3316
3317    #[test]
3318    fn test_skewness_kurtosis_dense() {
3319        // Data 1..=5 → mean 3, symmetric
3320        // population skewness = 0, population excess kurtosis = -1.3
3321        // sample skewness = 0, sample excess kurtosis = -1.2
3322        let d_f64 = vec64![1.0f64, 2.0, 3.0, 4.0, 5.0];
3323        // population
3324        assert!(approx_eq(
3325            skewness_f64(&d_f64, None, None, false).unwrap(),
3326            0.0,
3327            1e-12
3328        ));
3329        assert!(approx_eq(
3330            kurtosis_f64(&d_f64, None, None, false).unwrap(),
3331            -1.3,
3332            1e-12
3333        ));
3334        // sample
3335        assert!(approx_eq(
3336            skewness_f64(&d_f64, None, None, true).unwrap(),
3337            0.0,
3338            1e-12
3339        ));
3340        assert!(approx_eq(
3341            kurtosis_f64(&d_f64, None, None, true).unwrap(),
3342            -1.2,
3343            1e-12
3344        ));
3345
3346        let d_f32 = vec64![1.0f32, 2.0, 3.0, 4.0, 5.0];
3347        // population
3348        assert!(approx_eq(
3349            skewness_f32(&d_f32, None, None, false).unwrap(),
3350            0.0,
3351            1e-6
3352        ));
3353        assert!(approx_eq(
3354            kurtosis_f32(&d_f32, None, None, false).unwrap(),
3355            -1.3,
3356            1e-6
3357        ));
3358        // sample
3359        assert!(approx_eq(
3360            skewness_f32(&d_f32, None, None, true).unwrap(),
3361            0.0,
3362            1e-6
3363        ));
3364        assert!(approx_eq(
3365            kurtosis_f32(&d_f32, None, None, true).unwrap(),
3366            -1.2,
3367            1e-6
3368        ));
3369
3370        let d_i32 = vec64![1i32, 2, 3, 4, 5];
3371        assert!(approx_eq(
3372            skewness_i32(&d_i32, None, None, false).unwrap(),
3373            0.0,
3374            1e-12
3375        ));
3376        assert!(approx_eq(
3377            kurtosis_i32(&d_i32, None, None, false).unwrap(),
3378            -1.3,
3379            1e-12
3380        ));
3381        assert!(approx_eq(
3382            skewness_i32(&d_i32, None, None, true).unwrap(),
3383            0.0,
3384            1e-12
3385        ));
3386        assert!(approx_eq(
3387            kurtosis_i32(&d_i32, None, None, true).unwrap(),
3388            -1.2,
3389            1e-12
3390        ));
3391    }
3392
3393    #[test]
3394    fn test_skewness_kurtosis_masked() {
3395        // Mask out 1 & 5 → [2,3,4]
3396        // population skewness = 0, population & sample excess kurtosis = -1.5
3397        let data = vec64![1i32, 2, 3, 4, 5];
3398        let mask = mask_from_bools(&[false, true, true, true, false]);
3399        // population
3400        assert!(approx_eq(
3401            skewness_i32(&data, Some(&mask), Some(2), false).unwrap(),
3402            0.0,
3403            1e-12
3404        ));
3405        assert!(approx_eq(
3406            kurtosis_i32(&data, Some(&mask), Some(2), false).unwrap(),
3407            -1.5,
3408            1e-12
3409        ));
3410        // sample
3411        assert!(approx_eq(
3412            skewness_i32(&data, Some(&mask), Some(2), true).unwrap(),
3413            0.0,
3414            1e-12
3415        ));
3416        assert!(approx_eq(
3417            kurtosis_i32(&data, Some(&mask), Some(2), true).unwrap(),
3418            -1.5,
3419            1e-12
3420        ));
3421    }
3422
3423    #[test]
3424    fn test_skewness_kurtosis_constant_none() {
3425        // Variance zero → expect None
3426        let d = vec64![7u32; 5];
3427        assert_eq!(skewness_u32(&d, None, None, false), None);
3428        assert_eq!(kurtosis_u32(&d, None, None, false), None);
3429        assert_eq!(skewness_u32(&d, None, None, true), None);
3430        assert_eq!(kurtosis_u32(&d, None, None, true), None);
3431    }
3432
3433    #[test]
3434    fn test_kurtosis_skewness_large_f32() {
3435        // Mixed negative/positive of length 11:
3436        // population: skew ≈ -0.20081265, kurt ≈ -1.28414694
3437        // sample:     skew ≈ -0.23401565, kurt ≈ -1.30691156
3438        let data = vec64![-2.0, -1.0, -1.0, 0.0, 0.0, 1.0, 2.0, 2.0, 2.0, 3.0, 3.0];
3439        // population
3440        assert!(approx_eq(
3441            skewness_f32(&data, None, None, false).unwrap(),
3442            -0.20081265422848577_f64,
3443            1e-12
3444        ));
3445        assert!(approx_eq(
3446            kurtosis_f32(&data, None, None, false).unwrap(),
3447            -1.2841469381610144_f64,
3448            1e-12
3449        ));
3450        // sample
3451        assert!(approx_eq(
3452            skewness_f32(&data, None, None, true).unwrap(),
3453            -0.23401565397707677_f64,
3454            1e-12
3455        ));
3456        assert!(approx_eq(
3457            kurtosis_f32(&data, None, None, true).unwrap(),
3458            -1.3069115636016906_f64,
3459            1e-12
3460        ));
3461    }
3462
3463    fn bitmask_from_bools(bs: &[bool]) -> Bitmask {
3464        let mut mask = Bitmask::with_capacity(bs.len());
3465        for (i, &b) in bs.iter().enumerate() {
3466            mask.set(i, b);
3467        }
3468        mask
3469    }
3470
3471    #[test]
3472    fn test_kurtosis_skewness_large_masked_i32() {
3473        // Drop 4th element (4), leaving 9 values:
3474        // population: skew ≈ 2.43067998, kurt ≈ 3.99120344
3475        // sample:     skew ≈ 2.94642909, kurt ≈ 8.74514940
3476        let data = vec64![1, 2, 3, 4, 5, 6, 7, 100, 1000, 10000];
3477        let mask = mask_from_bools(&[true, true, true, false, true, true, true, true, true, true]);
3478        // population
3479        assert!(approx_eq(
3480            skewness_i32(&data, Some(&mask), None, false).unwrap(),
3481            2.4306799843134046_f64,
3482            1e-12
3483        ));
3484        assert!(approx_eq(
3485            kurtosis_i32(&data, Some(&mask), None, false).unwrap(),
3486            3.991203437112363_f64,
3487            1e-12
3488        ));
3489        // sample
3490        assert!(approx_eq(
3491            skewness_i32(&data, Some(&mask), None, true).unwrap(),
3492            2.946429085375575_f64,
3493            1e-12
3494        ));
3495        assert!(approx_eq(
3496            kurtosis_i32(&data, Some(&mask), None, true).unwrap(),
3497            8.745149404023547_f64,
3498            1e-12
3499        ));
3500    }
3501
3502    #[test]
3503    fn test_kurtosis_skewness_no_valids() {
3504        let data: [f64; 3] = [0.0, 0.0, 0.0];
3505        let mask = bitmask_from_bools(&[false, false, false]);
3506        assert_eq!(kurtosis_f64(&data, Some(&mask), None, false), None);
3507        assert_eq!(skewness_f64(&data, Some(&mask), None, false), None);
3508        assert_eq!(kurtosis_f64(&data, Some(&mask), None, true), None);
3509        assert_eq!(skewness_f64(&data, Some(&mask), None, true), None);
3510    }
3511
3512    #[test]
3513    fn test_kurtosis_skewness_single_value() {
3514        let data = vec64![1u32];
3515        assert_eq!(kurtosis_u32(&data, None, None, false), None);
3516        assert_eq!(skewness_u32(&data, None, None, false), None);
3517        assert_eq!(kurtosis_u32(&data, None, None, true), None);
3518        assert_eq!(skewness_u32(&data, None, None, true), None);
3519    }
3520
3521    #[test]
3522    fn test_kurtosis_skewness_all_equal() {
3523        let data = vec64![7.0, 7.0, 7.0, 7.0];
3524        assert_eq!(kurtosis_f64(&data, None, None, false), None);
3525        assert_eq!(skewness_f64(&data, None, None, false), None);
3526        assert_eq!(kurtosis_f64(&data, None, None, true), None);
3527        assert_eq!(skewness_f64(&data, None, None, true), None);
3528    }
3529
3530    #[test]
3531    fn test_kurtosis_skewness_mixed_nulls() {
3532        // Valid [2.0,7.0,10.0] (length 3):
3533        // population: skew ≈ -0.29479962, kurt = -1.5
3534        // sample:     skew ≈ -0.72210865, kurt = -1.5
3535        let data = vec64![2.0, 5.0, 7.0, 3.0, 10.0];
3536        let mask = mask_from_bools(&[true, false, true, false, true]);
3537        // population
3538        assert!(approx_eq(
3539            skewness_f64(&data, Some(&mask), None, false).unwrap(),
3540            -0.29479962014482897_f64,
3541            1e-12
3542        ));
3543        assert!(approx_eq(
3544            kurtosis_f64(&data, Some(&mask), None, false).unwrap(),
3545            -1.5_f64,
3546            1e-12
3547        ));
3548        // sample
3549        assert!(approx_eq(
3550            skewness_f64(&data, Some(&mask), None, true).unwrap(),
3551            -0.7221086457211346_f64,
3552            1e-12
3553        ));
3554        assert!(approx_eq(
3555            kurtosis_f64(&data, Some(&mask), None, true).unwrap(),
3556            -1.5_f64,
3557            1e-12
3558        ));
3559    }
3560
3561    #[test]
3562    fn test_kurtosis_skewness_integers_large() {
3563        let data = vec64![1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9];
3564        assert!(kurtosis_i32(&data, None, None, false).unwrap().abs() < 1.5);
3565        assert!(skewness_i32(&data, None, None, false).unwrap().abs() < 1e-6);
3566        // Sample
3567        assert!(kurtosis_i32(&data, None, None, true).unwrap().abs() < 2.0);
3568        assert!(skewness_i32(&data, None, None, true).unwrap().abs() < 1e-6);
3569    }
3570
3571    #[test]
3572    fn test_agg_product_dense() {
3573        // No mask, all values included
3574        let data = vec64![2, 3, 4, 5];
3575        let prod = agg_product(&data, None, 0, data.len());
3576        assert_eq!(prod, 120);
3577
3578        // Offset/len window
3579        let prod = agg_product(&data, None, 1, 2);
3580        assert_eq!(prod, 12); // 3*4
3581    }
3582
3583    #[test]
3584    fn test_agg_product_with_mask_all_valid() {
3585        let data = vec64![2, 3, 4, 5];
3586        let mask = Bitmask::new_set_all(4, true);
3587        let prod = agg_product(&data, Some(&mask), 0, 4);
3588        assert_eq!(prod, 120);
3589    }
3590
3591    #[test]
3592    fn test_agg_product_with_mask_some_nulls() {
3593        let data = vec64![2, 3, 4, 5];
3594        let mut mask = Bitmask::new_set_all(4, false);
3595        mask.set(0, true);
3596        mask.set(2, true);
3597        // Only data[0] and data[2] included: 2*4=8
3598        let prod = agg_product(&data, Some(&mask), 0, 4);
3599        assert_eq!(prod, 8);
3600    }
3601
3602    #[test]
3603    fn test_agg_product_zero_short_circuit() {
3604        let data = vec64![2, 0, 4, 5];
3605        let prod = agg_product(&data, None, 0, 4);
3606        assert_eq!(prod, 0);
3607    }
3608
3609    #[test]
3610    fn test_agg_product_empty() {
3611        let data: [i32; 0] = [];
3612        let prod = agg_product(&data, None, 0, 0);
3613        assert_eq!(prod, 1); // One by convention
3614    }
3615
3616    #[test]
3617    fn test_agg_product_mask_all_null() {
3618        let data = vec64![2, 3, 4, 5];
3619        let mask = Bitmask::new_set_all(4, false);
3620        let prod = agg_product(&data, Some(&mask), 0, 4);
3621        assert_eq!(prod, 1); // No values included: returns one
3622    }
3623
3624    #[test]
3625    fn test_agg_product_offset_and_mask() {
3626        let data = vec64![2, 3, 4, 5, 6];
3627        let mut mask = Bitmask::new_set_all(5, false);
3628        mask.set(1, true);
3629        mask.set(3, true);
3630        let prod = agg_product(&data, Some(&mask), 1, 3);
3631        assert_eq!(prod, 15);
3632    }
3633
3634    #[test]
3635    fn test_sum_squares_empty() {
3636        let v: [f64; 0] = [];
3637        assert_eq!(sum_squares(&v, None, None), 0.0);
3638    }
3639
3640    #[test]
3641    fn test_sum_squares_basic() {
3642        let v = vec64![1.0, 2.0, 3.0];
3643        assert_eq!(sum_squares(&v, None, None), 1.0 + 4.0 + 9.0);
3644    }
3645
3646    #[test]
3647    fn test_sum_squares_neg() {
3648        let v = vec64![-1.0, -2.0, -3.0];
3649        assert_eq!(sum_squares(&v, None, None), 1.0 + 4.0 + 9.0);
3650    }
3651
3652    #[test]
3653    fn test_sum_squares_large() {
3654        let v: Vec<f64> = (0..1000).map(|x| x as f64).collect();
3655        let expect: f64 = v.iter().map(|x| x * x).sum();
3656        assert!((sum_squares(&v, None, None) - expect).abs() < 1e-9);
3657    }
3658
3659    #[test]
3660    fn test_sum_squares_masked() {
3661        let v = vec64![1.0, 2.0, 3.0, 4.0];
3662        let mask = Bitmask::from_bools(&[true, false, true, false]);
3663        assert_eq!(sum_squares(&v, Some(&mask), Some(2)), 1.0 + 9.0);
3664    }
3665
3666    #[test]
3667    fn test_sum_cubes_empty() {
3668        let v: [f64; 0] = [];
3669        assert_eq!(sum_cubes(&v, None, None), 0.0);
3670    }
3671
3672    #[test]
3673    fn test_sum_cubes_basic() {
3674        let v = vec64![1.0, 2.0, 3.0];
3675        assert_eq!(sum_cubes(&v, None, None), 1.0 + 8.0 + 27.0);
3676    }
3677
3678    #[test]
3679    fn test_sum_cubes_neg() {
3680        let v = vec64![-1.0, -2.0, -3.0];
3681        assert_eq!(sum_cubes(&v, None, None), -1.0 - 8.0 - 27.0);
3682    }
3683
3684    #[test]
3685    fn test_sum_cubes_large() {
3686        let v: Vec<f64> = (0..1000).map(|x| x as f64).collect();
3687        let expect: f64 = v.iter().map(|x| x * x * x).sum();
3688        assert!((sum_cubes(&v, None, None) - expect).abs() < 1e-6);
3689    }
3690
3691    #[test]
3692    fn test_sum_cubes_masked() {
3693        let v = vec64![1.0, 2.0, 3.0, 4.0];
3694        let mask = Bitmask::from_bools(&[true, true, false, true]);
3695        assert_eq!(sum_cubes(&v, Some(&mask), Some(1)), 1.0 + 8.0 + 64.0);
3696    }
3697
3698    #[test]
3699    fn test_sum_quartics_empty() {
3700        let v: [f64; 0] = [];
3701        assert_eq!(sum_quartics(&v, None, None), 0.0);
3702    }
3703
3704    #[test]
3705    fn test_sum_quartics_basic() {
3706        let v = vec64![1.0, 2.0, 3.0];
3707        assert_eq!(sum_quartics(&v, None, None), 1.0 + 16.0 + 81.0);
3708    }
3709
3710    #[test]
3711    fn test_sum_quartics_neg() {
3712        let v = vec64![-1.0, -2.0, -3.0];
3713        assert_eq!(sum_quartics(&v, None, None), 1.0 + 16.0 + 81.0);
3714    }
3715
3716    #[test]
3717    fn test_sum_quartics_large() {
3718        let v: Vec<f64> = (0..1000).map(|x| x as f64).collect();
3719        let expect: f64 = v
3720            .iter()
3721            .map(|x| {
3722                let x2 = x * x;
3723                x2 * x2
3724            })
3725            .sum();
3726        assert!((sum_quartics(&v, None, None) - expect).abs() < 1e-3);
3727    }
3728
3729    #[test]
3730    fn test_sum_quartics_masked() {
3731        let v = vec64![1.0, 2.0, 3.0, 4.0];
3732        let mask = Bitmask::from_bools(&[false, true, false, true]);
3733        assert_eq!(sum_quartics(&v, Some(&mask), Some(2)), 16.0 + 256.0);
3734    }
3735
3736    #[test]
3737    fn test_sum_powers_singleton() {
3738        let v = vec64![4.2];
3739        assert!((sum_squares(&v, None, None) - 4.2 * 4.2).abs() < 1e-12);
3740        assert!((sum_cubes(&v, None, None) - 4.2 * 4.2 * 4.2).abs() < 1e-12);
3741        assert!((sum_quartics(&v, None, None) - (4.2 * 4.2 * 4.2 * 4.2)).abs() < 1e-12);
3742    }
3743
3744    #[test]
3745    fn test_sum_powers_f64_extremes() {
3746        let v = vec64![f64::MAX, -f64::MAX, 0.0, f64::MIN_POSITIVE];
3747        let ss = sum_squares(&v, None, None).is_infinite();
3748        let sc = sum_cubes(&v, None, None).is_infinite();
3749        let sq = sum_quartics(&v, None, None).is_infinite();
3750        println!("{:?}", ss);
3751        println!("{:?}", sc);
3752        println!("{:?}", sq);
3753        // f64::MAX * f64::MAX is inf, so expect inf for all powers >1
3754        assert!(sum_squares(&v, None, None).is_infinite());
3755        assert!(sum_cubes(&v, None, None).is_infinite());
3756        assert!(sum_quartics(&v, None, None).is_infinite());
3757    }
3758}