microimpute 3.1.2__tar.gz → 3.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {microimpute-3.1.2 → microimpute-3.1.4}/PKG-INFO +11 -2
- {microimpute-3.1.2 → microimpute-3.1.4}/microimpute/__init__.py +8 -1
- {microimpute-3.1.2 → microimpute-3.1.4}/microimpute/config.py +9 -16
- {microimpute-3.1.2 → microimpute-3.1.4}/microimpute/models/__init__.py +4 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/microimpute/models/imputer.py +1 -1
- {microimpute-3.1.2 → microimpute-3.1.4}/microimpute/models/matching.py +71 -10
- {microimpute-3.1.2 → microimpute-3.1.4}/microimpute/models/qrf.py +47 -13
- microimpute-3.1.4/microimpute/py.typed +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/microimpute.egg-info/PKG-INFO +11 -2
- {microimpute-3.1.2 → microimpute-3.1.4}/microimpute.egg-info/SOURCES.txt +2 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/pyproject.toml +20 -5
- {microimpute-3.1.2 → microimpute-3.1.4}/tests/test_autoimpute.py +34 -17
- microimpute-3.1.4/tests/test_public_api.py +22 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/README.md +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/microimpute/comparisons/__init__.py +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/microimpute/comparisons/autoimpute.py +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/microimpute/comparisons/autoimpute_helpers.py +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/microimpute/comparisons/imputations.py +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/microimpute/comparisons/metrics.py +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/microimpute/comparisons/validation.py +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/microimpute/evaluations/__init__.py +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/microimpute/evaluations/cross_validation.py +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/microimpute/evaluations/predictor_analysis.py +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/microimpute/models/mdn.py +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/microimpute/models/ols.py +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/microimpute/models/quantreg.py +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/microimpute/models/zero_inflated.py +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/microimpute/utils/__init__.py +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/microimpute/utils/dashboard_formatter.py +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/microimpute/utils/data.py +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/microimpute/utils/statmatch_hotdeck.py +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/microimpute/utils/type_handling.py +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/microimpute/visualizations/__init__.py +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/microimpute/visualizations/comparison_plots.py +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/microimpute/visualizations/performance_plots.py +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/microimpute.egg-info/dependency_links.txt +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/microimpute.egg-info/requires.txt +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/microimpute.egg-info/top_level.txt +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/setup.cfg +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/tests/test_basic.py +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/tests/test_dashboard_formatter.py +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/tests/test_data_preprocessing.py +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/tests/test_metrics.py +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/tests/test_predictor_analysis.py +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/tests/test_quantile_comparison.py +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/tests/test_smoke_qrf.py +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/tests/test_type_handling.py +0 -0
- {microimpute-3.1.2 → microimpute-3.1.4}/tests/test_visualizations.py +0 -0
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: microimpute
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.4
|
|
4
4
|
Summary: Benchmarking imputation methods for microdata
|
|
5
|
-
Author-email: María Juaristi <juaristi@uni.minerva.edu>, Nikhil Woodruff <nikhil.woodruff@outlook.com>
|
|
5
|
+
Author-email: Vahid Ahmadi <vahid@policyengine.org>, Max Ghenis <max@policyengine.org>, María Juaristi <juaristi@uni.minerva.edu>, Nikhil Woodruff <nikhil.woodruff@outlook.com>
|
|
6
|
+
Project-URL: Repository, https://github.com/PolicyEngine/microimpute
|
|
7
|
+
Project-URL: Documentation, https://policyengine.github.io/microimpute
|
|
8
|
+
Project-URL: Issues, https://github.com/PolicyEngine/microimpute/issues
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
6
15
|
Requires-Python: <3.15,>=3.12
|
|
7
16
|
Description-Content-Type: text/markdown
|
|
8
17
|
Requires-Dist: numpy<3.0.0,>=2.0.0
|
|
@@ -62,7 +62,14 @@ from microimpute.evaluations.predictor_analysis import (
|
|
|
62
62
|
)
|
|
63
63
|
|
|
64
64
|
# Import main models and utilities
|
|
65
|
-
from microimpute.models import
|
|
65
|
+
from microimpute.models import (
|
|
66
|
+
OLS,
|
|
67
|
+
QRF,
|
|
68
|
+
Imputer,
|
|
69
|
+
ImputerResults,
|
|
70
|
+
QuantReg,
|
|
71
|
+
ZeroInflatedImputer,
|
|
72
|
+
)
|
|
66
73
|
|
|
67
74
|
# Import data handling functions
|
|
68
75
|
from microimpute.utils.data import preprocess_data, unnormalize_predictions
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Configuration module for MicroImpute.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
Shared validation, analysis and plotting settings, plus compatibility constants
|
|
5
|
+
for existing callers. Model implementations define their own runtime defaults.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from typing import Any, Dict, List
|
|
@@ -14,7 +14,8 @@ from pydantic import ConfigDict
|
|
|
14
14
|
# arbitrary types like pd.DataFrame
|
|
15
15
|
VALIDATE_CONFIG = ConfigDict(arbitrary_types_allowed=True)
|
|
16
16
|
|
|
17
|
-
#
|
|
17
|
+
# Historical SCF years retained for imports in existing notebooks and callers.
|
|
18
|
+
# This compatibility list does not restrict datasets accepted by the imputers.
|
|
18
19
|
VALID_YEARS: List[int] = [
|
|
19
20
|
1989,
|
|
20
21
|
1992,
|
|
@@ -30,6 +31,7 @@ VALID_YEARS: List[int] = [
|
|
|
30
31
|
2022,
|
|
31
32
|
]
|
|
32
33
|
|
|
34
|
+
# Data configuration
|
|
33
35
|
TRAIN_SIZE: float = 0.8
|
|
34
36
|
TEST_SIZE: float = 0.2
|
|
35
37
|
|
|
@@ -39,40 +41,31 @@ QUANTILES: List[float] = [round(q, 2) for q in np.arange(0.05, 1.00, 0.05)]
|
|
|
39
41
|
# Random state for reproducibility
|
|
40
42
|
RANDOM_STATE: int = 42
|
|
41
43
|
|
|
42
|
-
#
|
|
44
|
+
# Historical parameter mapping retained for import compatibility. This mapping
|
|
45
|
+
# does not configure the learners; their implementations own runtime defaults.
|
|
43
46
|
DEFAULT_MODEL_PARAMS: Dict[str, Dict[str, Any]] = {
|
|
44
47
|
"qrf": {
|
|
45
|
-
# RandomForestQuantileRegressor parameters
|
|
46
48
|
"n_estimators": 100,
|
|
47
49
|
"max_depth": None,
|
|
48
50
|
"min_samples_split": 2,
|
|
49
51
|
"min_samples_leaf": 1,
|
|
50
52
|
"max_features": 1.0,
|
|
51
53
|
},
|
|
52
|
-
"quantreg": {
|
|
53
|
-
# statsmodels QuantReg uses default parameters
|
|
54
|
-
},
|
|
54
|
+
"quantreg": {},
|
|
55
55
|
"ols": {
|
|
56
|
-
# statsmodels OLS uses default parameters
|
|
57
|
-
# LogisticRegression params for categorical targets:
|
|
58
56
|
"l1_ratio": 0,
|
|
59
57
|
"C": 1.0,
|
|
60
58
|
"max_iter": 1000,
|
|
61
59
|
},
|
|
62
|
-
"matching": {
|
|
63
|
-
# StatMatch NND hotdeck default parameters
|
|
64
|
-
},
|
|
60
|
+
"matching": {},
|
|
65
61
|
"mdn": {
|
|
66
|
-
# Backbone network parameters
|
|
67
62
|
"layers": "128-64-32",
|
|
68
63
|
"activation": "ReLU",
|
|
69
64
|
"dropout": 0.0,
|
|
70
65
|
"use_batch_norm": False,
|
|
71
|
-
# MDN head parameters
|
|
72
66
|
"num_gaussian": 5,
|
|
73
67
|
"softmax_temperature": 1.0,
|
|
74
68
|
"n_samples": 100,
|
|
75
|
-
# Training parameters
|
|
76
69
|
"learning_rate": 1e-3,
|
|
77
70
|
"max_epochs": 100,
|
|
78
71
|
"early_stopping_patience": 10,
|
|
@@ -11,6 +11,9 @@ Available models:
|
|
|
11
11
|
- Matching: statistical matching/hot-deck imputation (optional, requires rpy2)
|
|
12
12
|
- MDN: Mixture Density Network for probabilistic imputation
|
|
13
13
|
(optional, requires pytorch-tabular)
|
|
14
|
+
- ZeroInflatedImputer: wrapper composing a model for the probability of a
|
|
15
|
+
zero with a model for the positive part, for variables such as asset
|
|
16
|
+
holdings where a large share of the population is at zero
|
|
14
17
|
|
|
15
18
|
Base classes:
|
|
16
19
|
- Imputer: abstract base class for all imputation models
|
|
@@ -34,3 +37,4 @@ except ImportError:
|
|
|
34
37
|
from microimpute.models.ols import OLS
|
|
35
38
|
from microimpute.models.qrf import QRF
|
|
36
39
|
from microimpute.models.quantreg import QuantReg
|
|
40
|
+
from microimpute.models.zero_inflated import ZeroInflatedImputer
|
|
@@ -261,7 +261,7 @@ class Imputer(ABC):
|
|
|
261
261
|
X_train: DataFrame containing the training data.
|
|
262
262
|
predictors: List of column names to use as predictors.
|
|
263
263
|
imputed_variables: List of column names to impute.
|
|
264
|
-
weight_col: Optional name of the column or column array/series containing sampling weights. When provided,
|
|
264
|
+
weight_col: Optional name of the column or column array/series containing sampling weights. When provided, the weights are passed to the underlying learner's own weighted-fit interface as sample weights. QRF, OLS and Matching support this; QuantReg and MDN raise `NotImplementedError` rather than silently returning an unweighted fit.
|
|
265
265
|
skip_missing: If True, skip variables missing from training data with warning. If False, raise error for missing variables.
|
|
266
266
|
not_numeric_categorical: Optional list of variable names that should
|
|
267
267
|
be treated as numeric even if they would normally be detected as
|
|
@@ -74,6 +74,7 @@ class MatchingResults(ImputerResults):
|
|
|
74
74
|
self.categorical_targets = categorical_targets or {}
|
|
75
75
|
self.boolean_targets = boolean_targets or {}
|
|
76
76
|
self.dummy_processor = dummy_processor
|
|
77
|
+
self.n_failed_records = 0
|
|
77
78
|
|
|
78
79
|
@validate_call(config=VALIDATE_CONFIG)
|
|
79
80
|
def _predict(
|
|
@@ -93,11 +94,24 @@ class MatchingResults(ImputerResults):
|
|
|
93
94
|
Dictionary mapping quantiles to imputed values.
|
|
94
95
|
If return_probs=True, includes 'probabilities' key with one-hot encodings.
|
|
95
96
|
|
|
97
|
+
Side effects:
|
|
98
|
+
Sets ``self.n_failed_records`` to the number of recipient records
|
|
99
|
+
that could not be matched and are NaN in the result, and mirrors it
|
|
100
|
+
on ``result.attrs["n_failed_records"]``. It is reset to 0 on entry,
|
|
101
|
+
so it always describes the most recent call. Matching runs
|
|
102
|
+
single-threaded (``autoimpute`` forces ``n_jobs=1`` when a Matching
|
|
103
|
+
model is present), so concurrent calls on one fitted object would
|
|
104
|
+
race on it.
|
|
105
|
+
|
|
96
106
|
Raises:
|
|
97
107
|
ValueError: If model is not properly set up or
|
|
98
108
|
input data is invalid.
|
|
99
109
|
RuntimeError: If matching or prediction fails.
|
|
100
110
|
"""
|
|
111
|
+
# Reset before any work: a prediction that raises must not leave the
|
|
112
|
+
# previous successful call's count readable as if it described this one.
|
|
113
|
+
self.n_failed_records = 0
|
|
114
|
+
|
|
101
115
|
try:
|
|
102
116
|
self.logger.info(f"Performing matching for {len(X_test)} recipient records")
|
|
103
117
|
|
|
@@ -325,6 +339,18 @@ class MatchingResults(ImputerResults):
|
|
|
325
339
|
)
|
|
326
340
|
raise RuntimeError("Failed to process matching results") from convert_error
|
|
327
341
|
|
|
342
|
+
# Both single-call and chunked predictions replace the previous count.
|
|
343
|
+
# Only missing target values represent unmatched output records.
|
|
344
|
+
self.n_failed_records = int(
|
|
345
|
+
fused0[self.imputed_variables].isna().any(axis=1).sum()
|
|
346
|
+
)
|
|
347
|
+
if self.n_failed_records:
|
|
348
|
+
self.logger.warning(
|
|
349
|
+
f"{self.n_failed_records} of {len(fused0)} records "
|
|
350
|
+
f"({self.n_failed_records / len(fused0):.1%}) could not be "
|
|
351
|
+
"matched and are NaN in the result."
|
|
352
|
+
)
|
|
353
|
+
|
|
328
354
|
# Create output dictionary with results
|
|
329
355
|
imputations: Dict[float, pd.DataFrame] = {}
|
|
330
356
|
prob_results = {} if return_probs else None
|
|
@@ -363,6 +389,12 @@ class MatchingResults(ImputerResults):
|
|
|
363
389
|
if return_probs and prob_results:
|
|
364
390
|
imputations["probabilities"] = prob_results
|
|
365
391
|
|
|
392
|
+
# Mirror the unmatched count onto each frame, so a caller can
|
|
393
|
+
# see it without reaching into the fitted model.
|
|
394
|
+
for frame in imputations.values():
|
|
395
|
+
if isinstance(frame, pd.DataFrame):
|
|
396
|
+
frame.attrs["n_failed_records"] = self.n_failed_records
|
|
397
|
+
|
|
366
398
|
return imputations
|
|
367
399
|
else:
|
|
368
400
|
# If no quantiles specified, use a default one
|
|
@@ -388,6 +420,7 @@ class MatchingResults(ImputerResults):
|
|
|
388
420
|
prob_results[variable] = prob_df
|
|
389
421
|
|
|
390
422
|
imputations[q_default] = imputed_df
|
|
423
|
+
imputed_df.attrs["n_failed_records"] = self.n_failed_records
|
|
391
424
|
|
|
392
425
|
# Add probabilities to results if requested
|
|
393
426
|
if return_probs and prob_results:
|
|
@@ -572,7 +605,13 @@ class Matching(Imputer):
|
|
|
572
605
|
f"Tuning Matching hyperparameters with {n_cv_folds}-fold CV and {n_trials} trials"
|
|
573
606
|
)
|
|
574
607
|
|
|
608
|
+
# Keeps the most recent underlying failure so an all-pruned study can
|
|
609
|
+
# report why, rather than only that nothing succeeded.
|
|
610
|
+
last_trial_error: Optional[BaseException] = None
|
|
611
|
+
|
|
575
612
|
def objective(trial: optuna.Trial) -> float:
|
|
613
|
+
nonlocal last_trial_error
|
|
614
|
+
|
|
576
615
|
params = {
|
|
577
616
|
"dist_fun": trial.suggest_categorical(
|
|
578
617
|
"dist_fun",
|
|
@@ -639,11 +678,19 @@ class Matching(Imputer):
|
|
|
639
678
|
)
|
|
640
679
|
y_pred_chunks.append(fused0[var].values)
|
|
641
680
|
y_val_chunks.append(chunk_y_val.values)
|
|
642
|
-
except Exception:
|
|
643
|
-
#
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
681
|
+
except Exception as e:
|
|
682
|
+
# Substituting the training mean here would
|
|
683
|
+
# score this trial as a mean-predictor, which
|
|
684
|
+
# can beat a genuine matching fit on a
|
|
685
|
+
# low-signal target. Prune instead, so a
|
|
686
|
+
# parameter set that cannot match is never
|
|
687
|
+
# selected as best.
|
|
688
|
+
self.logger.warning(
|
|
689
|
+
f"Matching failed for '{var}' on fold "
|
|
690
|
+
f"{fold_idx} chunk {i}: {e}. Pruning trial."
|
|
691
|
+
)
|
|
692
|
+
last_trial_error = e
|
|
693
|
+
raise optuna.TrialPruned() from e
|
|
647
694
|
|
|
648
695
|
# Combine chunk results
|
|
649
696
|
y_pred = np.concatenate(y_pred_chunks)
|
|
@@ -660,11 +707,15 @@ class Matching(Imputer):
|
|
|
660
707
|
)
|
|
661
708
|
y_pred = fused0[var].values
|
|
662
709
|
y_val_combined = y_val.values
|
|
663
|
-
except Exception:
|
|
664
|
-
#
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
710
|
+
except Exception as e:
|
|
711
|
+
# See above: score the trial on matching, or not at
|
|
712
|
+
# all.
|
|
713
|
+
self.logger.warning(
|
|
714
|
+
f"Matching failed for '{var}' on fold "
|
|
715
|
+
f"{fold_idx}: {e}. Pruning trial."
|
|
716
|
+
)
|
|
717
|
+
last_trial_error = e
|
|
718
|
+
raise optuna.TrialPruned() from e
|
|
668
719
|
|
|
669
720
|
# Use appropriate metric based on variable type
|
|
670
721
|
metric = variable_metrics[var]
|
|
@@ -709,6 +760,16 @@ class Matching(Imputer):
|
|
|
709
760
|
|
|
710
761
|
study.optimize(objective, n_trials=n_trials)
|
|
711
762
|
|
|
763
|
+
if not any(
|
|
764
|
+
trial.state == optuna.trial.TrialState.COMPLETE for trial in study.trials
|
|
765
|
+
):
|
|
766
|
+
raise ValueError(
|
|
767
|
+
"No matching hyperparameter trial succeeded. Last error: "
|
|
768
|
+
f"{last_trial_error}"
|
|
769
|
+
if last_trial_error
|
|
770
|
+
else "No matching hyperparameter trial succeeded"
|
|
771
|
+
)
|
|
772
|
+
|
|
712
773
|
best_value = study.best_value
|
|
713
774
|
self.logger.info(
|
|
714
775
|
f"Matching - Lowest average normalized quantile loss ({n_cv_folds}-fold CV): {best_value}"
|
|
@@ -10,7 +10,7 @@ from pydantic import validate_call
|
|
|
10
10
|
from quantile_forest import RandomForestQuantileRegressor
|
|
11
11
|
from sklearn.ensemble import RandomForestClassifier
|
|
12
12
|
|
|
13
|
-
from microimpute.config import VALIDATE_CONFIG
|
|
13
|
+
from microimpute.config import RANDOM_STATE, VALIDATE_CONFIG
|
|
14
14
|
from microimpute.models.imputer import Imputer, ImputerResults
|
|
15
15
|
|
|
16
16
|
try:
|
|
@@ -607,6 +607,7 @@ class QRF(Imputer):
|
|
|
607
607
|
batch_size: Optional[int] = None,
|
|
608
608
|
cleanup_interval: int = 10,
|
|
609
609
|
max_train_samples: Optional[int] = None,
|
|
610
|
+
seed: Optional[int] = RANDOM_STATE,
|
|
610
611
|
) -> None:
|
|
611
612
|
"""Initialize the QRF model.
|
|
612
613
|
|
|
@@ -618,8 +619,19 @@ class QRF(Imputer):
|
|
|
618
619
|
max_train_samples: If set, subsample X_train to at most this many
|
|
619
620
|
rows before fitting. Reduces memory and training time while
|
|
620
621
|
preserving sequential covariance structure.
|
|
622
|
+
seed: Base random seed. Each imputed variable is given a distinct
|
|
623
|
+
seed derived from it, so variables imputed together draw
|
|
624
|
+
independently. Pass None for non-reproducible draws.
|
|
621
625
|
"""
|
|
622
|
-
|
|
626
|
+
if seed is not None and (
|
|
627
|
+
isinstance(seed, bool)
|
|
628
|
+
or not isinstance(seed, (int, np.integer))
|
|
629
|
+
or not 0 <= seed < 2**32
|
|
630
|
+
):
|
|
631
|
+
raise ValueError(
|
|
632
|
+
f"seed must be an integer from 0 to 2**32 - 1, or None, got {seed!r}"
|
|
633
|
+
)
|
|
634
|
+
super().__init__(log_level=log_level, seed=seed)
|
|
623
635
|
self.models = {}
|
|
624
636
|
self.log_level = log_level
|
|
625
637
|
self.memory_efficient = memory_efficient
|
|
@@ -695,20 +707,45 @@ class QRF(Imputer):
|
|
|
695
707
|
|
|
696
708
|
return data
|
|
697
709
|
|
|
710
|
+
def _seed_for_variable(self, variable: str) -> Optional[int]:
|
|
711
|
+
"""Derive a distinct seed for one imputed variable.
|
|
712
|
+
|
|
713
|
+
Each per-variable model builds its own generator from the seed it is
|
|
714
|
+
given. Handing every variable the same seed makes them draw the same
|
|
715
|
+
random quantiles in the same row order, so variables imputed together
|
|
716
|
+
come out comonotonic regardless of their dependence in the donor. The
|
|
717
|
+
offset is shared with target-specific subsampling and wraps within
|
|
718
|
+
sklearn's uint32 seed range.
|
|
719
|
+
"""
|
|
720
|
+
if self.seed is None:
|
|
721
|
+
return None
|
|
722
|
+
try:
|
|
723
|
+
variable_offset = (self.imputed_variables or []).index(variable)
|
|
724
|
+
except ValueError as error:
|
|
725
|
+
# Falling back to offset 0 would hand this variable the same draws
|
|
726
|
+
# as the first target, which is the comonotonicity this method
|
|
727
|
+
# exists to prevent - and it would do so silently.
|
|
728
|
+
raise ValueError(
|
|
729
|
+
f"Cannot derive a seed for {variable!r}: it is not among the "
|
|
730
|
+
f"imputed variables {list(self.imputed_variables or [])}."
|
|
731
|
+
) from error
|
|
732
|
+
return (int(self.seed) + variable_offset) % 2**32
|
|
733
|
+
|
|
698
734
|
def _create_model_for_variable(self, variable: str, **kwargs) -> Any:
|
|
699
735
|
"""Create the appropriate model (classifier or regressor) based on variable type."""
|
|
700
736
|
categorical_targets = getattr(self, "categorical_targets", {})
|
|
701
737
|
boolean_targets = getattr(self, "boolean_targets", {})
|
|
738
|
+
seed = self._seed_for_variable(variable)
|
|
702
739
|
|
|
703
740
|
if variable in categorical_targets:
|
|
704
741
|
# Use classifier for categorical targets
|
|
705
|
-
return _RandomForestClassifierModel(seed=
|
|
742
|
+
return _RandomForestClassifierModel(seed=seed, logger=self.logger)
|
|
706
743
|
elif variable in boolean_targets:
|
|
707
744
|
# Use classifier for boolean targets
|
|
708
|
-
return _RandomForestClassifierModel(seed=
|
|
745
|
+
return _RandomForestClassifierModel(seed=seed, logger=self.logger)
|
|
709
746
|
else:
|
|
710
747
|
# Use QRF for numeric targets
|
|
711
|
-
return _QRFModel(seed=
|
|
748
|
+
return _QRFModel(seed=seed, logger=self.logger)
|
|
712
749
|
|
|
713
750
|
def _fit_model(
|
|
714
751
|
self,
|
|
@@ -800,12 +837,7 @@ class QRF(Imputer):
|
|
|
800
837
|
self.max_train_samples is not None
|
|
801
838
|
and len(target_train) > self.max_train_samples
|
|
802
839
|
):
|
|
803
|
-
|
|
804
|
-
variable_offset = (self.imputed_variables or []).index(variable)
|
|
805
|
-
except ValueError:
|
|
806
|
-
variable_offset = 0
|
|
807
|
-
seed = None if self.seed is None else self.seed + variable_offset
|
|
808
|
-
rng = np.random.default_rng(seed)
|
|
840
|
+
rng = np.random.default_rng(self._seed_for_variable(variable))
|
|
809
841
|
sel = rng.choice(
|
|
810
842
|
len(target_train), size=self.max_train_samples, replace=False
|
|
811
843
|
)
|
|
@@ -1465,7 +1497,9 @@ class QRF(Imputer):
|
|
|
1465
1497
|
y_val = X_val_fold[var]
|
|
1466
1498
|
|
|
1467
1499
|
# Create and fit QRF model with trial parameters
|
|
1468
|
-
model = _QRFModel(
|
|
1500
|
+
model = _QRFModel(
|
|
1501
|
+
seed=self._seed_for_variable(var), logger=self.logger
|
|
1502
|
+
)
|
|
1469
1503
|
model.fit(
|
|
1470
1504
|
X_train_augmented[encoded_predictors],
|
|
1471
1505
|
X_train_fold[var],
|
|
@@ -1623,7 +1657,7 @@ class QRF(Imputer):
|
|
|
1623
1657
|
|
|
1624
1658
|
# Create and fit RFC model with trial parameters
|
|
1625
1659
|
model = _RandomForestClassifierModel(
|
|
1626
|
-
seed=self.
|
|
1660
|
+
seed=self._seed_for_variable(var), logger=self.logger
|
|
1627
1661
|
)
|
|
1628
1662
|
|
|
1629
1663
|
# Determine variable type and fit appropriately
|
|
File without changes
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: microimpute
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.4
|
|
4
4
|
Summary: Benchmarking imputation methods for microdata
|
|
5
|
-
Author-email: María Juaristi <juaristi@uni.minerva.edu>, Nikhil Woodruff <nikhil.woodruff@outlook.com>
|
|
5
|
+
Author-email: Vahid Ahmadi <vahid@policyengine.org>, Max Ghenis <max@policyengine.org>, María Juaristi <juaristi@uni.minerva.edu>, Nikhil Woodruff <nikhil.woodruff@outlook.com>
|
|
6
|
+
Project-URL: Repository, https://github.com/PolicyEngine/microimpute
|
|
7
|
+
Project-URL: Documentation, https://policyengine.github.io/microimpute
|
|
8
|
+
Project-URL: Issues, https://github.com/PolicyEngine/microimpute/issues
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
6
15
|
Requires-Python: <3.15,>=3.12
|
|
7
16
|
Description-Content-Type: text/markdown
|
|
8
17
|
Requires-Dist: numpy<3.0.0,>=2.0.0
|
|
@@ -2,6 +2,7 @@ README.md
|
|
|
2
2
|
pyproject.toml
|
|
3
3
|
microimpute/__init__.py
|
|
4
4
|
microimpute/config.py
|
|
5
|
+
microimpute/py.typed
|
|
5
6
|
microimpute.egg-info/PKG-INFO
|
|
6
7
|
microimpute.egg-info/SOURCES.txt
|
|
7
8
|
microimpute.egg-info/dependency_links.txt
|
|
@@ -38,6 +39,7 @@ tests/test_dashboard_formatter.py
|
|
|
38
39
|
tests/test_data_preprocessing.py
|
|
39
40
|
tests/test_metrics.py
|
|
40
41
|
tests/test_predictor_analysis.py
|
|
42
|
+
tests/test_public_api.py
|
|
41
43
|
tests/test_quantile_comparison.py
|
|
42
44
|
tests/test_smoke_qrf.py
|
|
43
45
|
tests/test_type_handling.py
|
|
@@ -7,13 +7,23 @@ build-backend = "setuptools.build_meta"
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "microimpute"
|
|
10
|
-
version = "3.1.
|
|
10
|
+
version = "3.1.4"
|
|
11
11
|
description = "Benchmarking imputation methods for microdata"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
authors = [
|
|
14
|
+
{ name = "Vahid Ahmadi", email = "vahid@policyengine.org" },
|
|
15
|
+
{ name = "Max Ghenis", email = "max@policyengine.org" },
|
|
14
16
|
{ name = "María Juaristi", email = "juaristi@uni.minerva.edu" },
|
|
15
17
|
{ name = "Nikhil Woodruff", email = "nikhil.woodruff@outlook.com" }
|
|
16
18
|
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Intended Audience :: Science/Research",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Programming Language :: Python :: 3.14",
|
|
25
|
+
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
26
|
+
]
|
|
17
27
|
requires-python = ">=3.12,<3.15"
|
|
18
28
|
dependencies = [
|
|
19
29
|
"numpy>=2.0.0,<3.0.0",
|
|
@@ -62,12 +72,17 @@ images = [
|
|
|
62
72
|
"kaleido>=0.2.1,<0.3.0", # For exporting plots as PNG/JPG
|
|
63
73
|
]
|
|
64
74
|
|
|
65
|
-
[
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
[project.urls]
|
|
76
|
+
Repository = "https://github.com/PolicyEngine/microimpute"
|
|
77
|
+
Documentation = "https://policyengine.github.io/microimpute"
|
|
78
|
+
Issues = "https://github.com/PolicyEngine/microimpute/issues"
|
|
79
|
+
|
|
80
|
+
[tool.setuptools.packages.find]
|
|
81
|
+
include = ["microimpute*"]
|
|
82
|
+
exclude = ["tests*", "docs*", "examples*", "experiments*"]
|
|
68
83
|
|
|
69
84
|
[tool.setuptools.package-data]
|
|
70
|
-
"microimpute" = ["
|
|
85
|
+
"microimpute" = ["py.typed"]
|
|
71
86
|
|
|
72
87
|
[tool.towncrier]
|
|
73
88
|
package = "microimpute"
|
|
@@ -7,11 +7,10 @@ from sklearn.datasets import load_diabetes
|
|
|
7
7
|
|
|
8
8
|
from microimpute.comparisons.autoimpute import autoimpute, AutoImputeResult
|
|
9
9
|
from microimpute.visualizations import *
|
|
10
|
-
from microimpute.models import QRF, QuantReg, OLS
|
|
11
10
|
|
|
12
11
|
# Check if Matching is available
|
|
13
12
|
try:
|
|
14
|
-
from microimpute.models import Matching
|
|
13
|
+
from microimpute.models import Matching # noqa: F401 (import is the probe)
|
|
15
14
|
|
|
16
15
|
HAS_MATCHING = True
|
|
17
16
|
except ImportError:
|
|
@@ -19,12 +18,27 @@ except ImportError:
|
|
|
19
18
|
|
|
20
19
|
# Check if MDN is available
|
|
21
20
|
try:
|
|
22
|
-
from microimpute.models import MDN
|
|
21
|
+
from microimpute.models import MDN # noqa: F401 (import is the probe)
|
|
23
22
|
|
|
24
23
|
HAS_MDN = True
|
|
25
24
|
except ImportError:
|
|
26
25
|
HAS_MDN = False
|
|
27
26
|
|
|
27
|
+
|
|
28
|
+
def available_models():
|
|
29
|
+
"""The models autoimpute should use in this environment.
|
|
30
|
+
|
|
31
|
+
Matching needs rpy2 and R's StatMatch, and MDN needs pytorch-tabular.
|
|
32
|
+
Naming a model that did not import raises NameError before the call under
|
|
33
|
+
test runs, which is how a missing optional dependency turned into eight
|
|
34
|
+
test errors rather than a skip.
|
|
35
|
+
"""
|
|
36
|
+
# autoimpute's own default is already dependency-aware, building
|
|
37
|
+
# [QRF, OLS, QuantReg] plus Matching and MDN when they import. Passing None
|
|
38
|
+
# uses it, so the installed set is exercised whatever is present.
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
28
42
|
# === Fixtures ===
|
|
29
43
|
|
|
30
44
|
|
|
@@ -87,7 +101,7 @@ def test_autoimpute_basic_structure(
|
|
|
87
101
|
receiver_data=diabetes_receiver,
|
|
88
102
|
predictors=predictors,
|
|
89
103
|
imputed_variables=imputed_variables,
|
|
90
|
-
models=
|
|
104
|
+
models=available_models(),
|
|
91
105
|
hyperparameters={
|
|
92
106
|
"QRF": {"n_estimators": 50},
|
|
93
107
|
"Matching": {"constrained": True},
|
|
@@ -137,7 +151,7 @@ def test_autoimpute_all_models(
|
|
|
137
151
|
receiver_data=diabetes_receiver,
|
|
138
152
|
predictors=predictors,
|
|
139
153
|
imputed_variables=imputed_variables,
|
|
140
|
-
models=
|
|
154
|
+
models=available_models(),
|
|
141
155
|
impute_all=True, # Return results for all models
|
|
142
156
|
log_level="WARNING",
|
|
143
157
|
)
|
|
@@ -202,7 +216,7 @@ def test_autoimpute_with_hyperparameters(simple_data: tuple) -> None:
|
|
|
202
216
|
receiver_data=receiver,
|
|
203
217
|
predictors=["x1", "x2"],
|
|
204
218
|
imputed_variables=["y1"],
|
|
205
|
-
models=
|
|
219
|
+
models=available_models(),
|
|
206
220
|
hyperparameters=hyperparameters,
|
|
207
221
|
log_level="WARNING",
|
|
208
222
|
)
|
|
@@ -224,7 +238,7 @@ def test_autoimpute_multiple_imputed_variables(simple_data: tuple) -> None:
|
|
|
224
238
|
receiver_data=receiver,
|
|
225
239
|
predictors=["x1", "x2"],
|
|
226
240
|
imputed_variables=["y1", "y2"], # Multiple variables
|
|
227
|
-
models=
|
|
241
|
+
models=available_models(),
|
|
228
242
|
log_level="WARNING",
|
|
229
243
|
)
|
|
230
244
|
|
|
@@ -246,7 +260,7 @@ def test_autoimpute_large_receiver() -> None:
|
|
|
246
260
|
receiver_data=receiver,
|
|
247
261
|
predictors=["x"],
|
|
248
262
|
imputed_variables=["y"],
|
|
249
|
-
models=
|
|
263
|
+
models=available_models(),
|
|
250
264
|
log_level="WARNING",
|
|
251
265
|
)
|
|
252
266
|
|
|
@@ -267,7 +281,7 @@ def test_autoimpute_best_method_selection(simple_data: tuple) -> None:
|
|
|
267
281
|
receiver_data=receiver,
|
|
268
282
|
predictors=["x1", "x2"],
|
|
269
283
|
imputed_variables=["y1"],
|
|
270
|
-
models=
|
|
284
|
+
models=available_models(),
|
|
271
285
|
log_level="WARNING",
|
|
272
286
|
)
|
|
273
287
|
|
|
@@ -326,7 +340,7 @@ def test_autoimpute_cv_results_structure(simple_data: tuple) -> None:
|
|
|
326
340
|
receiver_data=receiver,
|
|
327
341
|
predictors=["x1", "x2"],
|
|
328
342
|
imputed_variables=["y1"],
|
|
329
|
-
models=
|
|
343
|
+
models=available_models(),
|
|
330
344
|
log_level="WARNING",
|
|
331
345
|
)
|
|
332
346
|
|
|
@@ -372,13 +386,13 @@ def test_autoimpute_missing_predictors() -> None:
|
|
|
372
386
|
}
|
|
373
387
|
)
|
|
374
388
|
|
|
375
|
-
with pytest.raises(Exception):
|
|
389
|
+
with pytest.raises(Exception, match=r"Missing columns in receiver data"):
|
|
376
390
|
autoimpute(
|
|
377
391
|
donor_data=donor,
|
|
378
392
|
receiver_data=receiver,
|
|
379
393
|
predictors=["x1", "x2"], # x2 not in receiver
|
|
380
394
|
imputed_variables=["y"],
|
|
381
|
-
models=
|
|
395
|
+
models=available_models(),
|
|
382
396
|
log_level="WARNING",
|
|
383
397
|
)
|
|
384
398
|
|
|
@@ -392,7 +406,9 @@ def test_autoimpute_invalid_model_specification() -> None:
|
|
|
392
406
|
receiver = pd.DataFrame({"x": np.random.randn(10)})
|
|
393
407
|
|
|
394
408
|
# Invalid model type
|
|
395
|
-
with pytest.raises(
|
|
409
|
+
with pytest.raises(
|
|
410
|
+
Exception, match=r"(?s)validation error.*Input should be a type"
|
|
411
|
+
):
|
|
396
412
|
autoimpute(
|
|
397
413
|
donor_data=donor,
|
|
398
414
|
receiver_data=receiver,
|
|
@@ -461,7 +477,7 @@ def test_autoimpute_consistency(simple_data: tuple) -> None:
|
|
|
461
477
|
receiver_data=receiver,
|
|
462
478
|
predictors=["x1", "x2"],
|
|
463
479
|
imputed_variables=["y1"],
|
|
464
|
-
models=
|
|
480
|
+
models=available_models(),
|
|
465
481
|
log_level="WARNING",
|
|
466
482
|
)
|
|
467
483
|
|
|
@@ -470,7 +486,7 @@ def test_autoimpute_consistency(simple_data: tuple) -> None:
|
|
|
470
486
|
receiver_data=receiver,
|
|
471
487
|
predictors=["x1", "x2"],
|
|
472
488
|
imputed_variables=["y1"],
|
|
473
|
-
models=
|
|
489
|
+
models=available_models(),
|
|
474
490
|
log_level="WARNING",
|
|
475
491
|
)
|
|
476
492
|
|
|
@@ -480,5 +496,6 @@ def test_autoimpute_consistency(simple_data: tuple) -> None:
|
|
|
480
496
|
if model_name in results2.cv_results:
|
|
481
497
|
loss1 = results1.cv_results[model_name]["quantile_loss"]["mean_test"]
|
|
482
498
|
loss2 = results2.cv_results[model_name]["quantile_loss"]["mean_test"]
|
|
483
|
-
|
|
484
|
-
|
|
499
|
+
assert not np.isnan(loss1), f"{model_name} produced a NaN loss"
|
|
500
|
+
assert not np.isnan(loss2), f"{model_name} produced a NaN loss"
|
|
501
|
+
np.testing.assert_allclose(loss1, loss2, rtol=0.10)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Compatibility checks for public imports used by published examples."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_legacy_scf_years_remain_importable() -> None:
|
|
5
|
+
"""Existing SCF examples retain their historical supported-year list."""
|
|
6
|
+
from microimpute.config import VALID_YEARS
|
|
7
|
+
|
|
8
|
+
assert VALID_YEARS == list(range(1989, 2023, 3))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_legacy_model_parameters_remain_importable() -> None:
|
|
12
|
+
"""The mapping stays importable for callers that still read it.
|
|
13
|
+
|
|
14
|
+
Asserting its full contents would freeze values nothing in the package
|
|
15
|
+
reads: `DEFAULT_MODEL_PARAMS` has no remaining callers inside microimpute,
|
|
16
|
+
and the real defaults live in each model. Check only what a downstream
|
|
17
|
+
caller relies on - that the import works and the expected keys are there.
|
|
18
|
+
"""
|
|
19
|
+
from microimpute.config import DEFAULT_MODEL_PARAMS
|
|
20
|
+
|
|
21
|
+
assert set(DEFAULT_MODEL_PARAMS) == {"qrf", "quantreg", "ols", "matching", "mdn"}
|
|
22
|
+
assert all(isinstance(v, dict) for v in DEFAULT_MODEL_PARAMS.values())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|