Skip to main content

nu_command/math/
variance.rs

1use crate::math::utils::{
2    NUMBER_INPUT_TYPES, NumericUnit, expand_range_input, run_with_function,
3    run_with_function_and_cell_paths, to_unit_f64, variance_denominator,
4};
5use nu_engine::command_prelude::*;
6
7#[derive(Clone)]
8pub struct MathVariance;
9
10impl Command for MathVariance {
11    fn name(&self) -> &str {
12        "math variance"
13    }
14
15    fn signature(&self) -> Signature {
16        Signature::build("math variance")
17            .input_output_types(vec![
18                (Type::List(Box::new(Type::Number)), Type::Number),
19                (Type::List(Box::new(Type::Duration)), Type::Number),
20                (Type::List(Box::new(Type::Filesize)), Type::Number),
21                (Type::Range, Type::Number),
22                (Type::table(), Type::record()),
23                (Type::record(), Type::record()),
24            ])
25            .switch(
26                "sample",
27                "Calculate sample variance (i.e. using N-1 as the denominator).",
28                Some('s'),
29            )
30            .rest(
31                "columns",
32                SyntaxShape::CellPath,
33                "The cell-paths/columns to operate on.",
34            )
35            .allow_variants_without_examples(true)
36            .category(Category::Math)
37    }
38
39    fn description(&self) -> &str {
40        "Returns the variance of a list of numbers or of each column in a table."
41    }
42
43    fn extra_description(&self) -> &str {
44        "For filesize and duration inputs, variance is computed in base units \
45         (bytes and nanoseconds) and returned as a plain number. There is no \
46         squared unit type in Nushell, so the result is the variance of the \
47         underlying byte or nanosecond values (B² or ns²), not of the display \
48         unit used when the values were written."
49    }
50
51    fn search_terms(&self) -> Vec<&str> {
52        vec!["deviation", "dispersion", "variation", "statistics"]
53    }
54
55    fn is_const(&self) -> bool {
56        true
57    }
58
59    fn run(
60        &self,
61        engine_state: &EngineState,
62        stack: &mut Stack,
63        call: &Call,
64        input: PipelineData,
65    ) -> Result<PipelineData, ShellError> {
66        let cell_paths: Vec<CellPath> = call.rest(engine_state, stack, 0)?;
67        let sample = call.has_flag(engine_state, stack, "sample")?;
68        let mf = compute_variance(sample);
69        if cell_paths.is_empty() {
70            let input = expand_range_input(input, call.head)?;
71            return run_with_function(call, input, mf);
72        }
73        run_with_function_and_cell_paths(call, input, cell_paths, engine_state.signals(), mf)
74    }
75
76    fn run_const(
77        &self,
78        working_set: &StateWorkingSet,
79        call: &Call,
80        input: PipelineData,
81    ) -> Result<PipelineData, ShellError> {
82        let cell_paths: Vec<CellPath> = call.rest_const(working_set, 0)?;
83        let sample = call.has_flag_const(working_set, "sample")?;
84        let mf = compute_variance(sample);
85        if cell_paths.is_empty() {
86            let input = expand_range_input(input, call.head)?;
87            return run_with_function(call, input, mf);
88        }
89        run_with_function_and_cell_paths(
90            call,
91            input,
92            cell_paths,
93            working_set.permanent().signals(),
94            mf,
95        )
96    }
97
98    fn examples(&self) -> Vec<Example<'_>> {
99        vec![
100            Example {
101                description: "Get the variance of a list of numbers.",
102                example: "[1 2 3 4 5] | math variance",
103                result: Some(Value::test_float(2.0)),
104            },
105            Example {
106                description: "Get the sample variance of a list of numbers.",
107                example: "[1 2 3 4 5] | math variance --sample",
108                result: Some(Value::test_float(2.5)),
109            },
110            Example {
111                description: "Compute the variance of each column in a table.",
112                example: "[[a b]; [1 2] [3 4]] | math variance",
113                result: Some(Value::test_record(record! {
114                    "a" => Value::test_int(1),
115                    "b" => Value::test_int(1),
116                })),
117            },
118            Example {
119                description: "Compute the variance of list-valued columns in a record.",
120                example: "{alice: [1 3], bob: [4 6]} | math variance",
121                result: Some(Value::test_record(record! {
122                    "alice" => Value::test_int(1),
123                    "bob" => Value::test_int(1),
124                })),
125            },
126            Example {
127                description: "Compute the variance of a single column using a cell path.",
128                example: "{alice: [1 3], bob: [4 6]} | math variance alice",
129                result: Some(Value::test_record(record! {
130                    "alice" => Value::test_int(1),
131                    "bob" => Value::list(
132                        vec![Value::test_int(4), Value::test_int(6)],
133                        Span::test_data(),
134                    ),
135                })),
136            },
137            Example {
138                // 1KB=1000B, 3KB=3000B; population variance is 1_000_000 (B²), not 1 (KB²).
139                description: "Variance of filesizes is a number of base units squared (bytes²).",
140                example: "[1KB 3KB] | math variance",
141                result: Some(Value::test_float(1_000_000.0)),
142            },
143        ]
144    }
145}
146
147fn sum_of_squares(values: &[Value], span: Span, head: Span) -> Result<Value, ShellError> {
148    let n = Value::int(values.len() as i64, span);
149    let mut sum_x = Value::int(0, span);
150    let mut sum_x2 = Value::int(0, span);
151    for value in values {
152        let v = match &value {
153            Value::Int { .. } | Value::Float { .. } => value.clone(),
154            Value::Error { error, .. } => return Err(*error.clone()),
155            other => {
156                return Err(ShellError::OnlySupportsThisInputType {
157                    exp_input_type: NUMBER_INPUT_TYPES.into(),
158                    wrong_type: other.get_type().to_string(),
159                    dst_span: head,
160                    src_span: other.span(),
161                });
162            }
163        };
164        let v_squared = &v.mul(span, &v, span)?;
165        sum_x2 = sum_x2.add(span, v_squared, span)?;
166        sum_x = sum_x.add(span, &v, span)?;
167    }
168
169    let sum_x_squared = sum_x.mul(span, &sum_x, span)?;
170    let sum_x_squared_div_n = sum_x_squared.div(span, &n, span)?;
171
172    let ss = sum_x2.sub(span, &sum_x_squared_div_n, span)?;
173
174    Ok(ss)
175}
176
177/// Variance for duration/filesize via `f64` units (ns / bytes) to avoid i64 overflow
178/// when squaring large values such as multi-second durations.
179fn variance_unit_f64(
180    values: &[Value],
181    sample: bool,
182    span: Span,
183    head: Span,
184) -> Result<f64, ShellError> {
185    let mut nums = Vec::with_capacity(values.len());
186    for value in values {
187        let (_, n) = to_unit_f64(value, head)?;
188        nums.push(n);
189    }
190    let denom = variance_denominator(nums.len(), sample, head, span)? as f64;
191    let mean = nums.iter().sum::<f64>() / nums.len() as f64;
192    let ss = nums
193        .iter()
194        .map(|x| {
195            let d = x - mean;
196            d * d
197        })
198        .sum::<f64>();
199    Ok(ss / denom)
200}
201
202pub fn compute_variance(
203    sample: bool,
204) -> impl Fn(&[Value], Span, Span) -> Result<Value, ShellError> {
205    move |values: &[Value], span: Span, head: Span| {
206        let unit = values_unit(values, head)?;
207        let denom = variance_denominator(values.len(), sample, head, span)?;
208        match unit {
209            // Duration/filesize: compute in f64 so large units (e.g. seconds as ns) don't overflow.
210            // Result is a plain number (squared units).
211            NumericUnit::Duration | NumericUnit::Filesize => {
212                let var = variance_unit_f64(values, sample, span, head)?;
213                Ok(Value::float(var, span))
214            }
215            NumericUnit::Number => {
216                // Arithmetic uses the original value span; errors point at the call head.
217                let ss = sum_of_squares(values, span, head)?;
218                let n = Value::int(denom as i64, head);
219                ss.div(head, &n, head)
220            }
221        }
222    }
223}
224
225/// Determine the common unit of a value list for re-wrapping stddev results.
226pub fn values_unit(values: &[Value], head: Span) -> Result<NumericUnit, ShellError> {
227    let mut unit = NumericUnit::Number;
228    for (i, value) in values.iter().enumerate() {
229        let (this_unit, _) = to_unit_f64(value, head)?;
230        if i == 0 {
231            unit = this_unit;
232            continue;
233        }
234        // int and float may mix; duration/filesize must stay homogeneous.
235        match (unit, this_unit) {
236            (NumericUnit::Number, NumericUnit::Number) => {}
237            (a, b) if a == b => {}
238            (a, _) => {
239                return Err(ShellError::OnlySupportsThisInputType {
240                    exp_input_type: a.as_str().into(),
241                    wrong_type: value.get_type().to_string(),
242                    dst_span: head,
243                    src_span: value.span(),
244                });
245            }
246        }
247    }
248    Ok(unit)
249}
250
251#[cfg(test)]
252mod test {
253    use super::*;
254
255    #[test]
256    fn test_examples() -> nu_test_support::Result {
257        nu_test_support::test().examples(MathVariance)
258    }
259}