3535The wrapper is generic over the base imputer — ``QRF`` is the obvious
3636default, but ``MDN``, ``OLS``, or ``Matching`` all compose the same way.
3737
38- Regime detection is parameterized by ``min_class_count`` and
39- ``min_class_fraction``: a class with fewer observations than both
40- thresholds collapses into the closest adjacent regime. This avoids
41- fitting a full three-sign split on a variable whose negative tail is
42- five outlier rows — the cost-benefit flips toward the simpler
43- architecture.
38+ Regime detection is based only on observed support. If the training data
39+ contains negative, zero, and positive values, the imputer uses the
40+ three-sign architecture. Callers do not provide sign/regime metadata.
4441"""
4542
4643from __future__ import annotations
4744
48- import logging
49- from typing import Any , Dict , List , Optional , Tuple , Type , Union
45+ from typing import Any , Dict , List , Optional , Type , Union
5046
5147import numpy as np
5248import pandas as pd
53- from pydantic import SkipValidation , validate_call
49+ from pydantic import validate_call
5450
5551from microimpute .config import RANDOM_STATE , VALIDATE_CONFIG
5652from microimpute .models .imputer import (
5753 Imputer ,
5854 ImputerResults ,
59- _ConstantValueModel ,
6055)
6156from microimpute .models .qrf import QRF
6257
@@ -95,19 +90,14 @@ def _make_classifier(kind: str, seed: int):
9590def _detect_regime (
9691 y : np .ndarray ,
9792 * ,
98- min_class_count : int ,
99- min_class_fraction : float ,
10093 zero_atol : float ,
10194) -> str :
10295 """Classify the training distribution into one of seven regimes.
10396
104- A class (neg/zero/pos) counts as present iff its count is at least
105- ``min_class_count`` AND its fraction of total rows is at least
106- ``min_class_fraction``. Below both thresholds, the class collapses
107- into its closest adjacent regime (minority negatives merge into
108- zero → ZI_POSITIVE; minority zeros merge into the majority sign;
109- etc.). This keeps the gate architecture stable in the presence of
110- measurement-error outliers.
97+ A class (neg/zero/pos) counts as present when it appears at least
98+ once in the training data. Sign support is inferred from donor data;
99+ callers cannot force a variable to be positive-only, negative-only,
100+ or signed.
111101 """
112102 n = len (y )
113103 if n == 0 :
@@ -121,22 +111,12 @@ def _detect_regime(
121111 n_pos = int (is_pos .sum ())
122112 n_neg = int (is_neg .sum ())
123113
124- # Apply both thresholds.
125- def _meaningful (count : int ) -> bool :
126- return count >= min_class_count and (count / n ) >= min_class_fraction
127-
128- has_zero = _meaningful (n_zero )
129- has_pos = _meaningful (n_pos )
130- has_neg = _meaningful (n_neg )
114+ has_zero = n_zero > 0
115+ has_pos = n_pos > 0
116+ has_neg = n_neg > 0
131117
132118 if not (has_zero or has_pos or has_neg ):
133- # All three classes are below threshold. Pick the one with the
134- # largest raw count as a degenerate fallback.
135- counts = {"zero" : n_zero , "pos" : n_pos , "neg" : n_neg }
136- majority = max (counts , key = counts .get )
137- if majority == "zero" :
138- return REGIME_DEGENERATE_ZERO
139- return REGIME_POSITIVE_ONLY if majority == "pos" else REGIME_NEGATIVE_ONLY
119+ return REGIME_DEGENERATE_ZERO
140120
141121 if has_pos and has_neg and has_zero :
142122 return REGIME_THREE_SIGN
@@ -161,11 +141,6 @@ class ZeroInflatedImputer(Imputer):
161141 regression step. Defaults to ``QRF``.
162142 base_imputer_kwargs: Keyword arguments forwarded to the base
163143 imputer constructor. ``{}`` by default.
164- min_class_count: Minimum raw count per class (neg/0/pos) for
165- that class to be considered present. Below this, the class
166- collapses into an adjacent regime. Defaults to 10.
167- min_class_fraction: Minimum fraction of total rows per class
168- for that class to be considered present. Defaults to 0.01.
169144 zero_atol: Absolute tolerance for "equals zero" in the regime
170145 detector. Defaults to 1e-6, matching the upstream
171146 ``_MultiSourceBase`` convention.
@@ -179,8 +154,6 @@ def __init__(
179154 self ,
180155 base_imputer_class : Optional [Type [Imputer ]] = None ,
181156 base_imputer_kwargs : Optional [Dict [str , Any ]] = None ,
182- min_class_count : int = 10 ,
183- min_class_fraction : float = 0.01 ,
184157 zero_atol : float = 1e-6 ,
185158 classifier_type : str = "hist_gb" ,
186159 seed : Optional [int ] = RANDOM_STATE ,
@@ -189,8 +162,6 @@ def __init__(
189162 super ().__init__ (seed = seed , log_level = log_level )
190163 self .base_imputer_class = base_imputer_class or QRF
191164 self .base_imputer_kwargs = dict (base_imputer_kwargs or {})
192- self .min_class_count = int (min_class_count )
193- self .min_class_fraction = float (min_class_fraction )
194165 self .zero_atol = float (zero_atol )
195166 self .classifier_type = classifier_type
196167
@@ -267,8 +238,6 @@ def fit(
267238 y = X_train [var ].to_numpy (dtype = float , copy = False )
268239 regime = _detect_regime (
269240 y ,
270- min_class_count = self .min_class_count ,
271- min_class_fraction = self .min_class_fraction ,
272241 zero_atol = self .zero_atol ,
273242 )
274243 self ._regimes [var ] = regime
0 commit comments