microimpute 3.1.0__tar.gz → 3.1.2__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.
Files changed (46) hide show
  1. {microimpute-3.1.0 → microimpute-3.1.2}/PKG-INFO +2 -2
  2. {microimpute-3.1.0 → microimpute-3.1.2}/microimpute/models/qrf.py +31 -0
  3. {microimpute-3.1.0 → microimpute-3.1.2}/microimpute/models/zero_inflated.py +37 -7
  4. {microimpute-3.1.0 → microimpute-3.1.2}/microimpute.egg-info/PKG-INFO +2 -2
  5. {microimpute-3.1.0 → microimpute-3.1.2}/microimpute.egg-info/requires.txt +1 -1
  6. {microimpute-3.1.0 → microimpute-3.1.2}/pyproject.toml +8 -2
  7. {microimpute-3.1.0 → microimpute-3.1.2}/README.md +0 -0
  8. {microimpute-3.1.0 → microimpute-3.1.2}/microimpute/__init__.py +0 -0
  9. {microimpute-3.1.0 → microimpute-3.1.2}/microimpute/comparisons/__init__.py +0 -0
  10. {microimpute-3.1.0 → microimpute-3.1.2}/microimpute/comparisons/autoimpute.py +0 -0
  11. {microimpute-3.1.0 → microimpute-3.1.2}/microimpute/comparisons/autoimpute_helpers.py +0 -0
  12. {microimpute-3.1.0 → microimpute-3.1.2}/microimpute/comparisons/imputations.py +0 -0
  13. {microimpute-3.1.0 → microimpute-3.1.2}/microimpute/comparisons/metrics.py +0 -0
  14. {microimpute-3.1.0 → microimpute-3.1.2}/microimpute/comparisons/validation.py +0 -0
  15. {microimpute-3.1.0 → microimpute-3.1.2}/microimpute/config.py +0 -0
  16. {microimpute-3.1.0 → microimpute-3.1.2}/microimpute/evaluations/__init__.py +0 -0
  17. {microimpute-3.1.0 → microimpute-3.1.2}/microimpute/evaluations/cross_validation.py +0 -0
  18. {microimpute-3.1.0 → microimpute-3.1.2}/microimpute/evaluations/predictor_analysis.py +0 -0
  19. {microimpute-3.1.0 → microimpute-3.1.2}/microimpute/models/__init__.py +0 -0
  20. {microimpute-3.1.0 → microimpute-3.1.2}/microimpute/models/imputer.py +0 -0
  21. {microimpute-3.1.0 → microimpute-3.1.2}/microimpute/models/matching.py +0 -0
  22. {microimpute-3.1.0 → microimpute-3.1.2}/microimpute/models/mdn.py +0 -0
  23. {microimpute-3.1.0 → microimpute-3.1.2}/microimpute/models/ols.py +0 -0
  24. {microimpute-3.1.0 → microimpute-3.1.2}/microimpute/models/quantreg.py +0 -0
  25. {microimpute-3.1.0 → microimpute-3.1.2}/microimpute/utils/__init__.py +0 -0
  26. {microimpute-3.1.0 → microimpute-3.1.2}/microimpute/utils/dashboard_formatter.py +0 -0
  27. {microimpute-3.1.0 → microimpute-3.1.2}/microimpute/utils/data.py +0 -0
  28. {microimpute-3.1.0 → microimpute-3.1.2}/microimpute/utils/statmatch_hotdeck.py +0 -0
  29. {microimpute-3.1.0 → microimpute-3.1.2}/microimpute/utils/type_handling.py +0 -0
  30. {microimpute-3.1.0 → microimpute-3.1.2}/microimpute/visualizations/__init__.py +0 -0
  31. {microimpute-3.1.0 → microimpute-3.1.2}/microimpute/visualizations/comparison_plots.py +0 -0
  32. {microimpute-3.1.0 → microimpute-3.1.2}/microimpute/visualizations/performance_plots.py +0 -0
  33. {microimpute-3.1.0 → microimpute-3.1.2}/microimpute.egg-info/SOURCES.txt +0 -0
  34. {microimpute-3.1.0 → microimpute-3.1.2}/microimpute.egg-info/dependency_links.txt +0 -0
  35. {microimpute-3.1.0 → microimpute-3.1.2}/microimpute.egg-info/top_level.txt +0 -0
  36. {microimpute-3.1.0 → microimpute-3.1.2}/setup.cfg +0 -0
  37. {microimpute-3.1.0 → microimpute-3.1.2}/tests/test_autoimpute.py +0 -0
  38. {microimpute-3.1.0 → microimpute-3.1.2}/tests/test_basic.py +0 -0
  39. {microimpute-3.1.0 → microimpute-3.1.2}/tests/test_dashboard_formatter.py +0 -0
  40. {microimpute-3.1.0 → microimpute-3.1.2}/tests/test_data_preprocessing.py +0 -0
  41. {microimpute-3.1.0 → microimpute-3.1.2}/tests/test_metrics.py +0 -0
  42. {microimpute-3.1.0 → microimpute-3.1.2}/tests/test_predictor_analysis.py +0 -0
  43. {microimpute-3.1.0 → microimpute-3.1.2}/tests/test_quantile_comparison.py +0 -0
  44. {microimpute-3.1.0 → microimpute-3.1.2}/tests/test_smoke_qrf.py +0 -0
  45. {microimpute-3.1.0 → microimpute-3.1.2}/tests/test_type_handling.py +0 -0
  46. {microimpute-3.1.0 → microimpute-3.1.2}/tests/test_visualizations.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microimpute
3
- Version: 3.1.0
3
+ Version: 3.1.2
4
4
  Summary: Benchmarking imputation methods for microdata
5
5
  Author-email: María Juaristi <juaristi@uni.minerva.edu>, Nikhil Woodruff <nikhil.woodruff@outlook.com>
6
6
  Requires-Python: <3.15,>=3.12
@@ -21,7 +21,7 @@ Requires-Dist: psutil
21
21
  Provides-Extra: dev
22
22
  Requires-Dist: pytest<10.0.0,>=8.0.0; extra == "dev"
23
23
  Requires-Dist: pytest-cov<8.0.0,>=6.0.0; extra == "dev"
24
- Requires-Dist: ruff>=0.9.0; extra == "dev"
24
+ Requires-Dist: ruff<0.17.0,>=0.16.0; extra == "dev"
25
25
  Requires-Dist: mypy<2.0.0,>=1.2.3; extra == "dev"
26
26
  Requires-Dist: build<2.0.0,>=1.2.0; extra == "dev"
27
27
  Requires-Dist: towncrier>=24.8.0; extra == "dev"
@@ -50,6 +50,7 @@ class _RandomForestClassifierModel:
50
50
  self.var_type = None
51
51
  self.categories = None
52
52
  self.label_map = None
53
+ self.feature_columns: List[str] = []
53
54
 
54
55
  def fit(
55
56
  self,
@@ -99,10 +100,24 @@ class _RandomForestClassifierModel:
99
100
  fit_kwargs = {}
100
101
  if sample_weight is not None:
101
102
  fit_kwargs["sample_weight"] = np.asarray(sample_weight, dtype=float)
103
+ self.feature_columns = list(X.columns)
102
104
  self.classifier.fit(X, y_encoded, **fit_kwargs)
103
105
 
106
+ def _align_features(self, X: pd.DataFrame) -> pd.DataFrame:
107
+ """Reorder prediction features to the fitted sklearn column contract."""
108
+ if not self.feature_columns:
109
+ return X
110
+ missing = [column for column in self.feature_columns if column not in X.columns]
111
+ if missing:
112
+ raise ValueError(
113
+ "Prediction data is missing fitted feature columns for "
114
+ f"{self.output_column}: {missing}"
115
+ )
116
+ return X.loc[:, self.feature_columns]
117
+
104
118
  def predict(self, X: pd.DataFrame, return_probs: bool = False) -> pd.Series:
105
119
  """Predict classes or probabilities."""
120
+ X = self._align_features(X)
106
121
  if return_probs:
107
122
  probs = self.classifier.predict_proba(X)
108
123
  # Return both probabilities and the original category labels
@@ -151,6 +166,7 @@ class _QRFModel:
151
166
  self.logger = logger
152
167
  self.qrf = None
153
168
  self.output_column = None
169
+ self.feature_columns: List[str] = []
154
170
  # Create the RNG once at construction so that repeated predict()
155
171
  # calls consume state progressively and return different draws.
156
172
  self._rng = np.random.default_rng(self.seed)
@@ -192,8 +208,21 @@ class _QRFModel:
192
208
  fit_kwargs = {}
193
209
  if sample_weight is not None:
194
210
  fit_kwargs["sample_weight"] = np.asarray(sample_weight, dtype=float)
211
+ self.feature_columns = list(X.columns)
195
212
  self.qrf.fit(X, y.values.ravel(), **fit_kwargs)
196
213
 
214
+ def _align_features(self, X: pd.DataFrame) -> pd.DataFrame:
215
+ """Reorder prediction features to the fitted QRF column contract."""
216
+ if not self.feature_columns:
217
+ return X
218
+ missing = [column for column in self.feature_columns if column not in X.columns]
219
+ if missing:
220
+ raise ValueError(
221
+ "Prediction data is missing fitted feature columns for "
222
+ f"{self.output_column}: {missing}"
223
+ )
224
+ return X.loc[:, self.feature_columns]
225
+
197
226
  def predict(
198
227
  self,
199
228
  X: pd.DataFrame,
@@ -218,6 +247,8 @@ class _QRFModel:
218
247
  across quantiles for a given row and is used when the caller
219
248
  supplies an explicit ``quantiles`` list.
220
249
  """
250
+ X = self._align_features(X)
251
+
221
252
  # Deterministic path: user asked for a specific quantile — query the
222
253
  # QRF directly so that for any row i,
223
254
  # prediction(q_low) <= prediction(q_mid) <= prediction(q_high).
@@ -232,6 +232,9 @@ class ZeroInflatedImputer(Imputer):
232
232
  for v in imputed_variables
233
233
  if v in self.numeric_targets or v in constant_numeric_targets
234
234
  ]
235
+ nested_not_numeric_categorical = list(
236
+ dict.fromkeys([*(not_numeric_categorical or []), *numeric_targets])
237
+ )
235
238
  # Sequential (chained-equations) imputation: condition each numeric
236
239
  # target on the original predictors plus the previously-fit numeric
237
240
  # targets, so the imputed vector preserves cross-variable joint
@@ -255,6 +258,7 @@ class ZeroInflatedImputer(Imputer):
255
258
  variable=var,
256
259
  regime=regime,
257
260
  y=y,
261
+ not_numeric_categorical=nested_not_numeric_categorical,
258
262
  )
259
263
  bundle["predictors"] = list(seq_predictors)
260
264
  self._per_variable[var] = bundle
@@ -302,6 +306,7 @@ class ZeroInflatedImputer(Imputer):
302
306
  variable: str,
303
307
  regime: str,
304
308
  y: np.ndarray,
309
+ not_numeric_categorical: Optional[List[str]] = None,
305
310
  ) -> Dict[str, Any]:
306
311
  """Fit the gate and base imputer(s) for one numeric target.
307
312
 
@@ -317,7 +322,12 @@ class ZeroInflatedImputer(Imputer):
317
322
  # No gate; single base imputer on the full training set.
318
323
  return {
319
324
  "kind": "single",
320
- "base": self._fit_base_single(X_train, predictors, variable),
325
+ "base": self._fit_base_single(
326
+ X_train,
327
+ predictors,
328
+ variable,
329
+ not_numeric_categorical=not_numeric_categorical,
330
+ ),
321
331
  }
322
332
 
323
333
  if regime == REGIME_ZI_POSITIVE:
@@ -326,7 +336,10 @@ class ZeroInflatedImputer(Imputer):
326
336
  clf.fit(X_pred, labels)
327
337
  pos_mask = y > self.zero_atol
328
338
  pos_base = self._fit_base_single(
329
- X_train.loc[pos_mask], predictors, variable
339
+ X_train.loc[pos_mask],
340
+ predictors,
341
+ variable,
342
+ not_numeric_categorical=not_numeric_categorical,
330
343
  )
331
344
  return {
332
345
  "kind": "zi_positive",
@@ -340,7 +353,10 @@ class ZeroInflatedImputer(Imputer):
340
353
  clf.fit(X_pred, labels)
341
354
  neg_mask = y < -self.zero_atol
342
355
  neg_base = self._fit_base_single(
343
- X_train.loc[neg_mask], predictors, variable
356
+ X_train.loc[neg_mask],
357
+ predictors,
358
+ variable,
359
+ not_numeric_categorical=not_numeric_categorical,
344
360
  )
345
361
  return {
346
362
  "kind": "zi_negative",
@@ -360,10 +376,16 @@ class ZeroInflatedImputer(Imputer):
360
376
  "kind": "sign_only",
361
377
  "classifier": clf,
362
378
  "positive_base": self._fit_base_single(
363
- X_train.loc[pos_mask], predictors, variable
379
+ X_train.loc[pos_mask],
380
+ predictors,
381
+ variable,
382
+ not_numeric_categorical=not_numeric_categorical,
364
383
  ),
365
384
  "negative_base": self._fit_base_single(
366
- X_train.loc[neg_mask], predictors, variable
385
+ X_train.loc[neg_mask],
386
+ predictors,
387
+ variable,
388
+ not_numeric_categorical=not_numeric_categorical,
367
389
  ),
368
390
  }
369
391
 
@@ -382,10 +404,16 @@ class ZeroInflatedImputer(Imputer):
382
404
  "kind": "three_sign",
383
405
  "classifier": clf,
384
406
  "positive_base": self._fit_base_single(
385
- X_train.loc[pos_mask], predictors, variable
407
+ X_train.loc[pos_mask],
408
+ predictors,
409
+ variable,
410
+ not_numeric_categorical=not_numeric_categorical,
386
411
  ),
387
412
  "negative_base": self._fit_base_single(
388
- X_train.loc[neg_mask], predictors, variable
413
+ X_train.loc[neg_mask],
414
+ predictors,
415
+ variable,
416
+ not_numeric_categorical=not_numeric_categorical,
389
417
  ),
390
418
  }
391
419
 
@@ -396,6 +424,7 @@ class ZeroInflatedImputer(Imputer):
396
424
  X_train: pd.DataFrame,
397
425
  predictors: List[str],
398
426
  variable: str,
427
+ not_numeric_categorical: Optional[List[str]] = None,
399
428
  ) -> ImputerResults:
400
429
  """Fit a single base Imputer on a (possibly filtered) slice."""
401
430
  imputer = self.base_imputer_class(
@@ -406,6 +435,7 @@ class ZeroInflatedImputer(Imputer):
406
435
  X_train=X_train,
407
436
  predictors=predictors,
408
437
  imputed_variables=[variable],
438
+ not_numeric_categorical=not_numeric_categorical,
409
439
  )
410
440
 
411
441
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microimpute
3
- Version: 3.1.0
3
+ Version: 3.1.2
4
4
  Summary: Benchmarking imputation methods for microdata
5
5
  Author-email: María Juaristi <juaristi@uni.minerva.edu>, Nikhil Woodruff <nikhil.woodruff@outlook.com>
6
6
  Requires-Python: <3.15,>=3.12
@@ -21,7 +21,7 @@ Requires-Dist: psutil
21
21
  Provides-Extra: dev
22
22
  Requires-Dist: pytest<10.0.0,>=8.0.0; extra == "dev"
23
23
  Requires-Dist: pytest-cov<8.0.0,>=6.0.0; extra == "dev"
24
- Requires-Dist: ruff>=0.9.0; extra == "dev"
24
+ Requires-Dist: ruff<0.17.0,>=0.16.0; extra == "dev"
25
25
  Requires-Dist: mypy<2.0.0,>=1.2.3; extra == "dev"
26
26
  Requires-Dist: build<2.0.0,>=1.2.0; extra == "dev"
27
27
  Requires-Dist: towncrier>=24.8.0; extra == "dev"
@@ -15,7 +15,7 @@ psutil
15
15
  [dev]
16
16
  pytest<10.0.0,>=8.0.0
17
17
  pytest-cov<8.0.0,>=6.0.0
18
- ruff>=0.9.0
18
+ ruff<0.17.0,>=0.16.0
19
19
  mypy<2.0.0,>=1.2.3
20
20
  build<2.0.0,>=1.2.0
21
21
  towncrier>=24.8.0
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
 
8
8
  [project]
9
9
  name = "microimpute"
10
- version = "3.1.0"
10
+ version = "3.1.2"
11
11
  description = "Benchmarking imputation methods for microdata"
12
12
  readme = "README.md"
13
13
  authors = [
@@ -35,7 +35,7 @@ dependencies = [
35
35
  dev = [
36
36
  "pytest>=8.0.0,<10.0.0",
37
37
  "pytest-cov>=6.0.0,<8.0.0",
38
- "ruff>=0.9.0",
38
+ "ruff>=0.16.0,<0.17.0",
39
39
  "mypy>=1.2.3,<2.0.0",
40
40
  "build>=1.2.0,<2.0.0",
41
41
  "towncrier>=24.8.0",
@@ -101,3 +101,9 @@ showcontent = true
101
101
  directory = "removed"
102
102
  name = "Removed"
103
103
  showcontent = true
104
+
105
+ [tool.ruff]
106
+ # Documentation is prose, not a lint surface. ruff 0.16.0 began formatting
107
+ # Python inside markdown code blocks, which reformatted five docs files that
108
+ # nobody had edited and turned the Lint job red on an untouched checkout.
109
+ extend-exclude = ["docs/**/*.md"]
File without changes
File without changes