Statistics

The statistical primitives shared by Combinations, Selectors and Stability monitoring. Every carver, selector and monitoring metric routes its arithmetic through this module, so a formula stated here is the one that runs.

Pearson’s \(\chi^2\)

AutoCarver.stats.pearson_chi2(observed: ndarray, *, guard_zero_expected: bool = False) float

Pearson \(\chi^2\) of a (B, C) observed contingency table.

Replicates scipy.stats.chi2_contingency() defaults: expected frequencies via the outer product of marginals divided by N, with Yates correction iff the table is exactly 2x2 (matches scipy’s own threshold).

\[\chi^2 = \sum_{i, j} \frac{(O_{ij} - E_{ij})^2}{E_{ij}}, \qquad E_{ij} = \frac{n_{i.}\, n_{.j}}{n}\]

where \(n_{i.}\) and \(n_{.j}\) are the row and column marginals and \(n\) the grand total. When the table is exactly \(2 \times 2\), Yates’ continuity correction shrinks \(|O_{ij} - E_{ij}|\) by \(0.5\) before squaring (matches scipy’s own threshold for applying it).

guard_zero_expected replaces the 0/0 of an all-zero row or column with 0 instead of nan. The selector kernels need it (they build tables by bincount, which can produce empty rows); the combination evaluators must not use it — they shift every cell by +tol beforehand, and changing that would break bit-exactness.

Cramér’s \(V\) and Tschuprow’s \(T\)

AutoCarver.stats.cramerv_tschuprowt(chi2: float, n_obs: float, n_rows: int, n_cols: int, tol: float) tuple[float, float]

Cramér’s V and Tschuprow’s T from a chi² computed on an (n_rows, n_cols) table.

\[V = \sqrt{\frac{\chi^2}{N (\min(B, K) - 1)}}, \qquad T = \sqrt{\frac{\chi^2}{N \sqrt{(B-1)(K-1)}}}\]

with \(B\) = n_rows, \(K\) = n_cols, \(N\) = n_obs. Both are NaN when their denominator vanishes (mirrors the binary/ordinal None-on-degenerate convention).

For n_cols == 2, T is instead derived from the (already rounded) V via \(T = V / \sqrt[4]{B - 1}\) — the exact expression the binary combination evaluator’s closed form uses. Both formulas are mathematically identical at K=2, but only computing it this way guarantees the binary and multiclass evaluators agree bit-for-bit (independent sqrt/pow sequences are not guaranteed to round identically) — pinned by the K=2 parity test.

AutoCarver.stats.cramerv_tschuprowt_unrounded(chi2: float, n_obs: float, n_mod_x: float, n_mod_y: float) tuple[float, float]

Selector-side V / T: no tol quantisation, T from the raw chi² (not from V).

\[V = \sqrt{\frac{\chi^2}{N (\min(n_x, n_y) - 1)}}, \qquad T = \sqrt{\frac{\chi^2}{N \sqrt{(n_x-1)(n_y-1)}}}\]

with \(n_x\), \(n_y\) the two features’ modality counts. Different normalisation from cramerv_tschuprowt()n_obs here is the non-missing pair count and there is no rounding — so this is kept as a separate function rather than merged into it; do not “simplify” the two into one.

Kruskal-Wallis’ \(H\)

AutoCarver.stats.tie_correction(values: ndarray) float

Kruskal-Wallis tie factor; 1.0 when N < 2.

\[C_{tie} = 1 - \frac{\sum_t (t^3 - t)}{N^3 - N}\]

where the sum runs over each group of \(t\) tied values and \(N\) is the total sample size. Matches scipy.stats.tiecorrect (which takes ranks; ties are the same either way).

AutoCarver.stats.h_from_rank_sums(rank_sums: ndarray, counts: ndarray, n_obs: float, tie_corr: float) float

Tie-corrected H from per-group rank sums and counts. nan when tie_corr == 0.

\[H = \frac{1}{C_{tie}} \left[\frac{12}{N(N+1)} \sum_g \frac{R_g^2}{n_g} - 3(N+1)\right]\]

where \(R_g\) and \(n_g\) are group g’s rank sum and count, \(N\) the total sample size, and \(C_{tie}\) the tie_correction() factor. nan also whenever any group is empty (0/0 propagates through the sum) — matches scipy.stats.kruskal, which rejects an empty sample.

Wilson frequency confidence bound

AutoCarver.stats.wilson_upper_bound(count: ndarray | int | float, nobs: int, alpha: float) ndarray | float

Upper bound of the two-sided Wilson score interval for count / nobs.

\[\text{upper} = \frac{\hat{p} + \frac{z^2}{2n} + z\sqrt{\frac{\hat{p}(1-\hat{p})}{n} + \frac{z^2}{4n^2}}} {1 + \frac{z^2}{n}}\]

where \(\hat{p} = \text{count}/n\), \(n\) = nobs, and \(z\) is the two-sided normal quantile for alpha.

Parameters:
  • count (array-like or scalar) – Observed successes. Accepts integer counts or float counts (e.g. weighted/aggregated frequencies).

  • nobs (int) – Number of trials. Must be >= 0; returns 1.0 when nobs == 0 so callers treat empty samples as non-significant.

  • alpha (float) – Two-sided significance level (e.g. 0.05 for a 95% interval).

Returns:

Wilson upper bound, same shape as count.

Return type:

array-like or scalar

AutoCarver.stats.is_significantly_below(count: ndarray | int | float, nobs: int, min_freq: float, alpha: float) ndarray | bool

Whether the observed proportion count / nobs is significantly below min_freq.

A modality is significantly below min_freq when the Wilson upper bound of its observed proportion is strictly below min_freq.

Ridit scores

AutoCarver.stats.ridits_from_counts(counts: Series) dict

Scores a count-marginal’s own levels, as a {level: ridit} dict.

Convenience wrapper over ridit_scores_for_levels() keeping the original (non-float-cast) level keys, so the result is directly y.map-able (the carver’s pre-sort scale).

AutoCarver.stats.ridit_scores_for_levels(levels, reference_counts: Series) ndarray

Ridits of arbitrary numeric levels against a fixed train count-marginal.

\[\text{ridit}(j) = F(j^-) + \frac{f_j}{2}\]

where \(f_j\) is level \(j\)’s train frequency and \(F(j^-)\) the cumulative train frequency of all strictly lower levels. A level unseen in the reference gets \(F(j^-)\) alone (zero mass at that level).

Parameters:
  • levels (iterable of numbers) – Levels to score (e.g. a crosstab’s columns) — need not all appear in the reference.

  • reference_counts (pd.Series) – Train count-marginal, indexed by level (value_counts of the train target, or a train crosstab’s column totals). Order does not matter.

Returns:

One ridit per queried level: F(j-1) + f_j/2 for reference levels; a level unseen in the reference gets P_train(y < level) (the natural CDF extension: zero mass at that level), so tables carrying extra levels stay well-defined.

Return type:

np.ndarray

Correspondence analysis

AutoCarver.stats.fit_ca_axis(xtab: DataFrame, tol: float = 1e-10) CAAxis

Fits the correspondence-analysis first axis of a crosstab.

xtab is an (n_rows, K) count crosstab (rows = modalities/groups, columns = target classes). Standardizes the table (row/column mass normalization), takes its SVD, and returns the (sign-fixed) first right singular vector plus the column mass vector needed to project any row’s own profile onto it (see ca_row_scores()).

AutoCarver.stats.ca_row_scores(xtab: DataFrame, axis: CAAxis) Series

Projects each row of xtab onto a fixed CAAxis.

\[\text{score}_i = \sum_k \frac{p_{ik} - c_k}{\sqrt{c_k}}\, v_{1k}\]

where \(p_{ik}\) is row \(i\)’s own profile (proportions across columns), \(c_k\) the fixed (training) column mass, and \(v_1\) the fitted first right singular vector. Only the row’s own profile and the fixed (training) column masses / axis are needed, so this is well-defined for any row set sharing xtab’s columns — including a dev-sample grouping the axis was never fit on, or a carver’s grouped candidate table.

Falls back to (deterministic) descending-frequency scoring when axis.degenerate (encoded as -row_total so ascending sort still yields frequency-descending order).

Raises:

ValueError – When xtab doesn’t carry exactly the classes the axis was fit on — typically a target class present in a later sample but unseen at fit time. The axis is fixed by construction, so such a table cannot be projected onto it (see AutoCarver.stability).

class AutoCarver.stats.CAAxis(col_mass: ndarray, v1: ndarray, degenerate: bool = False)

A fixed correspondence-analysis first axis, reusable to score new rows.

degenerate is True when the training table carried too little structure to define a meaningful axis (fewer than 3 rows, fewer than 2 columns, or an ~zero first singular value); callers then fall back to a frequency-based order (see ca_row_scores()).