Skip to content

OLS random quantile sampling draws only ten residual values, shared across calls and variables #208

Description

@vahid-ahmadi

OLS._predict_quantile generates a fixed number of residual draws — ten — regardless of how many rows are being predicted, and rebuilds its generator from self.seed on every call. The imputed conditional distribution is therefore a ten-point discrete mixture, and repeated calls and separate target variables all receive the same ten values in the same row order.

Lower priority than #207 because this path is opt-in: random_quantile_sample defaults to False (ols.py:304), autoimpute() never reaches it (comparisons/autoimpute_helpers.py:373-377 always passes an explicit quantiles=[quantile]), and no caller inside the package sets it True. It is still wrong for anyone who does opt in.

Mechanism

# microimpute/models/ols.py:390
count_samples: int = 10
# microimpute/models/ols.py:426
rng = np.random.default_rng(self.seed)

Ten beta draws are mapped through norm.ppf and then assigned across all n test rows, so the realised moments of the imputed distribution are whatever those ten draws happened to be — not the fitted residual distribution.

Reproduction

import numpy as np, pandas as pd, logging
from microimpute.models.ols import OLSResults

o = OLSResults.__new__(OLSResults); o.seed = 42; o.logger = logging.getLogger("x")
v = o._predict_quantile(pd.Series(np.zeros(500)), 1.0, 0.5, True)
print(len(np.unique(v.values)))                                               # 10, not 500
print(np.allclose(v, o._predict_quantile(pd.Series(np.zeros(500)), 1.0, 0.5, True)))  # True

Ten distinct multipliers for 500 rows, and the second call is identical to the first.

Because the per-row standard error is proportional between two models fitted on the same X, the ratio of the two variables' shocks has exactly one distinct value across the test set — the two imputed variables are perfectly comonotonic. On data with a true residual correlation of −0.041, the imputed shocks correlate at 0.9999999999999999.

Suggested fix

Draw one variate per row (size=len(mean_preds)), and hold a single np.random.Generator on the results object, advanced across calls and variables, rather than reseeding from self.seed inside the method. qrf.py:273 already draws per row; OLS did not follow. The cross-variable half of the problem is the same root cause as #207.

Origin

Long-standing rather than a regression: eb5e88c, "quantile sampling for default imputation (quantreg and ols)", 7 May 2025. No comment suggests the count of ten is deliberate.

Found during a pre-JOSS-submission audit (#201), reproduced independently twice.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions