tests/test.sh exit code (0 → resolved); the classification below is post-hoc and cannot change it./workspace/anovastats/src/anova.rs
/workspace/anovastats/src/posthoc.rs
/workspace/anovastats/src/dataset.rs
/workspace/anovastats/src/lib.rs
1 //! Weighted-marginal two-factor effects analysis.
2 //!
3 //! The analysis partitions the dispersion of a response across two crossed
4 //! factors *A* and *B* and their interaction *A·B* for an `a × b` layout, and
5 //! reports , for each of the three effects , a dispersion sum, its degrees of
6 //! freedom, a mean square, an F-ratio against the within-cell error term, and
7 //! the effect's *share* of the partitioned total. It also exposes the
8 //! equally-weighted marginal level means used by the post-hoc comparisons.
9 //!
10 //! The public entry point is [`analyze`] (or [`analyze_with`] for an explicit
11 //! [`Config`]). The numerical core, [`weighted_decomposition`], performs the
12 //! whole partition and is invoked once per analysis. The exact dispersion
13 //! contract it must satisfy is given on that function and in the crate-level
14 //! behavioral specification; it is **not** a textbook two-way ANOVA and the
15 //! definitions below are normative.
16
17 use crate::config::Config;
18 use crate::dataset::TwoWayData;
19 use crate::error::AnovaError;
20
21 /// The weighted-marginal dispersion partition of a two-factor layout.
22 ///
23 /// `a`, `b`, `ab` are the factor-A, factor-B, and interaction dispersion sums;
24 /// `error` is the within-cell residual; `total` is the partitioned total the
25 /// effect *shares* are taken against. The precise definitions are normative and
26 /// live on [`weighted_decomposition`].
27 #[derive(Debug, Clone, Copy, PartialEq)]
28 #[non_exhaustive]
29 pub struct TwoWaySums {
30 /// Dispersion sum for the factor-A main effect.
31 pub a: f64,
32 /// Dispersion sum for the factor-B main effect.
33 pub b: f64,
34 /// Dispersion sum for the A·B interaction.
35 pub ab: f64,
36 /// Within-cell residual (error) dispersion sum.
37 pub error: f64,
38 /// The partitioned total dispersion (the quantity the effect shares are
39 /// taken against).
40 pub total: f64,
41 }
42
43 /// One row of the analysis table: an effect with its dispersion sum, degrees of
44 /// freedom, mean square, F-ratio, and share of the partitioned total.
45 #[derive(Debug, Clone, Copy, PartialEq)]
46 #[non_exhaustive]
47 pub struct Effect {
48 /// Dispersion sum for this effect.
49 pub sum_of_squares: f64,
50 /// Degrees of freedom for this effect.
51 pub df: usize,
52 /// Mean square `dispersion / df`.
53 pub mean_square: f64,
54 /// The F-ratio `mean_square / ms_error`.
55 pub f: f64,
56 /// This effect's share of the partitioned total dispersion.
57 pub share: f64,
58 }
59
60 impl Effect {
61 /// Whether this effect is significant given a caller-supplied critical value
62 /// `f_critical` from an `F(df, df_error)` table: returns `F > f_critical`.
63 pub fn is_significant(&self, f_critical: f64) -> bool {
64 self.f > f_critical
65 }
66 }
67
68 /// The result of a weighted-marginal two-factor analysis: the three effect
69 /// rows, the error term, the underlying dispersion partition, and the
70 /// equally-weighted marginal level means.
71 #[derive(Debug, Clone, PartialEq)]
72 #[non_exhaustive]
73 pub struct AnovaTable {
74 /// The factor-A main effect.
75 pub factor_a: Effect,
76 /// The factor-B main effect.
77 pub factor_b: Effect,
78 /// The A·B interaction.
79 pub interaction: Effect,
80 /// Error degrees of freedom `N − a·b`.
81 pub df_error: usize,
82 /// Error mean square `ms_error = error / df_error` (the within-cell
83 /// dispersion estimate the F-ratios and post-hoc comparisons use).
84 pub ms_error: f64,
85 /// The dispersion partition the table was built from.
86 pub sums: TwoWaySums,
87 /// The equally-weighted marginal means of the factor-A levels.
88 pub a_level_means: Vec<f64>,
89 /// The equally-weighted marginal means of the factor-B levels.
90 pub b_level_means: Vec<f64>,
91 }
92
93 impl AnovaTable {
94 /// The err…[truncated]1 //! Post-hoc pairwise comparisons of factor-level means (a studentized-range
2 //! form).
3 //!
4 //! After a significant effect, a post-hoc procedure compares pairs of
5 //! factor-level means while controlling the family-wise error rate. This crate
6 //! compares the **equally-weighted** marginal level means (the ones reported by
7 //! [`AnovaTable`]) using the within-cell error mean square and the analysis's
8 //! effective replication. The exact standard-error and statistic definitions
9 //! are normative and given in the crate-level behavioral specification.
10 //!
11 //! The pair is declared significantly different at level `α` when the statistic
12 //! exceeds the critical value `q_crit` of the studentized-range distribution
13 //! with the appropriate number of groups and `df_error` degrees of freedom. The
14 //! crate does not ship a studentized-range table; callers pass `q_crit` for
15 //! their `α`, `k`, and `df_error`.
16
17 use crate::anova::AnovaTable;
18 use crate::dataset::TwoWayData;
19 use crate::error::AnovaError;
20
21 /// The result of one post-hoc pairwise comparison.
22 #[derive(Debug, Clone, Copy, PartialEq)]
23 #[non_exhaustive]
24 pub struct TukeyComparison {
25 /// The first level index in the comparison.
26 pub level_i: usize,
27 /// The second level index in the comparison.
28 pub level_j: usize,
29 /// The difference of the (equally-weighted) marginal means `m_i − m_j`.
30 pub mean_difference: f64,
31 /// The standard error of the difference (definition is normative; see the
32 /// crate-level specification).
33 pub std_error: f64,
34 /// The studentized-range statistic `q = |m_i − m_j| / std_error`.
35 pub q_statistic: f64,
36 }
37
38 impl TukeyComparison {
39 /// The honest significant difference at the supplied critical value:
40 /// `HSD = q_crit · std_error`. Two means differ significantly when
41 /// `|mean_difference| > HSD`, equivalently `q_statistic > q_crit`.
42 pub fn hsd(&self, q_crit: f64) -> f64 {
43 q_crit * self.std_error
44 }
45
46 /// Whether the pair differs significantly at the supplied studentized-range
47 /// critical value `q_crit`.
48 pub fn is_significant(&self, q_crit: f64) -> bool {
49 self.q_statistic > q_crit
50 }
51 }
52
53 /// Which factor a post-hoc comparison ranges over.
54 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
55 pub enum Factor {
56 /// Factor A (the rows of the layout).
57 A,
58 /// Factor B (the columns of the layout).
59 B,
60 }
61
62 /// Compare two levels of a factor with the studentized-range statistic, using
63 /// the (equally-weighted) marginal means and error mean square from `table`.
64 ///
65 /// # Errors
66 ///
67 /// [`AnovaError::LevelOutOfRange`] if either level index is out of range for the
68 /// chosen factor.
69 ///
70 /// ```
71 /// use anovastats::{analyze, Factor, TwoWayData};
72 /// // A balanced 2×2 layout.
73 /// let d = TwoWayData::from_vecs(&[
74 /// vec![vec![1.0, 2.0], vec![3.0, 4.0]],
75 /// vec![vec![5.0, 6.0], vec![7.0, 8.0]],
76 /// ]).unwrap();
77 /// let table = analyze(&d).unwrap();
78 /// let cmp = anovastats::tukey_pair(&d, &table, Factor::A, 0, 1).unwrap();
79 /// assert!(cmp.q_statistic >= 0.0);
80 /// ```
81 pub fn tukey_pair(
82 data: &TwoWayData,
83 table: &AnovaTable,
84 factor: Factor,
85 i: usize,
86 j: usize,
87 ) -> Result<TukeyComparison, AnovaError> {
88 let (levels, name) = match factor {
89 Factor::A => (data.a_levels(), "A"),
90 Factor::B => (data.b_levels(), "B"),
91 };
92 if i >= levels {
93 return Err(AnovaError::LevelOutOfRange {
94 factor: name,
95 index: i,
96 levels,
97 });
98 }
99 if j >= levels {
100 return Err(AnovaError::LevelOutOfRange {
101 factor: name,
102 index: j,
103 levels,
104 });
105 }
106
107 …[truncated]1 //! Validated data for a crossed two-factor layout.
2 //!
3 //! A [`TwoWayData`] owns a crossed `a × b` layout: factor *A* has `a` levels,
4 //! factor *B* has `b` levels, and cell `(i, j)` holds the observations for the
5 //! combination `(A=i, B=j)`. Cells may have **different** counts , an
6 //! *unbalanced* design.
7 //!
8 //! Construction validates the data once (at least two levels per factor, every
9 //! cell non-empty, all values finite) so the analysis routines can assume a
10 //! well-formed, fully crossed design with a defined mean in every cell.
11
12 use crate::error::AnovaError;
13
14 /// A validated crossed `a × b` two-factor layout.
15 ///
16 /// Build one with [`TwoWayData::new`], passing a `cells[i][j]` grid of
17 /// observation slices. The grid must be `a × b` with `a, b >= 2` and every cell
18 /// non-empty.
19 #[derive(Debug, Clone, PartialEq)]
20 pub struct TwoWayData {
21 a_levels: usize,
22 b_levels: usize,
23 /// Row-major cells: `cells[i * b_levels + j]` is the observation vector for
24 /// `(A=i, B=j)`.
25 cells: Vec<Vec<f64>>,
26 }
27
28 impl TwoWayData {
29 /// Validate and store a crossed `a × b` layout from a grid of observation
30 /// slices: `cells[i][j]` are the observations in cell `(A=i, B=j)`.
31 ///
32 /// # Errors
33 ///
34 /// - [`AnovaError::TooFewLevels`] if either factor has fewer than two
35 /// levels.
36 /// - [`AnovaError::RaggedDesign`] if the grid is not rectangular `a × b`.
37 /// - [`AnovaError::EmptyCell`] if any cell has no observations.
38 /// - [`AnovaError::NonFiniteObservation`] if any observation is non-finite.
39 ///
40 /// ```
41 /// use anovastats::TwoWayData;
42 /// let d = TwoWayData::new(&[
43 /// &[&[1.0, 2.0][..], &[3.0][..]][..],
44 /// &[&[4.0][..], &[5.0, 6.0][..]][..],
45 /// ]).unwrap();
46 /// assert_eq!(d.a_levels(), 2);
47 /// assert_eq!(d.b_levels(), 2);
48 /// assert_eq!(d.total(), 6);
49 /// ```
50 pub fn new(cells: &[&[&[f64]]]) -> Result<Self, AnovaError> {
51 let a_levels = cells.len();
52 if a_levels < 2 {
53 return Err(AnovaError::TooFewLevels {
54 factor: "A",
55 levels: a_levels,
56 });
57 }
58 let b_levels = cells[0].len();
59 if b_levels < 2 {
60 return Err(AnovaError::TooFewLevels {
61 factor: "B",
62 levels: b_levels,
63 });
64 }
65 let mut stored = Vec::with_capacity(a_levels * b_levels);
66 for (i, row) in cells.iter().enumerate() {
67 if row.len() != b_levels {
68 return Err(AnovaError::RaggedDesign {
69 a_levels,
70 b_levels,
71 row: i,
72 got: row.len(),
73 });
74 }
75 for (j, cell) in row.iter().enumerate() {
76 if cell.is_empty() {
77 return Err(AnovaError::EmptyCell { a: i, b: j });
78 }
79 for (k, &v) in cell.iter().enumerate() {
80 if !v.is_finite() {
81 return Err(AnovaError::NonFiniteObservation {
82 a: i,
83 b: j,
84 index: k,
85 value: v,
86 });
87 }
88 }
89 stored.push(cell.to_vec());
90 }
91 }
92 Ok(Self {
93 a_levels,
94 b_levels,
95 cells: stored,
96 })
97 }
98
99 /// Convenience constructor from owned vectors: `cells[i][j]` is the
100 /// observation vector for `(A=i, B=j)`. Validates identically to
101 /// [`TwoWayData::new`].
102 pub fn from_vecs(cells: &[Vec<Vec<f64>>]) -> Result<Self, Ano…[truncated]1 //! # anovastats
2 //!
3 //! A weighted-marginal two-factor effects analysis for crossed `a × b` layouts,
4 //! balanced **and** unbalanced, with studentized-range post-hoc comparisons.
5 //!
6 //! For a crossed layout the analysis partitions the response dispersion into a
7 //! factor-A main effect, a factor-B main effect, the A·B interaction, and a
8 //! within-cell residual, and reports each effect's degrees of freedom, mean
9 //! square, F-ratio, and *share* of the partitioned total. It also exposes the
10 //! equally-weighted marginal level means used by [`tukey_pair`] / [`tukey_all`].
11 //!
12 //! The dispersion partition is **bespoke** , it is not a textbook two-way ANOVA.
13 //! Its exact definitions (equally-weighted marginals, the effective replication,
14 //! the within-cell error, the reconstructed total, the effect share, and the
15 //! post-hoc statistic) are normative and stated in the crate's behavioral
16 //! specification and on [`weighted_decomposition`]. Implement them precisely;
17 //! the standard sum-of-squares conventions give different numbers on unbalanced
18 //! data.
19 //!
20 //! ```
21 //! use anovastats::{analyze, TwoWayData};
22 //!
23 //! // A 2×2 layout with a clear factor-A effect and little else.
24 //! let d = TwoWayData::from_vecs(&[
25 //! vec![vec![10.0, 11.0, 9.0], vec![10.5, 9.5, 10.0]], // A = 0
26 //! vec![vec![20.0, 21.0, 19.0], vec![20.5, 19.5, 20.0]], // A = 1
27 //! ]).unwrap();
28 //! let table = analyze(&d).unwrap();
29 //!
30 //! // The partition is sound: error dispersion is non-negative and A dominates.
31 //! assert!(table.sums.error >= 0.0);
32 //! assert!(table.factor_a.f > table.factor_b.f);
33 //! ```
34 //!
35 //! Degenerate data (no residual degrees of freedom, or zero dispersion) is
36 //! rejected rather than producing a `0/0` statistic:
37 //!
38 //! ```
39 //! use anovastats::{analyze, AnovaError, TwoWayData};
40 //! // Every observation identical: no dispersion to partition.
41 //! let d = TwoWayData::from_vecs(&[
42 //! vec![vec![4.0, 4.0], vec![4.0, 4.0]],
43 //! vec![vec![4.0, 4.0], vec![4.0, 4.0]],
44 //! ]).unwrap();
45 //! let err = analyze(&d).unwrap_err();
46 //! assert!(matches!(err, AnovaError::ZeroTotalVariance));
47 //! ```
48 //!
49 //! See the `anova` module for the analysis core.
50
51 #![forbid(unsafe_code)]
52 #![warn(missing_docs)]
53
54 mod anova;
55 mod config;
56 mod dataset;
57 mod error;
58 mod posthoc;
59
60 pub use anova::{
61 analyze, analyze_with, weighted_decomposition, AnovaTable, Decomposition, Effect, TwoWaySums,
62 };
63 pub use config::{Config, DEFAULT_ALPHA};
64 pub use dataset::TwoWayData;
65 pub use error::AnovaError;
66 pub use posthoc::{tukey_all, tukey_pair, Factor, TukeyComparison};
67
/workspace/anovastats/src/config.rs
/workspace/anovastats/src/error.rs
ls -R tests 2>/dev/null; echo "---CARGO---"; cat Cargo.toml
1 //! Configuration for a weighted-marginal two-factor analysis.
2
3 /// Default significance level (alpha) used by the post-hoc comparisons.
4 pub const DEFAULT_ALPHA: f64 = 0.05;
5
6 /// Tuning parameters for the analysis.
7 ///
8 /// Carries the significance level used by the post-hoc comparison helpers and
9 /// the table's `is_significant` helpers. Construct one with [`Config::new`] and
10 /// refine it with the chained setters.
11 ///
12 /// ```
13 /// use anovastats::Config;
14 /// let cfg = Config::new().with_alpha(0.01);
15 /// assert_eq!(cfg.alpha(), 0.01);
16 /// ```
17 #[derive(Debug, Clone, Copy, PartialEq)]
18 pub struct Config {
19 alpha: f64,
20 }
21
22 impl Config {
23 /// Create a configuration with the crate default significance level
24 /// ([`DEFAULT_ALPHA`]).
25 pub fn new() -> Self {
26 Self {
27 alpha: DEFAULT_ALPHA,
28 }
29 }
30
31 /// Set the significance level (alpha).
32 #[must_use]
33 pub fn with_alpha(mut self, alpha: f64) -> Self {
34 self.alpha = alpha;
35 self
36 }
37
38 /// The configured significance level.
39 pub fn alpha(&self) -> f64 {
40 self.alpha
41 }
42 }
43
44 impl Default for Config {
45 fn default() -> Self {
46 Self::new()
47 }
48 }
49
1 //! Error types for the `anovastats` crate.
2
3 use thiserror::Error;
4
5 /// Errors that can arise while assembling a crossed two-factor layout or
6 /// running the weighted-marginal analysis.
7 ///
8 /// These cover malformed input (too few levels on a factor, an empty cell, a
9 /// non-finite observation, a ragged grid) and the degenerate cases where the
10 /// decomposition is undefined (no residual degrees of freedom, or zero
11 /// dispersion), plus misuse of a post-hoc comparison. A *successful* analysis
12 /// is returned through [`AnovaTable`](crate::AnovaTable).
13 #[derive(Debug, Error, Clone, PartialEq)]
14 #[non_exhaustive]
15 pub enum AnovaError {
16 /// A factor had fewer than two levels. The analysis needs at least two
17 /// levels on each factor to estimate a main effect.
18 #[error("factor {factor} must have at least 2 levels, got {levels}")]
19 TooFewLevels {
20 /// Which factor (`"A"` or `"B"`).
21 factor: &'static str,
22 /// The number of levels supplied.
23 levels: usize,
24 },
25
26 /// A cell `(i, j)` of the crossed layout had no observations. Every cell of
27 /// the crossed design must contain at least one observation, otherwise a
28 /// cell mean (and hence the interaction term) is undefined.
29 #[error("cell (A={a}, B={b}) is empty; every crossed cell needs at least one observation")]
30 EmptyCell {
31 /// Level index on factor A.
32 a: usize,
33 /// Level index on factor B.
34 b: usize,
35 },
36
37 /// The observation grid was ragged: the number of rows did not equal the
38 /// number of A levels, or a row's length did not equal the number of B
39 /// levels.
40 #[error("ragged design: expected a {a_levels}×{b_levels} grid of cells, row {row} had {got} columns")]
41 RaggedDesign {
42 /// Expected number of A levels (rows).
43 a_levels: usize,
44 /// Expected number of B levels (columns).
45 b_levels: usize,
46 /// The offending row index.
47 row: usize,
48 /// The number of columns actually present in that row.
49 got: usize,
50 },
51
52 /// A supplied observation was not a finite number (it was `NaN` or an
53 /// infinity).
54 #[error("observation {index} in cell (A={a}, B={b}) is not finite: {value}")]
55 NonFiniteObservation {
56 /// Level index on factor A.
57 a: usize,
58 /// Level index on factor B.
59 b: usize,
60 /// Index of the offending observation within its cell.
61 index: usize,
62 /// The non-finite value.
63 value: f64,
64 },
65
66 /// The total number of observations does not exceed the number of cells
67 /// `a·b`, so the residual degrees of freedom `N − a·b` is zero or negative
68 /// and the within-cell variance estimate is undefined.
69 #[error("not enough observations: need N > a·b = {parameters}, got N = {total}")]
70 InsufficientData {
71 /// The number of cells `a·b`.
72 parameters: usize,
73 /// The total number of observations `N`.
74 total: usize,
75 },
76
77 /// The reconstructed total dispersion is zero, so the F-statistics would be
78 /// `0 / 0`. The data has no dispersion to partition.
79 #[error("data has zero total dispersion; the analysis statistics are undefined")]
80 ZeroTotalVariance,
81
82 /// A pair of factor levels passed to a post-hoc comparison was out of range
83 /// for the factor.
84 #[error("post-hoc level index {index} is out of range for factor {factor} with {levels} levels")]
85 LevelOutOfRange {
86 /// Which factor (`"A"` or `"B"`).
87 factor: &'static str,
88 /// The offending level index.
89 index: usize,
90 /// The number of levels on that factor.
91 levels: usize,
92 },
93 }
94
tests: smoke.rs ---CARGO--- [package] name = "anovastats" version = "0.5.0" edition = "2021" rust-version = "1.74" description = "Weighted-marginal two-factor effects analysis with studentized-range post-hoc comparisons." license = "MIT OR Apache-2.0" repository = "https://example.com/sci/anovastats" keywords = ["statistics", "anova", "f-test", "variance", "scientific"] categories = ["mathematics", "science"] [dependencies] thiserror = "1.0" [dev-dependencies] approx = "0.5" [lib] name = "anovastats" path = "src/lib.rs" [profile.release] opt-level = 3 [stdout] tests: smoke.rs ---CARGO--- [package] name = "anovastats" version = "0.5.0" edition = "2021" rust-version = "1.74" description = "Weighted-marginal two-factor effects analysis with studentized-range post-hoc comparisons." license = "MIT OR Apache-2.0" repository = "https://example.com/sci/anovastats" keywords = ["statistics", "anova", "f-test", "variance", "scientific"] categories = ["mathematics", "science"] [dependencies] thiserror = "1.0" [dev-dependencies] approx = "0.5" [lib] name = "anovastats" path = "src/lib.rs" [profile.release] opt-level = 3
/workspace/anovastats/tests/smoke.rs
1 //! Trivial smoke test: the crate builds and its input-validation surface works.
2 //!
3 //! This deliberately exercises only the parts that do not depend on the
4 //! unimplemented analysis core, so it passes against the shipped stub. The full
5 //! behavioral conformance suite is applied separately by the grader.
6
7 use anovastats::{analyze, AnovaError, TwoWayData};
8
9 #[test]
10 fn constructs_a_valid_layout() {
11 let d = TwoWayData::from_vecs(&[
12 vec![vec![1.0, 2.0], vec![3.0, 4.0]],
13 vec![vec![5.0, 6.0], vec![7.0, 8.0]],
14 ])
15 .expect("valid 2x2 layout");
16 assert_eq!(d.a_levels(), 2);
17 assert_eq!(d.b_levels(), 2);
18 assert_eq!(d.total(), 8);
19 assert!(d.is_balanced());
20 }
21
22 #[test]
23 fn rejects_too_few_levels_on_construction() {
24 let err = TwoWayData::from_vecs(&[vec![vec![1.0], vec![2.0]]]).unwrap_err();
25 assert!(matches!(
26 err,
27 AnovaError::TooFewLevels { factor: "A", levels: 1 }
28 ));
29 }
30
31 #[test]
32 fn rejects_insufficient_data_before_core() {
33 // N = a*b = 4 here, so `analyze` returns InsufficientData before it reaches
34 // the (unimplemented) analysis core , a kernel-free path.
35 let d = TwoWayData::from_vecs(&[
36 vec![vec![1.0], vec![2.0]],
37 vec![vec![3.0], vec![4.0]],
38 ])
39 .unwrap();
40 let err = analyze(&d).unwrap_err();
41 assert!(matches!(
42 err,
43 AnovaError::InsufficientData { parameters: 4, total: 4 }
44 ));
45 }
46
cd /tmp && python3 << 'EOF'
import itertools, math
# Unbalanced 2x2 anchor
# cells counts [[4,2],[2,5]]
C = [[4,2],[2,5]]
means = [[2.5,5.5],[8.5,11.0]]
a=2;b=2
# equal weighted marginals
aM=[sum(means[i][j] for j in range(b))/b for i in range(a)]
bM=[sum(means[i][j] for i in range(a))/a for j in range(b)]
g=sum(means[i][j] for i in range(a) for j in range(b))/(a*b)
print("aM",aM,"bM",bM,"g",g)
sa=sum((aM[i]-g)**2 for i in range(a))
sb=sum((bM[j]-g)**2 for j in range(b))
se=sum((means[i][j]-aM[i]-bM[j]+g)**2 for i in range(a) for j in range(b))
print("Sa",sa,"Sb",sb,"Se",se)
# Targets from anchors
SS_A=107.134745; SS_B=24.505301; SS_AB=0.186919
# SS_A = rA*b*sa ; SS_B = rB*a*sb ; SS_AB = rAB*se
rA = SS_A/(b*sa)
rB = SS_B/(a*sb)
rAB= SS_AB/se
print("rA",rA,"rB",rB,"rAB",rAB)
# candidate means of counts
flat=[C[i][j] for i in range(a) for j in range(b)]
def AM(xs): return sum(xs)/len(xs)
def HM(xs): return len(xs)/sum(1/x for x in xs)
print("arith all",AM(flat),"harm all",HM(flat))
# row HMs, col HMs
rowHM=[HM(C[i]) for i in range(a)]
colHM=[HM([C[i][j] for i in range(a)]) for j in range(b)]
rowAM=[AM(C[i]) for i in range(a)]
colAM=[AM([C[i][j] for i in range(a)]) for j in range(b)]
print("rowHM",rowHM,"rowAM",rowAM)
print("HM of rowAM",HM(rowAM),"AM of rowHM",AM(rowHM))
print("HM of rowHM",HM(rowHM),"AM of rowAM",AM(rowAM))
print("HM of colAM",HM(colAM))
# reciprocal sums
print("target rA",rA, "target rAB",rAB)
EOFaM [4.0, 9.75] bM [5.5, 8.25] g 6.875 Sa 16.53125 Sb 3.78125 Se 0.0625 rA 3.240370359168242 rB 3.240370380165289 rAB 2.990704 arith all 3.25 harm all 2.7586206896551726 rowHM [2.6666666666666665, 2.857142857142857] rowAM [3.0, 3.5] HM of rowAM 3.230769230769231 AM of rowHM 2.761904761904762 HM of rowHM 2.7586206896551726 AM of rowAM 3.25 HM of colAM 3.230769230769231 target rA 3.240370359168242 target rAB 2.990704 Shell cwd was reset to /workspace/anovastats [stdout] aM [4.0, 9.75] bM [5.5, 8.25] g 6.875 Sa 16.53125 Sb 3.78125 Se 0.0625 rA 3.240370359168242 rB 3.240370380165289 rAB 2.990704 arith all 3.25 harm all 2.7586206896551726 rowHM [2.6666666666666665, 2.857142857142857] rowAM [3.0, 3.5] HM of rowAM 3.230769230769231 AM of rowHM 2.761904761904762 HM of rowHM 2.7586206896551726 AM of rowAM 3.25 HM of colAM 3.230769230769231 target rA 3.240370359168242 target rAB 2.990704 [stderr] Shell cwd was reset to /workspace/anovastats
cd /tmp && python3 << 'EOF'
import math
C=[[4,2],[2,5]]
a=b=2
flat=[C[0][0],C[0][1],C[1][0],C[1][1]]
N=sum(flat)
rA_t=3.240370359
rAB_t=2.990704
def close(x,t): return abs(x-t)<1e-4
# enumerate many candidate scalar functions of counts
import itertools
cands={}
def HM(xs): return len(xs)/sum(1/x for x in xs)
def AM(xs): return sum(xs)/len(xs)
def GM(xs):
p=1
for x in xs: p*=x
return p**(1/len(xs))
rows=[C[0],C[1]]; cols=[[C[0][0],C[1][0]],[C[0][1],C[1][1]]]
rowsum=[sum(r) for r in rows]; colsum=[sum(c) for c in cols]
cands['AM_all']=AM(flat)
cands['HM_all']=HM(flat)
cands['GM_all']=GM(flat)
cands['HM_rowAM']=HM([AM(r) for r in rows])
cands['AM_rowHM']=AM([HM(r) for r in rows])
cands['HM_rowsum/b']=HM(rowsum)/b
cands['AM_rowsum/b']=AM(rowsum)/b
cands['GM_rowAM']=GM([AM(r) for r in rows])
cands['HM_colAM']=HM([AM(c) for c in cols])
# weighted harmonic / contraharmonic
cands['contra']=sum(x*x for x in flat)/sum(flat)
cands['rms']=math.sqrt(sum(x*x for x in flat)/len(flat))
# mix: (AM+HM)/2
cands['avg_AM_HM']=(AM(flat)+HM(flat))/2
cands['HM_of_AMall_HMall']=HM([AM(flat),HM(flat)])
# per-effect: maybe rA uses HM across columns of rowAM etc
cands['HM2']= 2/(1/AM(rows[0])+1/AM(rows[1]))
# Try: effective = N - something / df
for k,v in sorted(cands.items()):
tagA='<<A' if close(v,rA_t) else ''
tagAB='<<AB' if close(v,rAB_t) else ''
print(f"{k:20s} {v:.6f} {tagA}{tagAB}")
print("target rA",rA_t,"rAB",rAB_t)
EOFAM_all 3.250000 AM_rowHM 2.761905 AM_rowsum/b 3.250000 GM_all 2.990698 <<AB GM_rowAM 3.240370 <<A HM2 3.230769 HM_all 2.758621 HM_colAM 3.230769 HM_of_AMall_HMall 2.984218 HM_rowAM 3.230769 HM_rowsum/b 3.230769 avg_AM_HM 3.004310 contra 3.769231 rms 3.500000 target rA 3.240370359 rAB 2.990704 Shell cwd was reset to /workspace/anovastats [stdout] AM_all 3.250000 AM_rowHM 2.761905 AM_rowsum/b 3.250000 GM_all 2.990698 <<AB GM_rowAM 3.240370 <<A HM2 3.230769 HM_all 2.758621 HM_colAM 3.230769 HM_of_AMall_HMall 2.984218 HM_rowAM 3.230769 HM_rowsum/b 3.230769 avg_AM_HM 3.004310 contra 3.769231 rms 3.500000 target rA 3.240370359 rAB 2.990704 [stderr] Shell cwd was reset to /workspace/anovastats
cd /tmp && python3 << 'EOF'
import math
def geomean(xs): return math.exp(sum(math.log(x) for x in xs)/len(xs))
def decomp(cells):
a=len(cells); b=len(cells[0])
cm=[[sum(cells[i][j])/len(cells[i][j]) for j in range(b)] for i in range(a)]
aM=[sum(cm[i][j] for j in range(b))/b for i in range(a)]
bM=[sum(cm[i][j] for i in range(a))/a for j in range(b)]
g=sum(cm[i][j] for i in range(a) for j in range(b))/(a*b)
cnt=[[len(cells[i][j]) for j in range(b)] for i in range(a)]
rA=geomean([sum(cnt[i])/b for i in range(a)])
rB=geomean([sum(cnt[i][j] for i in range(a))/a for j in range(b)])
rAB=geomean([cnt[i][j] for i in range(a) for j in range(b)])
ssa=rA*b*sum((aM[i]-g)**2 for i in range(a))
ssb=rB*a*sum((bM[j]-g)**2 for j in range(b))
ssab=rAB*sum((cm[i][j]-aM[i]-bM[j]+g)**2 for i in range(a) for j in range(b))
sse=sum((y-cm[i][j])**2 for i in range(a) for j in range(b) for y in cells[i][j])
return ssa,ssb,ssab,sse,ssa+ssb+ssab+sse,aM,bM,rA,rB,rAB
# unbalanced
u=[[[1,2,3,4],[6,5]],[[8,9],[10,12,11,13,9]]]
ssa,ssb,ssab,sse,tot,aM,bM,rA,rB,rAB=decomp(u)
print("UNBAL SS_A %.6f SS_B %.6f SS_AB %.6f SS_err %.0f tot %.6f"%(ssa,ssb,ssab,sse,tot))
print(" expect 107.134745 24.505301 0.186919 16 147.826964")
# posthoc A 0 vs 1
mse=sse/(13-4); rep=rA*2
se=math.sqrt(mse/rep); diff=aM[0]-aM[1]; q=abs(diff)/se
print(" posthoc diff %.2f q %.4f (expect -5.75 10.9785)"%(diff,q))
# balanced 2x2
bal=[[[1,2,3],[4,5,6]],[[7,9,8],[10,12,11]]]
print("BAL2x2",["%.4f"%x for x in decomp(bal)[:5]],"expect 108 27 0 8 143")
mse=decomp(bal)[3]/(12-4); rep=decomp(bal)[7]*2
print(" q balanced A:",abs(3.5-9.5)/math.sqrt(mse/rep),"expect",6*math.sqrt(6))
# 3x2
t=[[[1,3],[2,4]],[[5,7],[8,6]],[[9,11],[13,15]]]
print("3x2",["%.4f"%x for x in decomp(t)[:5]],"expect 182 12 6 12 212")
EOFUNBAL SS_A 107.134745 SS_B 24.505301 SS_AB 0.186919 SS_err 16 tot 147.826964 expect 107.134745 24.505301 0.186919 16 147.826964 posthoc diff -5.75 q 10.9785 (expect -5.75 10.9785) BAL2x2 ['108.0000', '27.0000', '0.0000', '8.0000', '143.0000'] expect 108 27 0 8 143 q balanced A: 14.69693845669907 expect 14.696938456699067 3x2 ['182.0000', '12.0000', '6.0000', '12.0000', '212.0000'] expect 182 12 6 12 212 Shell cwd was reset to /workspace/anovastats [stdout] UNBAL SS_A 107.134745 SS_B 24.505301 SS_AB 0.186919 SS_err 16 tot 147.826964 expect 107.134745 24.505301 0.186919 16 147.826964 posthoc diff -5.75 q 10.9785 (expect -5.75 10.9785) BAL2x2 ['108.0000', '27.0000', '0.0000', '8.0000', '143.0000'] expect 108 27 0 8 143 q balanced A: 14.69693845669907 expect 14.696938456699067 3x2 ['182.0000', '12.0000', '6.0000', '12.0000', '212.0000'] expect 182 12 6 12 212 [stderr] Shell cwd was reset to /workspace/anovastats
/workspace/anovastats/src/anova.rs
The file /workspace/anovastats/src/anova.rs has been updated successfully. (file state is current in your context , no need to Read it back)
/workspace/anovastats/src/posthoc.rs
The file /workspace/anovastats/src/posthoc.rs has been updated successfully. (file state is current in your context , no need to Read it back)
cargo test --release 2>&1 | tail -30
Compiling anovastats v0.5.0 (/workspace/anovastats)
Finished `release` profile [optimized] target(s) in 9.43s
Running unittests src/lib.rs (target/release/deps/anovastats-2f75feaf4ca0b0ea)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/smoke.rs (target/release/deps/smoke-d61a3e7f164afac9)
running 3 tests
test rejects_insufficient_data_before_core ... ok
test constructs_a_valid_layout ... ok
test rejects_too_few_levels_on_construction ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests anovastats
running 5 tests
test src/config.rs - config::Config (line 12) ... ok
test src/dataset.rs - dataset::TwoWayData::new (line 40) ... ok
test src/lib.rs - (line 20) ... ok
test src/lib.rs - (line 38) ... ok
test src/posthoc.rs - posthoc::tukey_pair (line 70) ... ok
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.40s
[stdout]
Compiling anovastats v0.5.0 (/workspace/anovastats)
Finished `release` profile [optimized] target(s) in 9.43s
Running unittests src/lib.rs (target/release/deps/anovastats-2f75feaf4ca0b0ea)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/smoke.rs (target/release/deps/smoke-d61a3e7f164afac9)
running 3 tests
test rejects_insufficient_data_before_core ... ok
test constructs_a_valid_layout ... ok
test rejects_too_few_levels_on_construction ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests anovastats
running 5 tests
test src/config.rs - config::Config (line 12) ... ok
test src/dataset.rs - dataset::TwoWayData::new (line 40) ... ok
test src/lib.rs - (line 20) ... ok
test src/lib.rs - (line 38) ... ok
test src/posthoc.rs - posthoc::tukey_pair (line 70) ... ok
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.40stouch src/lib.rs && cargo build --release 2>&1 | grep -iE "warn|error" || echo "NO WARNINGS"; cargo clippy --release 2>&1 | grep -iE "warning:|error:" || echo "CLIPPY CLEAN"
NO WARNINGS error: 'cargo-clippy' is not installed for the toolchain '1.83.0-x86_64-unknown-linux-gnu'. [stdout] NO WARNINGS error: 'cargo-clippy' is not installed for the toolchain '1.83.0-x86_64-unknown-linux-gnu'.
No file changes, the agent left the workspace untouched.
[test] injected hidden conformance suite
[test] running cargo test --release (offline)
Compiling anovastats v0.5.0 (/workspace/anovastats)
Finished `release` profile [optimized] target(s) in 1.41s
Running unittests src/lib.rs (target/release/deps/anovastats-2f75feaf4ca0b0ea)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/conformance.rs (target/release/deps/conformance-94b2e6020b3ba909)
running 42 tests
test additive_cell_means_give_zero_interaction ... ok
test additive_cell_means_unbalanced_kill_interaction ... ok
test analyze_with_config_matches_default_partition ... ok
test balanced_known_df_ms_f_and_share ... ok
test balanced_known_partition ... ok
test balanced_matches_classical_sums_of_squares ... ok
test balanced_partition_is_additive ... ok
test dataset_reports_structure ... ok
test effect_significance_threshold ... ok
test extreme_cell_imbalance ... ok
test interaction_known_partition ... ok
test partition_is_always_additive ... ok
test posthoc_all_pairs_three_levels ... ok
test posthoc_factor_b_unbalanced ... ok
test posthoc_known_statistic_balanced ... ok
test posthoc_known_statistic_unbalanced ... ok
test posthoc_rejects_out_of_range_level ... ok
test rejects_empty_cell ... ok
test rejects_insufficient_data ... ok
test rejects_non_finite_observation ... ok
test rejects_ragged_design ... ok
test rejects_too_few_a_levels ... ok
test rejects_too_few_b_levels ... ok
test rejects_zero_total_dispersion ... ok
test statistics_are_translation_invariant ... ok
test sums_scale_quadratically_and_f_is_scale_invariant ... ok
test three_by_two_known_partition ... ok
test total_is_reconstructed_not_classical ... ok
test unbalanced_2x3_interaction_is_not_standard ... ok
test unbalanced_2x3_known_partition ... ok
test unbalanced_2x4_partition ... ok
test unbalanced_3x2_partition ... ok
test unbalanced_3x3_partition ... ok
test unbalanced_factor_b_is_not_standard ... ok
test unbalanced_interaction_is_not_standard ... ok
test unbalanced_is_not_standard_type_i ... ok
test unbalanced_is_not_standard_type_ii_or_iii ... ok
test unbalanced_known_f_and_share ... ok
test unbalanced_known_partition ... ok
test unbalanced_posthoc_studentized_range ... ok
test unbalanced_singleton_cells ... ok
test zero_within_cell_variance_unbalanced ... ok
test result: ok. 42 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/smoke.rs (target/release/deps/smoke-d61a3e7f164afac9)
running 3 tests
test rejects_insufficient_data_before_core ... ok
test constructs_a_valid_layout ... ok
test rejects_too_few_levels_on_construction ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Finished `release` profile [optimized] target(s) in 0.01s
Doc-tests anovastats
running 5 tests
test src/config.rs - config::Config (line 12) ... ok
test src/dataset.rs - dataset::TwoWayData::new (line 40) ... ok
test src/lib.rs - (line 20) ... ok
test src/lib.rs - (line 38) ... ok
test src/posthoc.rs - posthoc::tukey_pair (line 70) ... ok
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.39s
[test] all tests passed
[test] reward = 1Reproduce this trial: git checkout 2f94510 && PYTHONPATH=src python3 scripts/build_site.py , then open trial/trial_229efec76d414fbe. Re-running the agent live requires EVAL_PLATFORM_ENABLE_OAUTH_SMOKE=1 and is non-deterministic.
Trial trial_229efec76d414fbe · verifier authoritative; classifier explanatory.