Missing values, infinite values and degenerate input¶
This page says what the core estimators do with incomplete or degenerate data, and how to ask for a hard failure instead of row deletion.
Missing values: missing="drop" or missing="raise"¶
By default an estimator deletes every row that has a missing value (NaN,
None, pd.NA, NaT) in a column the call uses. That is listwise
deletion, as in Stata and R. The estimator says how many rows went, by
column, in an AssumptionWarning, and the fit reports the smaller N.
Replication work often needs the opposite. If the data should be complete,
a missing value is a bug upstream, and a fit on fewer rows hides it. Pass
missing="raise" and the call is refused instead:
import statspai as sp
fit = sp.regress("wage ~ educ + exper", df, cluster="firm", missing="raise")
# DataInsufficient: regress: missing='raise' and the data hold 3 missing
# value(s) in the columns this call uses, in 3 row(s) -- by column:
# 'exper' (2), 'firm' (1).
The error is a DataInsufficient. Its diagnostics carry
missing_by_column, n_missing and n_rows_missing, so an agent can
repair the call without parsing the message.
The columns counted are the ones the call uses. They are the outcome, the
regressors and covariates (formula terms included), the weights column, the
cluster column (also when it is spelled vce="cluster firm" or
vcov={"CRV1": "firm"}), and any other column named in the call. A missing
value in a column the call does not use is ignored. A missing weight, offset
or exposure drops its row like any other missing value; through 1.43.0,
sp.regress, sp.iv, sp.logit, sp.glm, sp.poisson, sp.panel and
sp.callaway_santanna refused the data instead.
Two kinds of missing value carry meaning and are not counted:
- a missing cohort in a staggered DiD (
g/first_treat/treat_time, ortreatinsp.did) means the unit is never treated; - a missing outcome in
sp.heckmanmarks an unselected row.
A rule per role: response and predictors¶
A missing outcome and a missing regressor are not the same problem. A
missing outcome is often the design (attrition, non-response), and a
silent drop there changes the estimand. A missing regressor is more often
a merge that did not match. missing= therefore also takes a dict with a
rule per role:
fit = sp.regress("wage ~ educ + exper", df, cluster="firm",
missing={"response": "raise", "predictors": "drop"})
There are two roles. "response" is the outcome: the left side of the
formula, or the outcome argument (y=, outcome=). "predictors" is
every other column the call uses. That covers regressors, covariates,
the treatment, instruments, the weights, the cluster column and the unit
and time identifiers. Each role takes "drop" or "raise", and a role
left out of the dict follows the default, "drop". Any other key or
value is refused with a MethodIncompatibility. Its diagnostics carry
allowed, allowed_roles, unknown_roles and invalid_values.
A role set to "raise" is refused as above. The error's diagnostics
then also carry missing_by_role (role to column to count) and
missing_roles, and missing_policy is the full rule. A role set to
"drop" is deleted listwise as before. The result records what that
role held in result.diagnostics["missing_by_role"]:
fit.diagnostics["missing_by_role"]
# {'policy': {'response': 'raise', 'predictors': 'drop'},
# 'missing_by_role': {'predictors': {'exper': 2, 'firm': 1}},
# 'n_rows_missing': 3}
The string forms are unchanged and record nothing. The two exemptions
below still hold: a missing cohort is not counted in either role, and in
sp.heckman the response role counts nothing, because a missing outcome
there marks an unselected row.
missing= is available on regress, logit, probit, poisson, glm,
ivreg, iv, feols, fepois, feglm, qreg, tobit, heckman,
did, callaway_santanna, sun_abraham, did_imputation,
event_study, rdrobust, synth, dml, panel, match, ipw and
aipw. With complete data both settings give the same numbers.
Infinite values¶
inf is not a missing value. It almost always comes from a transformation
that failed, such as log(0) or a division by zero. A used column holding
inf is refused with a DataInsufficient that names each column and its
count (diagnostics["infinite_by_column"]), whatever missing= says. To
have those rows dropped like missing ones, recode them first with
data.replace([np.inf, -np.inf], np.nan). The one exception is the
cohort column of a staggered DiD, where g = inf means never treated.
Other input the core estimators refuse¶
Before any fitting, the same shared check refuses these inputs with a typed error and a recovery hint.
| Input | Error |
|---|---|
| A frame with no rows | DataInsufficient, diagnostics["n_rows"] == 0 |
| A used column with no non-missing value | DataInsufficient, diagnostics["all_missing_columns"] |
| A text outcome where numbers are needed | MethodIncompatibility, diagnostics["column"] |
| A cluster variable with one value | DataInsufficient, diagnostics["n_clusters"] == 1 |
A number column stored as object (for example Decimal values from a SQL
NUMERIC column) is read as numbers, not refused.
Constant and collinear regressors¶
A regressor that never varies, or one that is a linear combination of the
others, is omitted with a note that names it, as Stata does. The
remaining coefficients are then the ones of the fit without that
regressor. sp.regress(collinear="raise") refuses instead. Estimators that
do not report regressor coefficients (the DiD family, sp.dml,
sp.match) give the fit without the redundant covariate. Two cases are
refused because nothing is left to estimate. One is sp.panel(method="fe")
when every regressor is absorbed by the fixed effects. The other is a
sp.rdrobust covariate that is collinear with the running-variable
polynomial.
Where this is tested¶
tests/test_edge_case_contract.py runs each of these inputs over the 25
estimators above, with one cell per role for the rule per role. It also checks that noise of relative size 1e-13 on the
outcome leaves every estimate unchanged and that exact fits recover the
coefficients that generated the data.