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 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
177fn 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 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 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
225pub 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 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}