1use bicmath_core::context::ExecContext;
8use bicmath_core::contract::{
9 Args, CostClass, Example, FunctionDescriptor, Outcome, ParamDescriptor, SimpleFunction,
10 require_mode,
11};
12use bicmath_core::envelope::Exactness;
13use bicmath_core::error::{EngineError, ErrorCode};
14use bicmath_core::number::{Number, NumericMode};
15use bicmath_core::schema::ValueSchema;
16use bicmath_core::value::Value;
17use num_bigint::BigInt;
18use num_rational::BigRational;
19use num_traits::Zero;
20use std::sync::Arc;
21
22use crate::common::*;
23use crate::mathfn::sqrt;
24
25fn moment_value(moment: &Moment) -> Result<Value, EngineError> {
26 match moment {
27 Moment::Exact(value) => Ok(rational_value(value.clone())),
28 Moment::Float(value) => float_value(*value),
29 }
30}
31
32fn count_descriptor() -> FunctionDescriptor {
37 FunctionDescriptor::new(
38 "statistics.count",
39 "statistics",
40 "1.0.0",
41 "Count",
42 "Count the observations in an array.",
43 )
44 .with_description(
45 "Returns the number of entries. Every entry must be a finite number; non-numeric or \
46 non-finite entries are rejected rather than silently skipped.",
47 )
48 .with_parameters(vec![ParamDescriptor::required(
49 "values",
50 "Observations to count.",
51 array_schema(any_number_schema()),
52 )])
53 .with_output(integer_schema(), "Number of observations.")
54 .with_modes(all_modes())
55 .with_cost(CostClass::Linear)
56 .with_method_ref("docs/methods/statistics.md#count")
57 .with_examples(vec![
58 Example::new(
59 "count three values",
60 example_args(&[("values", serde_json::json!([1, 2, 3]))]),
61 )
62 .with_value(parse_value(
63 serde_json::json!({"kind": "integer", "value": "3"}),
64 )),
65 ])
66}
67
68fn invoke_count(args: &Args, _ctx: &ExecContext) -> Result<Outcome, EngineError> {
69 let values = args.array("values")?;
70 for (index, item) in values.iter().enumerate() {
71 match item {
72 Value::Number(number) => {
73 ensure_finite(number).map_err(|e| e.with_path(format!("values[{index}]")))?;
74 }
75 other => {
76 return Err(EngineError::malformed(format!(
77 "expected a number at values[{index}], found {}",
78 other.kind_name()
79 ))
80 .with_path(format!("values[{index}]")));
81 }
82 }
83 }
84 Ok(Outcome::exact(Value::integer(BigInt::from(values.len()))))
85}
86
87fn mean_descriptor() -> FunctionDescriptor {
92 FunctionDescriptor::new(
93 "statistics.mean",
94 "statistics",
95 "1.0.0",
96 "Arithmetic mean",
97 "Exact arithmetic mean of a non-empty array.",
98 )
99 .with_description(
100 "Integer, rational, and decimal inputs produce an exact integer or rational mean. \
101 Float64 inputs require auto or scientific mode, are computed in binary64, and are \
102 marked approximate. An empty array is rejected with insufficient_observations.",
103 )
104 .with_parameters(vec![ParamDescriptor::required(
105 "values",
106 "Observations.",
107 array_schema(any_number_schema()),
108 )])
109 .with_output(any_number_schema(), "Arithmetic mean.")
110 .with_modes(all_modes())
111 .with_cost(CostClass::Linear)
112 .with_method_ref("docs/methods/statistics.md#mean")
113 .with_examples(vec![
114 Example::new(
115 "exact integer mean",
116 example_args(&[("values", serde_json::json!([1, 2, 3]))]),
117 )
118 .with_value(parse_value(
119 serde_json::json!({"kind": "integer", "value": "2"}),
120 )),
121 Example::new(
122 "exact rational mean",
123 example_args(&[("values", serde_json::json!([1, 2]))]),
124 )
125 .with_value(parse_value(
126 serde_json::json!({"kind": "rational", "numerator": "3", "denominator": "2"}),
127 )),
128 Example::new(
129 "empty array",
130 example_args(&[("values", serde_json::json!([]))]),
131 )
132 .with_error(ErrorCode::InsufficientObservations),
133 ])
134}
135
136fn invoke_mean(args: &Args, ctx: &ExecContext) -> Result<Outcome, EngineError> {
137 let series = classify_series(args, "values", ctx)?;
138 let moment = mean_moment(&series)?;
139 match moment {
140 Moment::Exact(value) => Ok(Outcome::exact(rational_value(value))),
141 Moment::Float(value) => Ok(Outcome::approximate(float_value(value)?)),
142 }
143}
144
145fn weighted_mean_descriptor() -> FunctionDescriptor {
150 FunctionDescriptor::new(
151 "statistics.weighted_mean",
152 "statistics",
153 "1.0.0",
154 "Weighted mean",
155 "Weighted arithmetic mean with frequency or reliability weights.",
156 )
157 .with_description(
158 "Frequency weights must be non-negative integers and count repeated observations; \
159 reliability weights must be non-negative numbers. Weights may be zero but the total \
160 weight must be positive. Negative weights, length mismatches, and a zero total weight \
161 are rejected. Exact inputs produce an exact rational result; float64 inputs require \
162 auto or scientific mode and are marked approximate.",
163 )
164 .with_parameters(vec![
165 ParamDescriptor::required("values", "Observations.", array_schema(any_number_schema())),
166 ParamDescriptor::required(
167 "weights",
168 "Non-negative weights, one per observation.",
169 array_schema(any_number_schema()),
170 ),
171 ParamDescriptor::optional(
172 "weight_type",
173 "Weight semantics: frequency (default) or reliability.",
174 bicmath_core::schema::ValueSchema::Enum {
175 variants: vec!["frequency".to_string(), "reliability".to_string()],
176 },
177 ),
178 ])
179 .with_output(any_number_schema(), "Weighted mean.")
180 .with_modes(all_modes())
181 .with_cost(CostClass::Linear)
182 .with_method_ref("docs/methods/statistics.md#weighted_mean")
183 .with_examples(vec![
184 Example::new(
185 "frequency weights",
186 example_args(&[
187 ("values", serde_json::json!([1, 2])),
188 ("weights", serde_json::json!([1, 1])),
189 ]),
190 )
191 .with_value(parse_value(
192 serde_json::json!({"kind": "rational", "numerator": "3", "denominator": "2"}),
193 )),
194 Example::new(
195 "negative weight",
196 example_args(&[
197 ("values", serde_json::json!([1, 2])),
198 ("weights", serde_json::json!([1, -1])),
199 ]),
200 )
201 .with_error(ErrorCode::DomainViolation),
202 ])
203}
204
205fn invoke_weighted_mean(args: &Args, ctx: &ExecContext) -> Result<Outcome, EngineError> {
206 let weight_type = parse_choice(
207 args,
208 "weight_type",
209 "frequency",
210 &["frequency", "reliability"],
211 )?;
212 let values = collect_numbers(args, "values")?;
213 let weights = collect_numbers(args, "weights")?;
214 if values.len() != weights.len() {
215 return Err(EngineError::malformed(format!(
216 "values and weights must have the same length ({} vs {})",
217 values.len(),
218 weights.len()
219 )));
220 }
221 if values.is_empty() {
222 return Err(insufficient(
223 "weighted_mean requires at least one observation",
224 ));
225 }
226 for (index, weight) in weights.iter().enumerate() {
227 if weight.is_negative() {
228 return Err(EngineError::domain("weights must be non-negative")
229 .with_path(format!("weights[{index}]")));
230 }
231 if weight_type == "frequency" {
232 let integral = match weight {
233 Number::Integer(_) => true,
234 Number::Rational(value) => value.is_integer(),
235 Number::Decimal(value) => value.to_bigint_if_integral().is_some(),
236 Number::Float64(value) => value.get().fract() == 0.0,
237 };
238 if !integral {
239 return Err(
240 EngineError::domain("frequency weights must be non-negative integers")
241 .with_path(format!("weights[{index}]")),
242 );
243 }
244 }
245 }
246 let has_float = values.iter().any(Number::is_float) || weights.iter().any(Number::is_float);
247 if has_float {
248 if ctx.numeric.mode == NumericMode::Exact {
249 return Err(EngineError::new(
250 ErrorCode::UnsupportedNumericMode,
251 "weighted_mean with float64 values or weights requires auto or scientific mode",
252 ));
253 }
254 let mut numerator = 0.0f64;
255 let mut numerator_compensation = 0.0f64;
256 let mut denominator = 0.0f64;
257 let mut denominator_compensation = 0.0f64;
258 for (value, weight) in values.iter().zip(weights.iter()) {
259 let value = number_to_f64(value)?;
260 let weight = number_to_f64(weight)?;
261 let term = value * weight;
262 let adjusted = term - numerator_compensation;
263 let next = numerator + adjusted;
264 numerator_compensation = (next - numerator) - adjusted;
265 numerator = next;
266
267 let adjusted = weight - denominator_compensation;
268 let next = denominator + adjusted;
269 denominator_compensation = (next - denominator) - adjusted;
270 denominator = next;
271 }
272 if denominator == 0.0 {
273 return Err(EngineError::domain(
274 "total weight must be strictly positive",
275 ));
276 }
277 return Ok(Outcome::approximate(float_value(numerator / denominator)?));
278 }
279 let mut numerator = BigRational::zero();
280 let mut denominator = BigRational::zero();
281 for (value, weight) in values.iter().zip(weights.iter()) {
282 let value = value
283 .as_exact_rational()
284 .ok_or_else(|| EngineError::internal("exact value could not be converted"))?;
285 let weight = weight
286 .as_exact_rational()
287 .ok_or_else(|| EngineError::internal("exact weight could not be converted"))?;
288 numerator += &value * &weight;
289 denominator += weight;
290 }
291 if denominator.is_zero() {
292 return Err(EngineError::domain(
293 "total weight must be strictly positive",
294 ));
295 }
296 Ok(Outcome::exact(rational_value(numerator / denominator)))
297}
298
299fn median_descriptor() -> FunctionDescriptor {
304 FunctionDescriptor::new(
305 "statistics.median",
306 "statistics",
307 "1.0.0",
308 "Median",
309 "Median of a non-empty array.",
310 )
311 .with_description(
312 "The values are sorted numerically without modifying the input. An odd count returns \
313 the middle value; an even count returns the exact mean of the two middle values. \
314 Exact inputs stay exact (median([1, 2, 10, 100]) = 6); float64 inputs require auto \
315 or scientific mode.",
316 )
317 .with_parameters(vec![ParamDescriptor::required(
318 "values",
319 "Observations.",
320 array_schema(any_number_schema()),
321 )])
322 .with_output(any_number_schema(), "Median value.")
323 .with_modes(all_modes())
324 .with_cost(CostClass::Linear)
325 .with_method_ref("docs/methods/statistics.md#median")
326 .with_examples(vec![
327 Example::new(
328 "even count",
329 example_args(&[("values", serde_json::json!([1, 2, 10, 100]))]),
330 )
331 .with_value(parse_value(
332 serde_json::json!({"kind": "integer", "value": "6"}),
333 )),
334 Example::new(
335 "odd count",
336 example_args(&[("values", serde_json::json!([2, 10, 30]))]),
337 )
338 .with_value(parse_value(
339 serde_json::json!({"kind": "integer", "value": "10"}),
340 )),
341 ])
342}
343
344fn invoke_median(args: &Args, ctx: &ExecContext) -> Result<Outcome, EngineError> {
345 let series = classify_series(args, "values", ctx)?;
346 let (value, exactness) = median_number(&series)?;
347 Ok(Outcome::new(Value::Number(value), exactness))
348}
349
350fn mode_descriptor() -> FunctionDescriptor {
355 FunctionDescriptor::new(
356 "statistics.mode",
357 "statistics",
358 "1.0.0",
359 "Mode",
360 "All tied modes with their counts.",
361 )
362 .with_description(
363 "Returns every value that occurs most often, sorted ascending, with the matching \
364 counts. Ties are reported in full; there is no arbitrary winner. The method label is \
365 always all_modes.",
366 )
367 .with_parameters(vec![ParamDescriptor::required(
368 "values",
369 "Observations.",
370 array_schema(any_number_schema()),
371 )])
372 .with_output(
373 record_schema(
374 vec![
375 field("modes", array_schema(any_number_schema())),
376 field("counts", array_schema(integer_schema())),
377 field("method", text_schema()),
378 ],
379 false,
380 ),
381 "Record with modes, counts, and the method label all_modes.",
382 )
383 .with_modes(all_modes())
384 .with_cost(CostClass::Linear)
385 .with_method_ref("docs/methods/statistics.md#mode")
386 .with_examples(vec![
387 Example::new(
388 "single mode",
389 example_args(&[("values", serde_json::json!([1, 2, 2, 3]))]),
390 )
391 .with_value(parse_value(serde_json::json!({
392 "modes": [2],
393 "counts": [2],
394 "method": "all_modes"
395 }))),
396 ])
397}
398
399fn invoke_mode(args: &Args, ctx: &ExecContext) -> Result<Outcome, EngineError> {
400 let series = classify_series(args, "values", ctx)?;
401 let (modes, counts) = mode_numbers(&series)?;
402 let value = record(vec![
403 (
404 "modes",
405 array_value(modes.into_iter().map(number_value).collect()),
406 ),
407 (
408 "counts",
409 array_value(
410 counts
411 .into_iter()
412 .map(|count| Value::integer(BigInt::from(count)))
413 .collect(),
414 ),
415 ),
416 ("method", text("all_modes")),
417 ]);
418 let exactness = if series.is_float() {
419 Exactness::Approximate
420 } else {
421 Exactness::Exact
422 };
423 Ok(Outcome::new(value, exactness))
424}
425
426fn minmax_descriptor(id: &str, title: &str, summary: &str, minimum: bool) -> FunctionDescriptor {
431 FunctionDescriptor::new(id, "statistics", "1.0.0", title, summary)
432 .with_description(
433 "Returns the smallest/largest observation without modifying the input. An empty \
434 array is rejected with insufficient_observations. Exact inputs return the exact \
435 selected value; float64 inputs require auto or scientific mode.",
436 )
437 .with_parameters(vec![ParamDescriptor::required(
438 "values",
439 "Observations.",
440 array_schema(any_number_schema()),
441 )])
442 .with_output(any_number_schema(), "Selected extreme value.")
443 .with_modes(all_modes())
444 .with_cost(CostClass::Linear)
445 .with_method_ref("docs/methods/statistics.md#min-max")
446 .with_examples(vec![
447 Example::new(
448 "extreme value",
449 example_args(&[("values", serde_json::json!([3, 1, 2]))]),
450 )
451 .with_value(parse_value(serde_json::json!({
452 "kind": "integer",
453 "value": if minimum { "1" } else { "3" }
454 }))),
455 ])
456}
457
458fn invoke_min(args: &Args, ctx: &ExecContext) -> Result<Outcome, EngineError> {
459 select_minmax(args, ctx, true)
460}
461
462fn invoke_max(args: &Args, ctx: &ExecContext) -> Result<Outcome, EngineError> {
463 select_minmax(args, ctx, false)
464}
465
466fn select_minmax(args: &Args, ctx: &ExecContext, minimum: bool) -> Result<Outcome, EngineError> {
467 let series = classify_series(args, "values", ctx)?;
468 let (value, exactness) = extreme_number(&series, minimum)?;
469 Ok(Outcome::new(Value::Number(value), exactness))
470}
471
472fn quantile_descriptor() -> FunctionDescriptor {
477 FunctionDescriptor::new(
478 "statistics.quantile",
479 "statistics",
480 "1.0.0",
481 "Quantile",
482 "Quantile with an explicit interpolation convention.",
483 )
484 .with_description(
485 "Let h = (n - 1) * q. linear (default, R-7) returns \
486 x[floor(h)] + (h - floor(h)) * (x[floor(h)+1] - x[floor(h)]); lower returns \
487 x[floor(h)]; higher returns x[ceil(h)]; midpoint returns the mean of those two order \
488 statistics; nearest returns x[round_half_to_even(h)]. q must be in [0, 1]. Exact \
489 inputs stay exact; float64 inputs require auto or scientific mode.",
490 )
491 .with_parameters(vec![
492 ParamDescriptor::required("values", "Observations.", array_schema(any_number_schema())),
493 ParamDescriptor::required("q", "Quantile level in [0, 1].", any_number_schema()),
494 ParamDescriptor::optional(
495 "method",
496 "Interpolation method: linear (default), lower, higher, midpoint, nearest.",
497 bicmath_core::schema::ValueSchema::Enum {
498 variants: vec![
499 "linear".to_string(),
500 "lower".to_string(),
501 "higher".to_string(),
502 "midpoint".to_string(),
503 "nearest".to_string(),
504 ],
505 },
506 ),
507 ])
508 .with_output(any_number_schema(), "Quantile value.")
509 .with_modes(all_modes())
510 .with_cost(CostClass::Linear)
511 .with_method_ref("docs/methods/statistics.md#quantile")
512 .with_examples(vec![
513 Example::new(
514 "linear interpolation",
515 example_args(&[
516 ("values", serde_json::json!([1, 2, 3, 4])),
517 ("q", serde_json::json!(0.5)),
518 ]),
519 )
520 .with_value(parse_value(
521 serde_json::json!({"kind": "rational", "numerator": "5", "denominator": "2"}),
522 )),
523 Example::new(
524 "out of range q",
525 example_args(&[
526 ("values", serde_json::json!([1, 2, 3])),
527 ("q", serde_json::json!(1.5)),
528 ]),
529 )
530 .with_error(ErrorCode::DomainViolation),
531 ])
532}
533
534fn invoke_quantile(args: &Args, ctx: &ExecContext) -> Result<Outcome, EngineError> {
535 let method = parse_choice(
536 args,
537 "method",
538 "linear",
539 &["linear", "lower", "higher", "midpoint", "nearest"],
540 )?;
541 let q = args.number("q")?.clone();
542 let series = classify_series(args, "values", ctx)?;
543 let (value, exactness) = quantile_number(&series, &q, &method)?;
544 Ok(Outcome::new(Value::Number(value), exactness))
545}
546
547fn variance_descriptor() -> FunctionDescriptor {
552 FunctionDescriptor::new(
553 "statistics.variance",
554 "statistics",
555 "1.0.0",
556 "Variance",
557 "Sample or population variance with explicit delta degrees of freedom.",
558 )
559 .with_description(
560 "Computes sum((x - mean)^2) / (n - ddof). ddof must be a non-negative integer and n \
561 must be strictly greater than ddof, otherwise insufficient_observations is returned. \
562 Exact inputs use exact rational arithmetic (variance([1, 2, 3], ddof=0) = 2/3); \
563 float64 inputs use Welford's stable one-pass algorithm and require auto or scientific \
564 mode.",
565 )
566 .with_parameters(vec![
567 ParamDescriptor::required("values", "Observations.", array_schema(any_number_schema())),
568 ParamDescriptor::required(
569 "ddof",
570 "Delta degrees of freedom: 0 for the population variance, 1 for the sample variance.",
571 integer_schema(),
572 ),
573 ])
574 .with_output(any_number_schema(), "Variance.")
575 .with_modes(all_modes())
576 .with_cost(CostClass::Linear)
577 .with_method_ref("docs/methods/statistics.md#variance")
578 .with_examples(vec![
579 Example::new(
580 "population variance",
581 example_args(&[
582 ("values", serde_json::json!([1, 2, 3])),
583 ("ddof", serde_json::json!(0)),
584 ]),
585 )
586 .with_value(parse_value(
587 serde_json::json!({"kind": "rational", "numerator": "2", "denominator": "3"}),
588 )),
589 Example::new(
590 "sample variance",
591 example_args(&[
592 ("values", serde_json::json!([1, 2, 3])),
593 ("ddof", serde_json::json!(1)),
594 ]),
595 )
596 .with_value(parse_value(
597 serde_json::json!({"kind": "integer", "value": "1"}),
598 )),
599 Example::new(
600 "too few observations",
601 example_args(&[
602 ("values", serde_json::json!([1])),
603 ("ddof", serde_json::json!(1)),
604 ]),
605 )
606 .with_error(ErrorCode::InsufficientObservations),
607 ])
608}
609
610fn invoke_variance(args: &Args, ctx: &ExecContext) -> Result<Outcome, EngineError> {
611 let ddof = non_negative_u64(&args.integer("ddof")?, "ddof")?;
612 let series = classify_series(args, "values", ctx)?;
613 let moment = variance_moment(&series, ddof)?;
614 match moment {
615 Moment::Exact(value) => Ok(Outcome::exact(rational_value(value))),
616 Moment::Float(value) => Ok(Outcome::approximate(float_value(value)?)),
617 }
618}
619
620fn stddev_descriptor() -> FunctionDescriptor {
621 FunctionDescriptor::new(
622 "statistics.stddev",
623 "statistics",
624 "1.0.0",
625 "Standard deviation",
626 "Square root of the variance with explicit exactness handling.",
627 )
628 .with_description(
629 "Computes sqrt(variance(values, ddof)). When the variance is a perfect square in the \
630 selected representation the result is exact. Otherwise exact mode returns \
631 unsupported_numeric_mode, auto mode returns a decimal approximation marked \
632 approximate, and scientific mode returns float64. Float64 inputs require auto or \
633 scientific mode.",
634 )
635 .with_parameters(vec![
636 ParamDescriptor::required("values", "Observations.", array_schema(any_number_schema())),
637 ParamDescriptor::required(
638 "ddof",
639 "Delta degrees of freedom: 0 for the population standard deviation, 1 for the sample.",
640 integer_schema(),
641 ),
642 ])
643 .with_output(any_number_schema(), "Standard deviation.")
644 .with_modes(all_modes())
645 .with_cost(CostClass::Linear)
646 .with_method_ref("docs/methods/statistics.md#stddev")
647 .with_examples(vec![
648 Example::new(
649 "exact perfect square",
650 example_args(&[
651 ("values", serde_json::json!([1, 2, 3])),
652 ("ddof", serde_json::json!(1)),
653 ]),
654 )
655 .with_value(parse_value(
656 serde_json::json!({"kind": "integer", "value": "1"}),
657 )),
658 Example::new(
659 "non-square variance in exact mode",
660 example_args(&[
661 ("values", serde_json::json!([1, 2, 3])),
662 ("ddof", serde_json::json!(0)),
663 ]),
664 )
665 .with_error(ErrorCode::UnsupportedNumericMode),
666 ])
667}
668
669fn invoke_stddev(args: &Args, ctx: &ExecContext) -> Result<Outcome, EngineError> {
670 let ddof = non_negative_u64(&args.integer("ddof")?, "ddof")?;
671 let series = classify_series(args, "values", ctx)?;
672 let moment = variance_moment(&series, ddof)?;
673 let (value, exactness) = stddev_moment(&moment, ctx.numeric.mode)?;
674 Ok(Outcome::new(Value::Number(value), exactness))
675}
676
677fn covariance_descriptor() -> FunctionDescriptor {
678 FunctionDescriptor::new(
679 "statistics.covariance",
680 "statistics",
681 "1.0.0",
682 "Covariance",
683 "Sample or population covariance of two paired series.",
684 )
685 .with_description(
686 "Computes sum((x - mean_x) * (y - mean_y)) / (n - ddof) for paired observations. The \
687 two arrays must have equal length; n must be strictly greater than ddof. Exact inputs \
688 use exact rational arithmetic; float64 inputs use a stable two-pass centered \
689 algorithm and require auto or scientific mode.",
690 )
691 .with_parameters(vec![
692 ParamDescriptor::required("xs", "First series.", array_schema(any_number_schema())),
693 ParamDescriptor::required(
694 "ys",
695 "Second series, paired with xs.",
696 array_schema(any_number_schema()),
697 ),
698 ParamDescriptor::required("ddof", "Delta degrees of freedom.", integer_schema()),
699 ])
700 .with_output(any_number_schema(), "Covariance.")
701 .with_modes(all_modes())
702 .with_cost(CostClass::Linear)
703 .with_method_ref("docs/methods/statistics.md#covariance")
704 .with_examples(vec![
705 Example::new(
706 "covariance of identical series",
707 example_args(&[
708 ("xs", serde_json::json!([1, 2, 3])),
709 ("ys", serde_json::json!([1, 2, 3])),
710 ("ddof", serde_json::json!(0)),
711 ]),
712 )
713 .with_value(parse_value(
714 serde_json::json!({"kind": "rational", "numerator": "2", "denominator": "3"}),
715 )),
716 Example::new(
717 "length mismatch",
718 example_args(&[
719 ("xs", serde_json::json!([1, 2, 3])),
720 ("ys", serde_json::json!([1, 2])),
721 ("ddof", serde_json::json!(0)),
722 ]),
723 )
724 .with_error(ErrorCode::MalformedInput),
725 ])
726}
727
728fn invoke_covariance(args: &Args, ctx: &ExecContext) -> Result<Outcome, EngineError> {
729 let ddof = non_negative_u64(&args.integer("ddof")?, "ddof")?;
730 let xs = classify_series(args, "xs", ctx)?;
731 let ys = classify_series(args, "ys", ctx)?;
732 if xs.len() != ys.len() {
733 return Err(EngineError::malformed(format!(
734 "xs and ys must have the same length ({} vs {})",
735 xs.len(),
736 ys.len()
737 )));
738 }
739 let moment = covariance_moment(&xs, &ys, ddof)?;
740 match moment {
741 Moment::Exact(value) => Ok(Outcome::exact(rational_value(value))),
742 Moment::Float(value) => Ok(Outcome::approximate(float_value(value)?)),
743 }
744}
745
746fn correlation_descriptor() -> FunctionDescriptor {
747 FunctionDescriptor::new(
748 "statistics.correlation",
749 "statistics",
750 "1.0.0",
751 "Pearson correlation",
752 "Pearson product-moment correlation coefficient.",
753 )
754 .with_description(
755 "Returns the Pearson correlation r = cov(x, y) / (sd(x) * sd(y)) as float64. If either \
756 series has zero variance the result is undefined and a domain_violation is returned \
757 instead of NaN. At least two paired observations are required. The output is always \
758 approximate, so exact mode is not supported.",
759 )
760 .with_parameters(vec![
761 ParamDescriptor::required("xs", "First series.", array_schema(any_number_schema())),
762 ParamDescriptor::required(
763 "ys",
764 "Second series, paired with xs.",
765 array_schema(any_number_schema()),
766 ),
767 ])
768 .with_output(float64_schema(), "Pearson correlation coefficient.")
769 .with_modes(inferential_modes())
770 .with_cost(CostClass::Linear)
771 .with_method_ref("docs/methods/statistics.md#correlation")
772 .with_examples(vec![
773 Example::new(
774 "perfect positive correlation",
775 example_args(&[
776 ("xs", serde_json::json!([1, 2, 3])),
777 ("ys", serde_json::json!([2, 4, 6])),
778 ]),
779 )
780 .with_value(parse_value(
781 serde_json::json!({"kind": "float64", "value": "1"}),
782 )),
783 Example::new(
784 "zero variance",
785 example_args(&[
786 ("xs", serde_json::json!([1, 1, 1])),
787 ("ys", serde_json::json!([1, 2, 3])),
788 ]),
789 )
790 .with_error(ErrorCode::DomainViolation),
791 ])
792}
793
794fn invoke_correlation(args: &Args, ctx: &ExecContext) -> Result<Outcome, EngineError> {
795 require_mode(ctx, &inferential_modes(), "statistics.correlation")?;
796 let xs = collect_numbers(args, "xs")?;
797 let ys = collect_numbers(args, "ys")?;
798 if xs.len() != ys.len() {
799 return Err(EngineError::malformed(format!(
800 "xs and ys must have the same length ({} vs {})",
801 xs.len(),
802 ys.len()
803 )));
804 }
805 if xs.len() < 2 {
806 return Err(insufficient(
807 "correlation requires at least two paired observations",
808 ));
809 }
810 let xs: Vec<f64> = xs
811 .iter()
812 .enumerate()
813 .map(|(index, value)| number_to_f64(value).map_err(|e| e.with_path(format!("xs[{index}]"))))
814 .collect::<Result<_, _>>()?;
815 let ys: Vec<f64> = ys
816 .iter()
817 .enumerate()
818 .map(|(index, value)| number_to_f64(value).map_err(|e| e.with_path(format!("ys[{index}]"))))
819 .collect::<Result<_, _>>()?;
820 let n = xs.len();
821 let mean_x = mean_f64(&xs);
822 let mean_y = mean_f64(&ys);
823 let mut sxx = 0.0f64;
824 let mut syy = 0.0f64;
825 let mut sxy = 0.0f64;
826 for index in 0..n {
827 let dx = xs[index] - mean_x;
828 let dy = ys[index] - mean_y;
829 sxx += dx * dx;
830 syy += dy * dy;
831 sxy += dx * dy;
832 }
833 if sxx <= 0.0 || syy <= 0.0 {
834 return Err(EngineError::domain(
835 "correlation is undefined when either series has zero variance",
836 ));
837 }
838 let denominator = sqrt(sxx * syy);
839 if denominator <= 0.0 {
840 return Err(EngineError::domain(
841 "correlation is undefined when either series has zero variance",
842 ));
843 }
844 let r = (sxy / denominator).clamp(-1.0, 1.0);
845 Ok(Outcome::approximate(float_value(r)?))
846}
847
848fn summary_descriptor() -> FunctionDescriptor {
853 FunctionDescriptor::new(
854 "statistics.summary",
855 "statistics",
856 "1.0.0",
857 "Summary statistics",
858 "One-call descriptive summary with quantiles.",
859 )
860 .with_description(
861 "Returns count, mean, min, max, median, sample variance (ddof = 1), sample standard \
862 deviation, and the linear quantiles 0.25, 0.5, and 0.75. At least two observations \
863 are required. Exact inputs stay exact wherever the representation allows; the \
864 standard deviation follows the stddev exactness rules.",
865 )
866 .with_parameters(vec![ParamDescriptor::required(
867 "values",
868 "Observations.",
869 array_schema(any_number_schema()),
870 )])
871 .with_output(
872 record_schema(
873 vec![
874 field("count", integer_schema()),
875 field("mean", any_number_schema()),
876 field("min", any_number_schema()),
877 field("max", any_number_schema()),
878 field("median", any_number_schema()),
879 field("variance", any_number_schema()),
880 field("stddev", any_number_schema()),
881 field("quantiles", ValueSchema::Any),
882 ],
883 false,
884 ),
885 "Record with count, mean, min, max, median, variance, stddev, and quantiles.",
886 )
887 .with_modes(all_modes())
888 .with_cost(CostClass::Linear)
889 .with_method_ref("docs/methods/statistics.md#summary")
890 .with_examples(vec![
891 Example::new(
892 "too few observations",
893 example_args(&[("values", serde_json::json!([1]))]),
894 )
895 .with_error(ErrorCode::InsufficientObservations),
896 ])
897}
898
899fn invoke_summary(args: &Args, ctx: &ExecContext) -> Result<Outcome, EngineError> {
900 let series = classify_series(args, "values", ctx)?;
901 if series.len() < 2 {
902 return Err(insufficient(
903 "summary requires at least two observations because variance uses ddof = 1",
904 ));
905 }
906 let mean = mean_moment(&series)?;
907 let variance = variance_moment(&series, 1)?;
908 let (stddev, stddev_exactness) = stddev_moment(&variance, ctx.numeric.mode)?;
909 let (minimum, _) = extreme_number(&series, true)?;
910 let (maximum, _) = extreme_number(&series, false)?;
911 let (median, _) = median_number(&series)?;
912 let quarter = rational_to_number(BigRational::new(BigInt::from(1), BigInt::from(4)));
913 let half = rational_to_number(BigRational::new(BigInt::from(1), BigInt::from(2)));
914 let three_quarters = rational_to_number(BigRational::new(BigInt::from(3), BigInt::from(4)));
915 let (q25, _) = quantile_number(&series, &quarter, "linear")?;
916 let (q50, _) = quantile_number(&series, &half, "linear")?;
917 let (q75, _) = quantile_number(&series, &three_quarters, "linear")?;
918 let value = record(vec![
919 ("count", integer_value(series.len() as u64)),
920 ("mean", moment_value(&mean)?),
921 ("min", Value::Number(minimum)),
922 ("max", Value::Number(maximum)),
923 ("median", Value::Number(median)),
924 ("variance", moment_value(&variance)?),
925 ("stddev", Value::Number(stddev)),
926 (
927 "quantiles",
928 record(vec![
929 ("0.25", Value::Number(q25)),
930 ("0.5", Value::Number(q50)),
931 ("0.75", Value::Number(q75)),
932 ]),
933 ),
934 ]);
935 let exactness = Exactness::Exact.combine(stddev_exactness);
936 Ok(Outcome::new(value, exactness))
937}
938
939pub fn functions() -> Vec<Arc<dyn bicmath_core::contract::Function>> {
944 vec![
945 SimpleFunction::arc(count_descriptor(), invoke_count),
946 SimpleFunction::arc(mean_descriptor(), invoke_mean),
947 SimpleFunction::arc(weighted_mean_descriptor(), invoke_weighted_mean),
948 SimpleFunction::arc(median_descriptor(), invoke_median),
949 SimpleFunction::arc(mode_descriptor(), invoke_mode),
950 SimpleFunction::arc(
951 minmax_descriptor(
952 "statistics.min",
953 "Minimum",
954 "Smallest observation in a non-empty array.",
955 true,
956 ),
957 invoke_min,
958 ),
959 SimpleFunction::arc(
960 minmax_descriptor(
961 "statistics.max",
962 "Maximum",
963 "Largest observation in a non-empty array.",
964 false,
965 ),
966 invoke_max,
967 ),
968 SimpleFunction::arc(quantile_descriptor(), invoke_quantile),
969 SimpleFunction::arc(variance_descriptor(), invoke_variance),
970 SimpleFunction::arc(stddev_descriptor(), invoke_stddev),
971 SimpleFunction::arc(covariance_descriptor(), invoke_covariance),
972 SimpleFunction::arc(correlation_descriptor(), invoke_correlation),
973 SimpleFunction::arc(summary_descriptor(), invoke_summary),
974 ]
975}
976
977#[cfg(test)]
978mod tests {
979 use super::*;
980 use std::collections::BTreeMap;
981
982 fn call(id: &str, raw: serde_json::Value) -> Result<Outcome, EngineError> {
983 call_with_mode(id, raw, NumericMode::Auto)
984 }
985
986 fn call_with_mode(
987 id: &str,
988 raw: serde_json::Value,
989 mode: NumericMode,
990 ) -> Result<Outcome, EngineError> {
991 let module = crate::module();
992 let function = module
993 .functions
994 .iter()
995 .find(|f| f.descriptor().id == id)
996 .expect("function exists");
997 let ctx = ExecContext {
998 numeric: bicmath_core::number::NumericContext {
999 mode,
1000 ..bicmath_core::number::NumericContext::default()
1001 },
1002 ..ExecContext::conservative()
1003 };
1004 let args_json = raw.as_object().expect("object args");
1005 let mut values = BTreeMap::new();
1006 for (name, value) in args_json {
1007 let param = function
1008 .descriptor()
1009 .parameter(name)
1010 .expect("parameter exists");
1011 values.insert(
1012 name.clone(),
1013 param
1014 .schema
1015 .coerce(value, name, &ctx.limits, true)
1016 .expect("argument coerces"),
1017 );
1018 }
1019 function.invoke(&Args::new(values), &ctx)
1020 }
1021
1022 fn text_of(outcome: &Outcome) -> String {
1023 match &outcome.value {
1024 Value::Number(number) => number.to_string(),
1025 other => panic!("expected number, got {other:?}"),
1026 }
1027 }
1028
1029 #[test]
1030 fn exact_descriptive_values() {
1031 assert_eq!(
1032 text_of(&call("statistics.mean", serde_json::json!({"values": [1, 2, 3]})).unwrap()),
1033 "2"
1034 );
1035 assert_eq!(
1036 text_of(&call("statistics.mean", serde_json::json!({"values": [1, 2]})).unwrap()),
1037 "3/2"
1038 );
1039 assert_eq!(
1040 text_of(
1041 &call(
1042 "statistics.median",
1043 serde_json::json!({"values": [2, 10, 30]})
1044 )
1045 .unwrap()
1046 ),
1047 "10"
1048 );
1049 assert_eq!(
1050 text_of(
1051 &call(
1052 "statistics.median",
1053 serde_json::json!({"values": [1, 2, 10, 100]})
1054 )
1055 .unwrap()
1056 ),
1057 "6"
1058 );
1059 assert_eq!(
1060 text_of(
1061 &call(
1062 "statistics.variance",
1063 serde_json::json!({"values": [1, 2, 3], "ddof": 0})
1064 )
1065 .unwrap()
1066 ),
1067 "2/3"
1068 );
1069 assert_eq!(
1070 text_of(
1071 &call(
1072 "statistics.variance",
1073 serde_json::json!({"values": [1, 2, 3], "ddof": 1})
1074 )
1075 .unwrap()
1076 ),
1077 "1"
1078 );
1079 }
1080
1081 #[test]
1082 fn original_input_is_not_mutated() {
1083 let raw = serde_json::json!({"values": [3, 1, 2]});
1084 let outcome = call("statistics.median", raw.clone()).unwrap();
1085 assert_eq!(text_of(&outcome), "2");
1086 let args = raw.as_object().unwrap();
1087 assert_eq!(args["values"], serde_json::json!([3, 1, 2]));
1088 }
1089
1090 #[test]
1091 fn float_inputs_are_approximate() {
1092 let outcome = call(
1093 "statistics.mean",
1094 serde_json::json!({"values": [
1095 {"kind": "float64", "value": "1"},
1096 {"kind": "float64", "value": "2"}
1097 ]}),
1098 )
1099 .unwrap();
1100 assert_eq!(outcome.exactness, Exactness::Approximate);
1101 assert_eq!(text_of(&outcome), "1.5");
1102 }
1103
1104 #[test]
1105 fn exact_mode_rejects_float_inputs() {
1106 let err = call_with_mode(
1107 "statistics.mean",
1108 serde_json::json!({"values": [{"kind": "float64", "value": "1"}]}),
1109 NumericMode::Exact,
1110 )
1111 .unwrap_err();
1112 assert_eq!(err.code, ErrorCode::UnsupportedNumericMode);
1113 }
1114
1115 #[test]
1116 fn non_numeric_entries_are_rejected() {
1117 let module = crate::module();
1118 let function = module
1119 .functions
1120 .iter()
1121 .find(|f| f.descriptor().id == "statistics.mean")
1122 .expect("function exists");
1123 let mut values = BTreeMap::new();
1124 values.insert(
1125 "values".to_string(),
1126 Value::Array(vec![
1127 Value::Number(Number::Integer(BigInt::from(1))),
1128 Value::text("not a number"),
1129 ]),
1130 );
1131 let err = function
1132 .invoke(&Args::new(values), &ExecContext::conservative())
1133 .unwrap_err();
1134 assert_eq!(err.code, ErrorCode::MalformedInput);
1135 }
1136
1137 #[test]
1138 fn mode_returns_all_ties() {
1139 let outcome = call(
1140 "statistics.mode",
1141 serde_json::json!({"values": [1, 2, 2, 3, 3]}),
1142 )
1143 .unwrap();
1144 let Value::Record(fields) = &outcome.value else {
1145 panic!("expected record");
1146 };
1147 assert_eq!(
1148 fields.get("modes"),
1149 Some(&Value::Array(vec![
1150 Value::Number(Number::Integer(BigInt::from(2))),
1151 Value::Number(Number::Integer(BigInt::from(3))),
1152 ]))
1153 );
1154 }
1155
1156 #[test]
1157 fn weighted_mean_rejects_bad_weights() {
1158 let err = call(
1159 "statistics.weighted_mean",
1160 serde_json::json!({"values": [1, 2], "weights": [1, -1]}),
1161 )
1162 .unwrap_err();
1163 assert_eq!(err.code, ErrorCode::DomainViolation);
1164 let err = call(
1165 "statistics.weighted_mean",
1166 serde_json::json!({"values": [1, 2], "weights": [0, 0]}),
1167 )
1168 .unwrap_err();
1169 assert_eq!(err.code, ErrorCode::DomainViolation);
1170 let err = call(
1171 "statistics.weighted_mean",
1172 serde_json::json!({"values": [1, 2], "weights": [1]}),
1173 )
1174 .unwrap_err();
1175 assert_eq!(err.code, ErrorCode::MalformedInput);
1176 }
1177
1178 #[test]
1179 fn stddev_exact_and_approximate_paths() {
1180 let exact = call(
1181 "statistics.stddev",
1182 serde_json::json!({"values": [1, 2, 3], "ddof": 1}),
1183 )
1184 .unwrap();
1185 assert_eq!(exact.exactness, Exactness::Exact);
1186 let err = call_with_mode(
1187 "statistics.stddev",
1188 serde_json::json!({"values": [1, 2, 3], "ddof": 0}),
1189 NumericMode::Exact,
1190 )
1191 .unwrap_err();
1192 assert_eq!(err.code, ErrorCode::UnsupportedNumericMode);
1193 let approximate = call(
1194 "statistics.stddev",
1195 serde_json::json!({"values": [1, 2, 3], "ddof": 0}),
1196 )
1197 .unwrap();
1198 assert_eq!(approximate.exactness, Exactness::Approximate);
1199 }
1200
1201 #[test]
1202 fn correlation_detects_zero_variance() {
1203 let err = call(
1204 "statistics.correlation",
1205 serde_json::json!({"xs": [1, 1, 1], "ys": [1, 2, 3]}),
1206 )
1207 .unwrap_err();
1208 assert_eq!(err.code, ErrorCode::DomainViolation);
1209 }
1210
1211 #[test]
1212 fn quantile_methods() {
1213 let lower = call(
1214 "statistics.quantile",
1215 serde_json::json!({"values": [1, 2, 3, 4], "q": "0.5", "method": "lower"}),
1216 )
1217 .unwrap();
1218 assert_eq!(text_of(&lower), "2");
1219 let higher = call(
1220 "statistics.quantile",
1221 serde_json::json!({"values": [1, 2, 3, 4], "q": "0.5", "method": "higher"}),
1222 )
1223 .unwrap();
1224 assert_eq!(text_of(&higher), "3");
1225 let nearest = call(
1227 "statistics.quantile",
1228 serde_json::json!({"values": [1, 2, 3, 4], "q": "0.5", "method": "nearest"}),
1229 )
1230 .unwrap();
1231 assert_eq!(text_of(&nearest), "3");
1232 }
1233
1234 #[test]
1235 fn summary_exact_record() {
1236 let outcome = call(
1237 "statistics.summary",
1238 serde_json::json!({"values": [1, 2, 3]}),
1239 )
1240 .unwrap();
1241 assert_eq!(outcome.exactness, Exactness::Exact);
1242 let Value::Record(fields) = &outcome.value else {
1243 panic!("expected record");
1244 };
1245 assert_eq!(
1246 fields.get("variance"),
1247 Some(&Value::Number(Number::Integer(BigInt::from(1))))
1248 );
1249 }
1250}