0117 nan policy is offered where scipy offers it - CyrilB1531/lodestar GitHub Wiki

0117 โ€” nan_policy is offered where scipy offers it, and omission is a filter

Status: accepted ยท Date: 2026-09-12

Context

scipy's nan_policy is 'propagate', 'raise' or 'omit'. A caller arriving from scipy with real data arrives with missing values, and this package had no answer: #687.

Two facts shaped the answer, both measured rather than read.

'propagate' is already the shipped behaviour. docs/equivalence.md records that nine of the ten families follow scipy's default exactly, and that ChiSquare.Contingency is the exception, refusing a NaN cell because a contingency table's cells are counts whose marginals are divided by.

scipy does not give nan_policy to every test. Read from scipy 1.18.1's signatures, chi2_contingency, fisher_exact and false_discovery_control do not take it. The issue's premise that "every test above takes nan_policy" is wrong on those three.

Decision

A three-valued NanPolicy enum, on the eleven entry points whose scipy counterpart takes nan_policy, defaulting to Propagate.

public enum NanPolicy { Propagate = 0, Raise, Omit }

Propagate = 0 makes default(NanPolicy) the same thing, and no existing call changes behaviour.

ChiSquare.Contingency, FisherExact.Test and the three MultipleComparisons methods do not take it. That is parity, not an omission.

Parity with the library a caller migrates from is this repository's tie-breaker (0008). It settles the argument that 'propagate' and 'raise' are each one line at a call site so only 'omit' carries content: true, and outweighed, because a caller porting a script wants the parameter the script already passes, and a two-valued subset of a three-valued parameter is a divergence to explain.

Omission drops pairs where the inputs are aligned

entry point omission
TTest.Paired, Wilcoxon.Paired listwise โ€” drop the index when either side is NaN
ChiSquare.GoodnessOfFit with expected given listwise
the other eight per-sample

Measured, not reasoned: ttest_rel on [1, 2, NaN, 4, 5] against [2, NaN, 3, 5, 7] with nan_policy='omit' returns statistic = -4.0, df = 2 โ€” three pairs kept, which is listwise and not what dropping each sample independently gives.

Omission is a filter, not a second policy

The family's own guards run afterwards, unchanged. ShapiroWilk.Test still raises below n = 3 and KruskalWallis.Test still raises on a fully tied pool, where scipy returns (nan, nan) with a warning โ€” the two divergences docs/equivalence.md already records.

Answering scipy's (nan, nan) on a path reached only through omission was rejected: it would make one degenerate input raise or not according to how it arrived.

Two consequences worth recording

ChiSquare.GoodnessOfFit with an explicit expected will commonly raise under Omit, because chisquare requires the two to sum alike and omission breaks that by construction. scipy raises ValueError; this package already raises for the same reason.

OneWayAnova.Test and KruskalWallis.Test are params double[][], and C# forbids a parameter after a params array. They take an overload with the policy first โ€” Test(NanPolicy nanPolicy, params double[][] groups) โ€” which is string.Join's shape and keeps the varargs form a caller already uses.

Consequences

Eleven entry points gain an optional parameter; no existing call changes behaviour. Raise throws ArgumentException, the package's idiom.

No axis parameter. scipy's nan_policy belongs to an array-API surface that also carries axis; this package takes spans and single samples, where axis has no meaning.