microimpute 3.2.0__tar.gz → 3.2.1__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 (49) hide show
  1. {microimpute-3.2.0 → microimpute-3.2.1}/PKG-INFO +1 -1
  2. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute.egg-info/PKG-INFO +1 -1
  3. {microimpute-3.2.0 → microimpute-3.2.1}/pyproject.toml +1 -1
  4. {microimpute-3.2.0 → microimpute-3.2.1}/tests/test_moop_spm_experiment.py +186 -3
  5. {microimpute-3.2.0 → microimpute-3.2.1}/README.md +0 -0
  6. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute/__init__.py +0 -0
  7. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute/comparisons/__init__.py +0 -0
  8. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute/comparisons/autoimpute.py +0 -0
  9. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute/comparisons/autoimpute_helpers.py +0 -0
  10. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute/comparisons/imputations.py +0 -0
  11. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute/comparisons/metrics.py +0 -0
  12. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute/comparisons/validation.py +0 -0
  13. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute/config.py +0 -0
  14. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute/evaluations/__init__.py +0 -0
  15. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute/evaluations/cross_validation.py +0 -0
  16. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute/evaluations/predictor_analysis.py +0 -0
  17. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute/models/__init__.py +0 -0
  18. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute/models/imputer.py +0 -0
  19. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute/models/matching.py +0 -0
  20. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute/models/mdn.py +0 -0
  21. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute/models/ols.py +0 -0
  22. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute/models/qrf.py +0 -0
  23. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute/models/quantreg.py +0 -0
  24. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute/models/zero_inflated.py +0 -0
  25. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute/py.typed +0 -0
  26. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute/utils/__init__.py +0 -0
  27. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute/utils/dashboard_formatter.py +0 -0
  28. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute/utils/data.py +0 -0
  29. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute/utils/statmatch_hotdeck.py +0 -0
  30. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute/utils/type_handling.py +0 -0
  31. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute/visualizations/__init__.py +0 -0
  32. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute/visualizations/comparison_plots.py +0 -0
  33. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute/visualizations/performance_plots.py +0 -0
  34. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute.egg-info/SOURCES.txt +0 -0
  35. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute.egg-info/dependency_links.txt +0 -0
  36. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute.egg-info/requires.txt +0 -0
  37. {microimpute-3.2.0 → microimpute-3.2.1}/microimpute.egg-info/top_level.txt +0 -0
  38. {microimpute-3.2.0 → microimpute-3.2.1}/setup.cfg +0 -0
  39. {microimpute-3.2.0 → microimpute-3.2.1}/tests/test_autoimpute.py +0 -0
  40. {microimpute-3.2.0 → microimpute-3.2.1}/tests/test_basic.py +0 -0
  41. {microimpute-3.2.0 → microimpute-3.2.1}/tests/test_dashboard_formatter.py +0 -0
  42. {microimpute-3.2.0 → microimpute-3.2.1}/tests/test_data_preprocessing.py +0 -0
  43. {microimpute-3.2.0 → microimpute-3.2.1}/tests/test_metrics.py +0 -0
  44. {microimpute-3.2.0 → microimpute-3.2.1}/tests/test_predictor_analysis.py +0 -0
  45. {microimpute-3.2.0 → microimpute-3.2.1}/tests/test_public_api.py +0 -0
  46. {microimpute-3.2.0 → microimpute-3.2.1}/tests/test_quantile_comparison.py +0 -0
  47. {microimpute-3.2.0 → microimpute-3.2.1}/tests/test_smoke_qrf.py +0 -0
  48. {microimpute-3.2.0 → microimpute-3.2.1}/tests/test_type_handling.py +0 -0
  49. {microimpute-3.2.0 → microimpute-3.2.1}/tests/test_visualizations.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microimpute
3
- Version: 3.2.0
3
+ Version: 3.2.1
4
4
  Summary: Benchmarking imputation methods for microdata
5
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
6
  Project-URL: Repository, https://github.com/PolicyEngine/microimpute
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microimpute
3
- Version: 3.2.0
3
+ Version: 3.2.1
4
4
  Summary: Benchmarking imputation methods for microdata
5
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
6
  Project-URL: Repository, https://github.com/PolicyEngine/microimpute
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
 
8
8
  [project]
9
9
  name = "microimpute"
10
- version = "3.2.0"
10
+ version = "3.2.1"
11
11
  description = "Benchmarking imputation methods for microdata"
12
12
  readme = "README.md"
13
13
  authors = [
@@ -15,12 +15,26 @@ the invariants the published comparison relies on:
15
15
  - The logistic-participation variants follow their stated rules.
16
16
  - Census's MOOP and Medicare premium imputations both count as allocated.
17
17
  - Extracted CSVs that differ from the pinned zip are extracted again.
18
- - The fold cache misses whenever the environment, microimpute's source
19
- or the model inputs change.
18
+ - The fold cache misses whenever the environment, microimpute's or
19
+ Microcosm's source or the model inputs change.
20
+ - ``installed_source`` raises, rather than reading a package as absent,
21
+ when its metadata names no version, and ``environment`` raises when a
22
+ Microcosm package imports without distribution metadata.
23
+ - Microcosm's draws are zero or lie within the range of the spenders
24
+ that carry weight, and they do not depend on the number of threads.
25
+ - Microcosm fits its gate on every donor's row: the donor's predictors
26
+ and whether it spends, with its unit weight in the weighted fit and no
27
+ weights in the unweighted fit.
28
+
29
+ The tests of Microcosm's draws, threads and gate skip on a machine
30
+ without Microcosm, which installs from git.
20
31
  """
21
32
 
22
33
  import argparse
34
+ import dataclasses
35
+ import importlib
23
36
  import os
37
+ import re
24
38
  import zipfile
25
39
 
26
40
  import numpy as np
@@ -48,6 +62,8 @@ from experiments.moop_spm.methods import (
48
62
  assign_cells,
49
63
  fit_participation,
50
64
  logit_family,
65
+ microcosm_qrf,
66
+ microcosm_threads,
51
67
  nearest_donors,
52
68
  participation_probability,
53
69
  )
@@ -57,6 +73,8 @@ from experiments.moop_spm.run import (
57
73
  collect_folds,
58
74
  config_hash,
59
75
  frame_sha256,
76
+ installed_package_sha256,
77
+ installed_source,
60
78
  )
61
79
 
62
80
  SETTINGS = settings(
@@ -280,6 +298,7 @@ def _fold(seed: int) -> FoldData:
280
298
  return FoldData(
281
299
  X_donor=X_donor,
282
300
  moop_donor=moop,
301
+ weight_donor=rng.uniform(100, 3_000, n_donor),
283
302
  cells_donor=cells_donor,
284
303
  group_donor=cells_donor["group"].to_numpy(),
285
304
  X_recipient=features(n_recipient),
@@ -315,6 +334,156 @@ def test_logit_family_rules(seed):
315
334
  assert np.all((drawn == 0) | np.isclose(drawn, means[i]))
316
335
 
317
336
 
337
+ @settings(max_examples=3, deadline=None)
338
+ @given(seed=st.integers(0, 2**31), cap_quantile=st.floats(0.3, 0.9))
339
+ def test_microcosm_draws_stay_within_weighted_spenders(seed, cap_quantile):
340
+ pytest.importorskip("microcosm.fit")
341
+ fold = _fold(seed)
342
+ n_recipients = len(fold.X_recipient)
343
+ spent = fold.moop_donor[fold.moop_donor > 0]
344
+
345
+ unweighted = microcosm_qrf(fold, 3, seed=seed, weighted=False, n_jobs=1)
346
+ assert unweighted.shape == (3, n_recipients)
347
+ assert np.isfinite(unweighted).all()
348
+ # A draw is zero or lies between the smallest and largest spender.
349
+ drawn = unweighted[unweighted != 0]
350
+ assert (drawn >= spent.min()).all()
351
+ assert (drawn <= spent.max()).all()
352
+ assert not np.array_equal(unweighted[0], unweighted[1])
353
+ # The thread count changes no draw.
354
+ again = microcosm_qrf(fold, 3, seed=seed, weighted=False, n_jobs=2)
355
+ assert np.array_equal(again, unweighted)
356
+
357
+ # Donors without weight shape no draw: with zero weight on every
358
+ # spender above the cap, a weighted draw is zero or lies between the
359
+ # smallest and largest spender that keeps its weight.
360
+ cap = float(np.quantile(spent, cap_quantile))
361
+ capped = dataclasses.replace(
362
+ fold,
363
+ weight_donor=np.where(fold.moop_donor > cap, 0.0, fold.weight_donor),
364
+ )
365
+ kept = capped.moop_donor[(capped.moop_donor > 0) & (capped.weight_donor > 0)]
366
+ weighted = microcosm_qrf(capped, 3, seed=seed, weighted=True, n_jobs=1)
367
+ assert weighted.shape == (3, n_recipients)
368
+ assert np.isfinite(weighted).all()
369
+ drawn = weighted[weighted != 0]
370
+ assert (drawn >= kept.min()).all()
371
+ assert (drawn <= kept.max()).all()
372
+ assert weighted.max() <= cap
373
+ # The thread count changes no weighted draw either.
374
+ again = microcosm_qrf(capped, 3, seed=seed, weighted=True, n_jobs=2)
375
+ assert np.array_equal(again, weighted)
376
+
377
+
378
+ def test_microcosm_threads_sets_and_restores_widths(monkeypatch):
379
+ qrf_module = pytest.importorskip("microcosm.fit.qrf")
380
+ fit_name, draw_name = qrf_module._N_JOBS_ENV, qrf_module._PREDICT_WORKERS_ENV
381
+ monkeypatch.delenv(fit_name, raising=False)
382
+ monkeypatch.setenv(draw_name, "7")
383
+ with microcosm_threads(3):
384
+ # Microcosm's own resolvers read the cap.
385
+ assert qrf_module._fit_n_jobs() == 3
386
+ assert qrf_module._predict_workers() == 3
387
+ assert fit_name not in os.environ
388
+ assert os.environ[draw_name] == "7"
389
+
390
+
391
+ def test_microcosm_gate_takes_donor_weights_in_order(monkeypatch):
392
+ pytest.importorskip("microcosm.fit")
393
+ from sklearn.ensemble import HistGradientBoostingClassifier
394
+
395
+ recorded = []
396
+ original = HistGradientBoostingClassifier.fit
397
+
398
+ def spy(self, X, y, sample_weight=None, **kwargs):
399
+ weights = sample_weight
400
+ if weights is not None:
401
+ weights = np.array(weights, dtype=float)
402
+ recorded.append((np.array(X, dtype=float), np.array(y), weights))
403
+ return original(self, X, y, sample_weight=sample_weight, **kwargs)
404
+
405
+ monkeypatch.setattr(HistGradientBoostingClassifier, "fit", spy)
406
+ fold = _fold(7)
407
+ donors = fold.X_donor.to_numpy(dtype=float)
408
+ spends = (fold.moop_donor > 0).astype(int)
409
+ microcosm_qrf(fold, 1, seed=7, weighted=True, n_jobs=1)
410
+ # Microcosm fits one in-sample gate on every donor's row: the donor's
411
+ # predictors and whether it spends, with that donor's weight.
412
+ assert len(recorded) == 1
413
+ features, labels, weights = recorded[0]
414
+ assert np.array_equal(features, donors)
415
+ assert np.array_equal(labels, spends)
416
+ assert weights is not None
417
+ assert weights.shape == fold.weight_donor.shape
418
+ assert np.allclose(
419
+ weights / weights.sum(), fold.weight_donor / fold.weight_donor.sum()
420
+ )
421
+ # A reversed donor order would fail the same check.
422
+ assert not np.allclose(
423
+ weights / weights.sum(),
424
+ fold.weight_donor[::-1] / fold.weight_donor.sum(),
425
+ )
426
+
427
+ recorded.clear()
428
+ microcosm_qrf(fold, 1, seed=7, weighted=False, n_jobs=1)
429
+ assert len(recorded) == 1
430
+ features, labels, weights = recorded[0]
431
+ assert np.array_equal(features, donors)
432
+ assert np.array_equal(labels, spends)
433
+ assert weights is None
434
+
435
+
436
+ def test_installed_source_and_hash_tolerate_only_absent_packages(tmp_path, monkeypatch):
437
+ assert installed_source("no-such-distribution-moop") is None
438
+ assert installed_package_sha256("no_such_package_moop.sub") is None
439
+ # A package that is present but fails to import still raises.
440
+ broken = tmp_path / "broken_package_moop"
441
+ broken.mkdir()
442
+ (broken / "__init__.py").write_text("import no_such_dependency_moop\n")
443
+ # So does a distribution whose metadata names no version.
444
+ versionless = tmp_path / "versionless_moop-0.0.dist-info"
445
+ versionless.mkdir()
446
+ (versionless / "METADATA").write_text(
447
+ "Metadata-Version: 2.1\nName: versionless-moop\n"
448
+ )
449
+ monkeypatch.syspath_prepend(str(tmp_path))
450
+ importlib.invalidate_caches()
451
+ with pytest.raises(ModuleNotFoundError):
452
+ installed_package_sha256("broken_package_moop")
453
+ with pytest.raises(RuntimeError, match="names no version"):
454
+ installed_source("versionless-moop")
455
+ source = installed_source("microcosm-fit")
456
+ if source is not None:
457
+ # A git install records its commit.
458
+ assert re.fullmatch(r"\S+ @ ([0-9a-f]{40}|\S+)", source)
459
+
460
+
461
+ def test_environment_refuses_microcosm_imports_without_metadata(tmp_path, monkeypatch):
462
+ # Two packages import; only the first has distribution metadata.
463
+ for name in ("recorded_moop", "unrecorded_moop"):
464
+ (tmp_path / name).mkdir()
465
+ (tmp_path / name / "__init__.py").write_text("")
466
+ recorded = tmp_path / "recorded_moop-1.0.dist-info"
467
+ recorded.mkdir()
468
+ (recorded / "METADATA").write_text(
469
+ "Metadata-Version: 2.1\nName: recorded-moop\nVersion: 1.0\n"
470
+ )
471
+ monkeypatch.syspath_prepend(str(tmp_path))
472
+ importlib.invalidate_caches()
473
+ present = {
474
+ "recorded": ("recorded-moop", "recorded_moop"),
475
+ "absent": ("no-such-distribution-moop", "no_such_package_moop"),
476
+ }
477
+ monkeypatch.setattr(run_module, "MICROCOSM_PACKAGES", present)
478
+ found = run_module.environment()
479
+ assert found["recorded"] == "1.0"
480
+ assert found["absent"] is None
481
+ unrecorded = {"unrecorded": ("unrecorded-moop", "unrecorded_moop")}
482
+ monkeypatch.setattr(run_module, "MICROCOSM_PACKAGES", unrecorded)
483
+ with pytest.raises(RuntimeError, match="unrecorded_moop imports"):
484
+ run_module.environment()
485
+
486
+
318
487
  @settings(max_examples=10, deadline=None)
319
488
  @given(seed=st.integers(0, 2**31))
320
489
  def test_evaluate_invariants(seed):
@@ -426,7 +595,7 @@ def _stub_fit(value: float, folds: np.ndarray, calls: list):
426
595
  return fit
427
596
 
428
597
 
429
- def test_cache_misses_when_environment_or_microimpute_changes(tmp_path, monkeypatch):
598
+ def test_cache_misses_when_environment_or_model_source_changes(tmp_path, monkeypatch):
430
599
  args, units, X, cells, folds = _cache_inputs()
431
600
  base_env = run_module.environment()
432
601
  monkeypatch.setattr(run_module, "environment", lambda: base_env)
@@ -454,7 +623,21 @@ def test_cache_misses_when_environment_or_microimpute_changes(tmp_path, monkeypa
454
623
  assert calls == [0, 1, 2]
455
624
  assert (fresh["qrf"] == 99.0).all()
456
625
 
626
+ # Microcosm from another commit refits.
627
+ moved = {**base_env, "microcosm_fit": "0.1.0 @ another-commit"}
628
+ monkeypatch.setattr(run_module, "environment", lambda: moved)
629
+ assert config_hash(build_config(args, units, X, cells, folds)) != tag
630
+
457
631
  monkeypatch.setattr(run_module, "environment", lambda: base_env)
632
+ assert config_hash(build_config(args, units, X, cells, folds)) == tag
633
+ # Edited Microcosm source refits, and so does edited microimpute source.
634
+ original = run_module.installed_package_sha256
635
+ monkeypatch.setattr(
636
+ run_module, "installed_package_sha256", lambda package: "edited"
637
+ )
638
+ assert config_hash(build_config(args, units, X, cells, folds)) != tag
639
+ monkeypatch.setattr(run_module, "installed_package_sha256", original)
640
+ assert config_hash(build_config(args, units, X, cells, folds)) == tag
458
641
  monkeypatch.setattr(run_module, "package_sha256", lambda package: "edited")
459
642
  assert config_hash(build_config(args, units, X, cells, folds)) != tag
460
643
 
File without changes
File without changes