mixed-imputer 0.1.0__py3-none-any.whl
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.
Potentially problematic release.
This version of mixed-imputer might be problematic. Click here for more details.
- mixed_imputer/__init__.py +4 -0
- mixed_imputer/_imputer.py +740 -0
- mixed_imputer/_version.py +1 -0
- mixed_imputer-0.1.0.dist-info/METADATA +152 -0
- mixed_imputer-0.1.0.dist-info/RECORD +8 -0
- mixed_imputer-0.1.0.dist-info/WHEEL +5 -0
- mixed_imputer-0.1.0.dist-info/licenses/LICENSE +21 -0
- mixed_imputer-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mixed-type MICE imputer for scikit-learn.
|
|
3
|
+
|
|
4
|
+
Provides a single ``MixedTypeImputer`` transformer that handles DataFrames
|
|
5
|
+
containing both numerical and categorical (string) columns. Internally it
|
|
6
|
+
encodes categoricals with ``OrdinalEncoder``, runs an iterative MICE-style
|
|
7
|
+
imputer that automatically chooses a regressor or classifier per column, and
|
|
8
|
+
decodes categoricals back to their original string values.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import warnings
|
|
12
|
+
import numpy as np
|
|
13
|
+
import pandas as pd
|
|
14
|
+
from scipy import stats
|
|
15
|
+
|
|
16
|
+
# Unlock experimental feature (must be imported BEFORE IterativeImputer)
|
|
17
|
+
from sklearn.experimental import enable_iterative_imputer # noqa
|
|
18
|
+
|
|
19
|
+
from sklearn.base import BaseEstimator, TransformerMixin, clone
|
|
20
|
+
from sklearn.preprocessing import OrdinalEncoder
|
|
21
|
+
from sklearn.impute import IterativeImputer
|
|
22
|
+
from sklearn.utils import _safe_indexing
|
|
23
|
+
from sklearn.utils.validation import check_is_fitted, check_array
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ----------------------------------------------------------------------
|
|
27
|
+
# _safe_assign was removed from sklearn.utils in recent versions;
|
|
28
|
+
# implement a minimal local replacement.
|
|
29
|
+
# ----------------------------------------------------------------------
|
|
30
|
+
def _safe_assign(X, values, *, row_indexer=None, column_indexer=None):
|
|
31
|
+
"""Assign values into X with numpy-compatible row/column indexers."""
|
|
32
|
+
if row_indexer is None:
|
|
33
|
+
row_indexer = slice(None)
|
|
34
|
+
if column_indexer is None:
|
|
35
|
+
column_indexer = slice(None)
|
|
36
|
+
X[row_indexer, column_indexer] = values
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ----------------------------------------------------------------------
|
|
40
|
+
# 1. Internal mixed-type iterative imputer
|
|
41
|
+
# ----------------------------------------------------------------------
|
|
42
|
+
class _MixedTypeIterativeImputer(IterativeImputer):
|
|
43
|
+
"""
|
|
44
|
+
IterativeImputer that uses a ``_ColumnTypeEstimator`` to switch between
|
|
45
|
+
a regressor and a classifier depending on the target column type.
|
|
46
|
+
|
|
47
|
+
This is a drop-in for scikit-learn's ``IterativeImputer`` that overrides
|
|
48
|
+
``_impute_one_feature`` to:
|
|
49
|
+
|
|
50
|
+
- Use ``predict_proba`` for categorical target columns when posterior
|
|
51
|
+
sampling is enabled.
|
|
52
|
+
- Round categorical predictions back to integers.
|
|
53
|
+
- Clip numeric predictions to the observed min/max range.
|
|
54
|
+
- Support ``return_std`` from the regressor for posterior sampling on
|
|
55
|
+
numeric columns.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def _impute_one_feature(
|
|
59
|
+
self,
|
|
60
|
+
X_filled,
|
|
61
|
+
mask_missing_values,
|
|
62
|
+
target_feature_index,
|
|
63
|
+
predictor_feature_indices,
|
|
64
|
+
estimator=None,
|
|
65
|
+
fit_mode=True,
|
|
66
|
+
params=None,
|
|
67
|
+
):
|
|
68
|
+
if estimator is None and fit_mode is False:
|
|
69
|
+
raise ValueError(
|
|
70
|
+
"If fit_mode is False, an already-fitted estimator must be provided."
|
|
71
|
+
)
|
|
72
|
+
if estimator is None:
|
|
73
|
+
estimator = clone(self._estimator)
|
|
74
|
+
|
|
75
|
+
missing_row_mask = mask_missing_values[:, target_feature_index]
|
|
76
|
+
|
|
77
|
+
if fit_mode:
|
|
78
|
+
X_train = _safe_indexing(
|
|
79
|
+
_safe_indexing(X_filled, predictor_feature_indices, axis=1),
|
|
80
|
+
~missing_row_mask,
|
|
81
|
+
axis=0,
|
|
82
|
+
)
|
|
83
|
+
y_train = _safe_indexing(
|
|
84
|
+
_safe_indexing(X_filled, target_feature_index, axis=1),
|
|
85
|
+
~missing_row_mask,
|
|
86
|
+
axis=0,
|
|
87
|
+
)
|
|
88
|
+
estimator.set_params(
|
|
89
|
+
target_feature_index=target_feature_index,
|
|
90
|
+
predictor_feature_indices=predictor_feature_indices,
|
|
91
|
+
)
|
|
92
|
+
if params is None:
|
|
93
|
+
estimator.fit(X_train, y_train)
|
|
94
|
+
else:
|
|
95
|
+
estimator.fit(X_train, y_train, **params)
|
|
96
|
+
|
|
97
|
+
if np.sum(missing_row_mask) == 0:
|
|
98
|
+
return X_filled, estimator
|
|
99
|
+
|
|
100
|
+
X_test = _safe_indexing(
|
|
101
|
+
_safe_indexing(X_filled, predictor_feature_indices, axis=1),
|
|
102
|
+
missing_row_mask,
|
|
103
|
+
axis=0,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
target_is_categorical = estimator.is_categorical(target_feature_index)
|
|
107
|
+
|
|
108
|
+
if target_is_categorical:
|
|
109
|
+
if self.sample_posterior and hasattr(estimator.model_, "predict_proba"):
|
|
110
|
+
proba = estimator.model_.predict_proba(X_test)
|
|
111
|
+
sampled_indices = _sample_from_probabilities(
|
|
112
|
+
proba, random_state=self.random_state_
|
|
113
|
+
)
|
|
114
|
+
# sampled_indices are column indices into predict_proba
|
|
115
|
+
# (0 .. n_classes-1). Map them back to the actual class
|
|
116
|
+
# labels via model_.classes_ so the OrdinalEncoder can
|
|
117
|
+
# decode them correctly.
|
|
118
|
+
imputed_values = estimator.model_.classes_[sampled_indices]
|
|
119
|
+
else:
|
|
120
|
+
imputed_values = estimator.predict(X_test)
|
|
121
|
+
|
|
122
|
+
# Ensure integer output (same dtype as original)
|
|
123
|
+
non_missing_vals = X_filled[~missing_row_mask, target_feature_index]
|
|
124
|
+
target_dtype = non_missing_vals.dtype
|
|
125
|
+
imputed_values = np.round(imputed_values).astype(target_dtype)
|
|
126
|
+
|
|
127
|
+
else:
|
|
128
|
+
if self.sample_posterior:
|
|
129
|
+
mus, sigmas = estimator.predict(X_test, return_std=True)
|
|
130
|
+
imputed_values = np.zeros(mus.shape, dtype=X_filled.dtype)
|
|
131
|
+
positive_sigmas = sigmas > 0
|
|
132
|
+
imputed_values[~positive_sigmas] = mus[~positive_sigmas]
|
|
133
|
+
mus_too_low = mus < self._min_value[target_feature_index]
|
|
134
|
+
imputed_values[mus_too_low] = self._min_value[target_feature_index]
|
|
135
|
+
mus_too_high = mus > self._max_value[target_feature_index]
|
|
136
|
+
imputed_values[mus_too_high] = self._max_value[target_feature_index]
|
|
137
|
+
inrange_mask = positive_sigmas & ~mus_too_low & ~mus_too_high
|
|
138
|
+
mus = mus[inrange_mask]
|
|
139
|
+
sigmas = sigmas[inrange_mask]
|
|
140
|
+
a = (self._min_value[target_feature_index] - mus) / sigmas
|
|
141
|
+
b = (self._max_value[target_feature_index] - mus) / sigmas
|
|
142
|
+
truncated_normal = stats.truncnorm(a=a, b=b, loc=mus, scale=sigmas)
|
|
143
|
+
imputed_values[inrange_mask] = truncated_normal.rvs(
|
|
144
|
+
random_state=self.random_state_
|
|
145
|
+
)
|
|
146
|
+
else:
|
|
147
|
+
imputed_values = estimator.predict(X_test)
|
|
148
|
+
imputed_values = np.clip(
|
|
149
|
+
imputed_values,
|
|
150
|
+
self._min_value[target_feature_index],
|
|
151
|
+
self._max_value[target_feature_index],
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
_safe_assign(
|
|
155
|
+
X_filled,
|
|
156
|
+
imputed_values,
|
|
157
|
+
row_indexer=missing_row_mask,
|
|
158
|
+
column_indexer=target_feature_index,
|
|
159
|
+
)
|
|
160
|
+
return X_filled, estimator
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ----------------------------------------------------------------------
|
|
164
|
+
# 2. Estimator that switches between regressor / classifier
|
|
165
|
+
# ----------------------------------------------------------------------
|
|
166
|
+
class _ColumnTypeEstimator(BaseEstimator):
|
|
167
|
+
"""
|
|
168
|
+
Meta-estimator that delegates to either a regressor or a classifier
|
|
169
|
+
depending on whether the target column is categorical.
|
|
170
|
+
|
|
171
|
+
Parameters
|
|
172
|
+
----------
|
|
173
|
+
regressor : estimator
|
|
174
|
+
Regressor used for numerical target columns.
|
|
175
|
+
classifier : estimator
|
|
176
|
+
Classifier used for categorical target columns.
|
|
177
|
+
categorical_indices : list of int
|
|
178
|
+
Column indices in the concatenated array that are treated as categorical.
|
|
179
|
+
target_feature_index : int or None
|
|
180
|
+
Index of the column currently being imputed (set at fit time).
|
|
181
|
+
predictor_feature_indices : list of int or None
|
|
182
|
+
Indices of columns used as predictors (set at fit time).
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
def __init__(
|
|
186
|
+
self,
|
|
187
|
+
regressor,
|
|
188
|
+
classifier,
|
|
189
|
+
categorical_indices,
|
|
190
|
+
target_feature_index=None,
|
|
191
|
+
predictor_feature_indices=None,
|
|
192
|
+
**params,
|
|
193
|
+
):
|
|
194
|
+
self.regressor = regressor
|
|
195
|
+
self.classifier = classifier
|
|
196
|
+
self.categorical_indices = categorical_indices
|
|
197
|
+
self.target_feature_index = target_feature_index
|
|
198
|
+
self.predictor_feature_indices = predictor_feature_indices
|
|
199
|
+
self.model_ = None
|
|
200
|
+
|
|
201
|
+
def is_categorical(self, feature_index):
|
|
202
|
+
"""Return True if the given feature index is categorical."""
|
|
203
|
+
return feature_index in self.categorical_indices
|
|
204
|
+
|
|
205
|
+
def fit(self, X, y):
|
|
206
|
+
"""Fit the appropriate model (regressor or classifier) on the data.
|
|
207
|
+
|
|
208
|
+
Parameters
|
|
209
|
+
----------
|
|
210
|
+
X : ndarray of shape (n_samples, n_features)
|
|
211
|
+
Training features.
|
|
212
|
+
y : ndarray of shape (n_samples,)
|
|
213
|
+
Target values.
|
|
214
|
+
"""
|
|
215
|
+
X = np.asarray(X)
|
|
216
|
+
if self.is_categorical(self.target_feature_index):
|
|
217
|
+
self.model_ = clone(self.classifier)
|
|
218
|
+
else:
|
|
219
|
+
self.model_ = clone(self.regressor)
|
|
220
|
+
self.model_.fit(X, y)
|
|
221
|
+
return self
|
|
222
|
+
|
|
223
|
+
def predict(self, X, **kwargs):
|
|
224
|
+
"""Predict using the fitted model.
|
|
225
|
+
|
|
226
|
+
Parameters
|
|
227
|
+
----------
|
|
228
|
+
X : ndarray of shape (n_samples, n_features)
|
|
229
|
+
Features.
|
|
230
|
+
**kwargs : dict
|
|
231
|
+
Additional keyword arguments forwarded to the underlying model's
|
|
232
|
+
``predict`` method. ``return_std=True`` is handled specially for
|
|
233
|
+
models that do not natively support it.
|
|
234
|
+
|
|
235
|
+
Returns
|
|
236
|
+
-------
|
|
237
|
+
y_pred : ndarray of shape (n_samples,)
|
|
238
|
+
Predicted values. If ``return_std=True``, returns a tuple
|
|
239
|
+
``(y_pred, std)``.
|
|
240
|
+
"""
|
|
241
|
+
check_is_fitted(self, "model_")
|
|
242
|
+
# If return_std is requested but the model doesn't support it,
|
|
243
|
+
# fall back to a plain prediction with unit standard deviation.
|
|
244
|
+
return_std = kwargs.pop("return_std", False)
|
|
245
|
+
try:
|
|
246
|
+
if return_std:
|
|
247
|
+
preds, std = self.model_.predict(X, return_std=True)
|
|
248
|
+
return preds, std
|
|
249
|
+
else:
|
|
250
|
+
return self.model_.predict(X, **kwargs)
|
|
251
|
+
except TypeError:
|
|
252
|
+
if return_std:
|
|
253
|
+
preds = self.model_.predict(X, **kwargs)
|
|
254
|
+
return preds, np.ones_like(preds)
|
|
255
|
+
raise
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _sample_from_probabilities(proba, random_state=None):
|
|
259
|
+
"""Draw a class for each row from the predicted probability matrix."""
|
|
260
|
+
rng = np.random.default_rng(random_state)
|
|
261
|
+
n_samples = proba.shape[0]
|
|
262
|
+
cumulative = proba.cumsum(axis=1)
|
|
263
|
+
random_values = rng.random(n_samples).reshape(-1, 1)
|
|
264
|
+
sampled = (random_values < cumulative).argmax(axis=1)
|
|
265
|
+
return sampled
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# ----------------------------------------------------------------------
|
|
269
|
+
# 3. Public, user-friendly wrapper
|
|
270
|
+
# ----------------------------------------------------------------------
|
|
271
|
+
class MixedTypeImputer(BaseEstimator, TransformerMixin):
|
|
272
|
+
"""
|
|
273
|
+
Imputer for DataFrames containing both numerical and categorical (string)
|
|
274
|
+
columns.
|
|
275
|
+
|
|
276
|
+
Internally encodes categoricals with OrdinalEncoder, runs an iterative
|
|
277
|
+
MICE-style imputer that automatically chooses a regressor or classifier
|
|
278
|
+
per column, and decodes categoricals back to their original strings.
|
|
279
|
+
|
|
280
|
+
Parameters
|
|
281
|
+
----------
|
|
282
|
+
categorical_features : list of int or str, or None
|
|
283
|
+
Column indices or names (if DataFrame) that are categorical. If None
|
|
284
|
+
and the input is a DataFrame, columns with object, string, or category
|
|
285
|
+
dtype are automatically treated as categorical.
|
|
286
|
+
|
|
287
|
+
numeric_features : list of int or str, or None
|
|
288
|
+
Numeric columns. If None, all remaining columns (or float/int types)
|
|
289
|
+
are treated as numeric. Only used as a check.
|
|
290
|
+
|
|
291
|
+
regressor : estimator, default=HistGradientBoostingRegressor()
|
|
292
|
+
Regressor used for numerical target columns.
|
|
293
|
+
|
|
294
|
+
classifier : estimator, default=HistGradientBoostingClassifier()
|
|
295
|
+
Classifier used for categorical target columns.
|
|
296
|
+
|
|
297
|
+
max_iter : int, default=10
|
|
298
|
+
Maximum number of imputation rounds.
|
|
299
|
+
|
|
300
|
+
tol : float, default=1e-3
|
|
301
|
+
Tolerance of the stopping condition.
|
|
302
|
+
|
|
303
|
+
initial_strategy : str, default='mean'
|
|
304
|
+
Initial imputation strategy passed to IterativeImputer.
|
|
305
|
+
|
|
306
|
+
sample_posterior : bool, default=False
|
|
307
|
+
Whether to sample from the predictive posterior for multiple imputation.
|
|
308
|
+
Categorical sampling uses predicted class probabilities.
|
|
309
|
+
|
|
310
|
+
random_state : int, RandomState instance or None, default=None
|
|
311
|
+
Seed for reproducibility.
|
|
312
|
+
|
|
313
|
+
verbose : int, default=0
|
|
314
|
+
Verbosity of the underlying IterativeImputer.
|
|
315
|
+
|
|
316
|
+
add_indicator : bool, default=False
|
|
317
|
+
If True, add missing indicators. (They will appear in the output
|
|
318
|
+
DataFrame as boolean columns.)
|
|
319
|
+
|
|
320
|
+
keep_empty_features : bool, default=False
|
|
321
|
+
Keep features that are all missing at fit time.
|
|
322
|
+
|
|
323
|
+
Attributes
|
|
324
|
+
----------
|
|
325
|
+
imputation_model_ : _MixedTypeIterativeImputer
|
|
326
|
+
The fitted internal imputer.
|
|
327
|
+
|
|
328
|
+
encoder_ : OrdinalEncoder
|
|
329
|
+
Fitted encoder for categorical columns.
|
|
330
|
+
|
|
331
|
+
categorical_features_ : list of int
|
|
332
|
+
Column indices in the encoded array that are categorical.
|
|
333
|
+
|
|
334
|
+
output_feature_order_ : list of str
|
|
335
|
+
Final column order of the output DataFrame.
|
|
336
|
+
"""
|
|
337
|
+
|
|
338
|
+
def __init__(
|
|
339
|
+
self,
|
|
340
|
+
categorical_features=None,
|
|
341
|
+
numeric_features=None,
|
|
342
|
+
regressor=None,
|
|
343
|
+
classifier=None,
|
|
344
|
+
max_iter=10,
|
|
345
|
+
tol=1e-3,
|
|
346
|
+
initial_strategy="mean",
|
|
347
|
+
sample_posterior=False,
|
|
348
|
+
random_state=None,
|
|
349
|
+
verbose=0,
|
|
350
|
+
add_indicator=False,
|
|
351
|
+
keep_empty_features=False,
|
|
352
|
+
):
|
|
353
|
+
self.categorical_features = categorical_features
|
|
354
|
+
self.numeric_features = numeric_features
|
|
355
|
+
self.regressor = regressor
|
|
356
|
+
self.classifier = classifier
|
|
357
|
+
self.max_iter = max_iter
|
|
358
|
+
self.tol = tol
|
|
359
|
+
self.initial_strategy = initial_strategy
|
|
360
|
+
self.sample_posterior = sample_posterior
|
|
361
|
+
self.random_state = random_state
|
|
362
|
+
self.verbose = verbose
|
|
363
|
+
self.add_indicator = add_indicator
|
|
364
|
+
self.keep_empty_features = keep_empty_features
|
|
365
|
+
|
|
366
|
+
def _validate_input(self, X, reset=True):
|
|
367
|
+
"""Validate input and store feature metadata.
|
|
368
|
+
|
|
369
|
+
Parameters
|
|
370
|
+
----------
|
|
371
|
+
X : DataFrame or ndarray
|
|
372
|
+
The input data.
|
|
373
|
+
reset : bool
|
|
374
|
+
If True, recompute categorical/numeric feature lists from scratch.
|
|
375
|
+
Set to False during ``transform`` to reuse the lists from ``fit``.
|
|
376
|
+
|
|
377
|
+
Returns
|
|
378
|
+
-------
|
|
379
|
+
X : DataFrame or ndarray
|
|
380
|
+
The (possibly validated) input.
|
|
381
|
+
"""
|
|
382
|
+
if isinstance(X, pd.DataFrame):
|
|
383
|
+
self._input_is_df = True
|
|
384
|
+
self._input_columns = X.columns.tolist()
|
|
385
|
+
self._input_index = X.index
|
|
386
|
+
if self.categorical_features is None:
|
|
387
|
+
# auto-detect object, string, and category columns
|
|
388
|
+
cats = []
|
|
389
|
+
for col, dtype in X.dtypes.items():
|
|
390
|
+
if (pd.api.types.is_object_dtype(dtype)
|
|
391
|
+
or pd.api.types.is_string_dtype(dtype)
|
|
392
|
+
or isinstance(dtype, pd.CategoricalDtype)):
|
|
393
|
+
cats.append(col)
|
|
394
|
+
self.categorical_features = cats
|
|
395
|
+
if self.numeric_features is None:
|
|
396
|
+
# Only include columns that are actually numeric dtype
|
|
397
|
+
self.numeric_features = [
|
|
398
|
+
col
|
|
399
|
+
for col in X.columns
|
|
400
|
+
if col not in self.categorical_features
|
|
401
|
+
and pd.api.types.is_numeric_dtype(X[col].dtype)
|
|
402
|
+
]
|
|
403
|
+
else:
|
|
404
|
+
# ensure numeric_features not in categorical
|
|
405
|
+
pass
|
|
406
|
+
# map names to indices for internal use
|
|
407
|
+
self._cat_idx_names = self.categorical_features
|
|
408
|
+
self._num_idx_names = self.numeric_features
|
|
409
|
+
else:
|
|
410
|
+
# array input
|
|
411
|
+
self._input_is_df = False
|
|
412
|
+
X = check_array(X, ensure_all_finite=False, dtype=None)
|
|
413
|
+
n_cols = X.shape[1]
|
|
414
|
+
if self.categorical_features is None:
|
|
415
|
+
raise ValueError(
|
|
416
|
+
"For array input, categorical_features must be provided "
|
|
417
|
+
"as a list of column indices."
|
|
418
|
+
)
|
|
419
|
+
self._cat_idx_names = self.categorical_features
|
|
420
|
+
if self.numeric_features is None:
|
|
421
|
+
self._num_idx_names = [
|
|
422
|
+
i for i in range(n_cols) if i not in self._cat_idx_names
|
|
423
|
+
]
|
|
424
|
+
else:
|
|
425
|
+
self._num_idx_names = self.numeric_features
|
|
426
|
+
return X
|
|
427
|
+
|
|
428
|
+
def fit(self, X, y=None):
|
|
429
|
+
"""Fit the encoder and the iterative imputer.
|
|
430
|
+
|
|
431
|
+
Parameters
|
|
432
|
+
----------
|
|
433
|
+
X : DataFrame or ndarray of shape (n_samples, n_features)
|
|
434
|
+
Input data with missing values.
|
|
435
|
+
y : Ignored
|
|
436
|
+
Not used, present for API consistency.
|
|
437
|
+
|
|
438
|
+
Returns
|
|
439
|
+
-------
|
|
440
|
+
self : MixedTypeImputer
|
|
441
|
+
The fitted instance.
|
|
442
|
+
"""
|
|
443
|
+
X = self._validate_input(X, reset=True)
|
|
444
|
+
|
|
445
|
+
# Separate numeric and categorical parts
|
|
446
|
+
if self._input_is_df:
|
|
447
|
+
X_cat = X[self._cat_idx_names].copy()
|
|
448
|
+
X_num = X[self._num_idx_names].copy()
|
|
449
|
+
else:
|
|
450
|
+
X_cat = X[:, self._cat_idx_names]
|
|
451
|
+
X_num = X[:, self._num_idx_names]
|
|
452
|
+
|
|
453
|
+
# Replace categorical NaNs with a sentinel string so OrdinalEncoder works.
|
|
454
|
+
# Convert all categorical columns to string and replace NaN with sentinel.
|
|
455
|
+
sentinel = "__MISSING__"
|
|
456
|
+
if hasattr(X_cat, "fillna"):
|
|
457
|
+
# DataFrame path: convert each column to string, replacing NaN with sentinel
|
|
458
|
+
X_cat_filled = X_cat.copy()
|
|
459
|
+
for col in (X_cat_filled.columns if hasattr(X_cat_filled, "columns") else range(X_cat_filled.shape[1])):
|
|
460
|
+
if hasattr(X_cat_filled, "iloc"):
|
|
461
|
+
na_mask = X_cat_filled[col].isna()
|
|
462
|
+
as_str = X_cat_filled[col].astype(str)
|
|
463
|
+
X_cat_filled[col] = as_str.where(~na_mask, other=sentinel)
|
|
464
|
+
else:
|
|
465
|
+
# NumPy column access
|
|
466
|
+
col_data = X_cat_filled[:, col]
|
|
467
|
+
na_mask = pd.isnull(col_data)
|
|
468
|
+
X_cat_filled[:, col] = np.where(na_mask, sentinel, col_data.astype(str))
|
|
469
|
+
else:
|
|
470
|
+
# NumPy array path
|
|
471
|
+
na_mask = pd.isnull(X_cat)
|
|
472
|
+
X_cat_filled = X_cat.astype(str)
|
|
473
|
+
X_cat_filled[na_mask] = sentinel
|
|
474
|
+
|
|
475
|
+
# Fit OrdinalEncoder on the filled categorical part (skip if no cat cols)
|
|
476
|
+
if X_cat_filled.shape[1] > 0:
|
|
477
|
+
self.encoder_ = OrdinalEncoder(
|
|
478
|
+
handle_unknown="use_encoded_value",
|
|
479
|
+
unknown_value=-1,
|
|
480
|
+
dtype=np.int64,
|
|
481
|
+
)
|
|
482
|
+
self.encoder_.fit(X_cat_filled)
|
|
483
|
+
else:
|
|
484
|
+
self.encoder_ = None
|
|
485
|
+
|
|
486
|
+
# Transform to integers and find the encoded value for sentinel
|
|
487
|
+
if self.encoder_ is not None:
|
|
488
|
+
X_cat_encoded = self.encoder_.transform(X_cat_filled) # int array
|
|
489
|
+
# For each categorical column, find the index of sentinel
|
|
490
|
+
self._missing_encoded_values_ = []
|
|
491
|
+
for i, categories in enumerate(self.encoder_.categories_):
|
|
492
|
+
if sentinel in categories:
|
|
493
|
+
enc_val = np.where(categories == sentinel)[0][0]
|
|
494
|
+
else:
|
|
495
|
+
# If sentinel not present (no missing values in that column),
|
|
496
|
+
# choose an impossible integer to later replace with NaN
|
|
497
|
+
enc_val = -9999
|
|
498
|
+
self._missing_encoded_values_.append(enc_val)
|
|
499
|
+
|
|
500
|
+
# Replace the encoded sentinel value with np.nan so the imputer sees it as missing
|
|
501
|
+
X_cat_processed = X_cat_encoded.astype(np.float64)
|
|
502
|
+
for col_idx, miss_val in enumerate(self._missing_encoded_values_):
|
|
503
|
+
mask = X_cat_processed[:, col_idx] == miss_val
|
|
504
|
+
X_cat_processed[mask, col_idx] = np.nan
|
|
505
|
+
else:
|
|
506
|
+
X_cat_processed = np.empty((X.shape[0], 0), dtype=np.float64)
|
|
507
|
+
|
|
508
|
+
# Numeric part: ensure float and keep NaN
|
|
509
|
+
if isinstance(X_num, pd.DataFrame) and X_num.shape[1] > 0:
|
|
510
|
+
X_num_processed = X_num.to_numpy(dtype=np.float64, na_value=np.nan)
|
|
511
|
+
elif isinstance(X_num, pd.DataFrame):
|
|
512
|
+
X_num_processed = np.empty((X_num.shape[0], 0), dtype=np.float64)
|
|
513
|
+
elif isinstance(X_num, np.ndarray) and X_num.shape[1] > 0:
|
|
514
|
+
X_num_processed = X_num.astype(np.float64)
|
|
515
|
+
else:
|
|
516
|
+
X_num_processed = np.empty((X_num.shape[0], 0), dtype=np.float64)
|
|
517
|
+
|
|
518
|
+
# Concatenate: first categorical encoded columns, then numeric
|
|
519
|
+
X_all = np.hstack([X_cat_processed, X_num_processed])
|
|
520
|
+
|
|
521
|
+
# Categorical indices in the concatenated array are 0..n_cat-1
|
|
522
|
+
n_cat = X_cat_processed.shape[1]
|
|
523
|
+
self.categorical_features_ = list(range(n_cat))
|
|
524
|
+
|
|
525
|
+
# Build the internal imputer
|
|
526
|
+
if self.regressor is None:
|
|
527
|
+
from sklearn.ensemble import HistGradientBoostingRegressor
|
|
528
|
+
reg = HistGradientBoostingRegressor(random_state=self.random_state)
|
|
529
|
+
else:
|
|
530
|
+
reg = self.regressor
|
|
531
|
+
if self.classifier is None:
|
|
532
|
+
from sklearn.ensemble import HistGradientBoostingClassifier
|
|
533
|
+
clf = HistGradientBoostingClassifier(random_state=self.random_state)
|
|
534
|
+
else:
|
|
535
|
+
clf = self.classifier
|
|
536
|
+
|
|
537
|
+
self.imputation_model_ = _MixedTypeIterativeImputer(
|
|
538
|
+
estimator=_ColumnTypeEstimator(
|
|
539
|
+
regressor=reg,
|
|
540
|
+
classifier=clf,
|
|
541
|
+
categorical_indices=self.categorical_features_,
|
|
542
|
+
),
|
|
543
|
+
max_iter=self.max_iter,
|
|
544
|
+
tol=self.tol,
|
|
545
|
+
initial_strategy=self.initial_strategy,
|
|
546
|
+
sample_posterior=self.sample_posterior,
|
|
547
|
+
random_state=self.random_state,
|
|
548
|
+
verbose=self.verbose,
|
|
549
|
+
add_indicator=self.add_indicator,
|
|
550
|
+
keep_empty_features=self.keep_empty_features,
|
|
551
|
+
)
|
|
552
|
+
self.imputation_model_.fit(X_all)
|
|
553
|
+
|
|
554
|
+
# Track which features were dropped by the internal imputer
|
|
555
|
+
# (relevant when keep_empty_features=False and columns are all-NaN).
|
|
556
|
+
if (not self.keep_empty_features
|
|
557
|
+
and hasattr(self.imputation_model_, "_is_empty_feature")):
|
|
558
|
+
empty_mask = self.imputation_model_._is_empty_feature
|
|
559
|
+
self._all_empty_features_ = list(np.flatnonzero(empty_mask))
|
|
560
|
+
self._all_valid_features_ = list(np.flatnonzero(~empty_mask))
|
|
561
|
+
else:
|
|
562
|
+
self._all_empty_features_ = []
|
|
563
|
+
self._all_valid_features_ = list(range(X_all.shape[1]))
|
|
564
|
+
|
|
565
|
+
return self
|
|
566
|
+
|
|
567
|
+
def transform(self, X):
|
|
568
|
+
"""Impute missing values and return a DataFrame like the input.
|
|
569
|
+
|
|
570
|
+
Parameters
|
|
571
|
+
----------
|
|
572
|
+
X : DataFrame or ndarray of shape (n_samples, n_features)
|
|
573
|
+
|
|
574
|
+
Returns
|
|
575
|
+
-------
|
|
576
|
+
X_imputed : DataFrame or ndarray
|
|
577
|
+
"""
|
|
578
|
+
check_is_fitted(self, "imputation_model_")
|
|
579
|
+
X = self._validate_input(X, reset=False)
|
|
580
|
+
|
|
581
|
+
# Split and encode just like in fit
|
|
582
|
+
if self._input_is_df:
|
|
583
|
+
X_cat = X[self._cat_idx_names].copy()
|
|
584
|
+
X_num = X[self._num_idx_names].copy()
|
|
585
|
+
else:
|
|
586
|
+
X_cat = X[:, self._cat_idx_names]
|
|
587
|
+
X_num = X[:, self._num_idx_names]
|
|
588
|
+
|
|
589
|
+
sentinel = "__MISSING__"
|
|
590
|
+
if hasattr(X_cat, "fillna"):
|
|
591
|
+
# DataFrame path: convert each column to string, replacing NaN with sentinel
|
|
592
|
+
X_cat_filled = X_cat.copy()
|
|
593
|
+
for col in (X_cat_filled.columns if hasattr(X_cat_filled, "columns") else range(X_cat_filled.shape[1])):
|
|
594
|
+
if hasattr(X_cat_filled, "iloc"):
|
|
595
|
+
na_mask = X_cat_filled[col].isna()
|
|
596
|
+
as_str = X_cat_filled[col].astype(str)
|
|
597
|
+
X_cat_filled[col] = as_str.where(~na_mask, other=sentinel)
|
|
598
|
+
else:
|
|
599
|
+
col_data = X_cat_filled[:, col]
|
|
600
|
+
na_mask = pd.isnull(col_data)
|
|
601
|
+
X_cat_filled[:, col] = np.where(na_mask, sentinel, col_data.astype(str))
|
|
602
|
+
else:
|
|
603
|
+
# NumPy array path
|
|
604
|
+
na_mask = pd.isnull(X_cat)
|
|
605
|
+
X_cat_filled = X_cat.astype(str)
|
|
606
|
+
X_cat_filled[na_mask] = sentinel
|
|
607
|
+
|
|
608
|
+
if self.encoder_ is not None:
|
|
609
|
+
X_cat_encoded = self.encoder_.transform(X_cat_filled)
|
|
610
|
+
X_cat_processed = X_cat_encoded.astype(np.float64)
|
|
611
|
+
for col_idx, miss_val in enumerate(self._missing_encoded_values_):
|
|
612
|
+
mask = X_cat_processed[:, col_idx] == miss_val
|
|
613
|
+
X_cat_processed[mask, col_idx] = np.nan
|
|
614
|
+
else:
|
|
615
|
+
X_cat_processed = np.empty((X.shape[0], 0), dtype=np.float64)
|
|
616
|
+
|
|
617
|
+
if isinstance(X_num, pd.DataFrame) and X_num.shape[1] > 0:
|
|
618
|
+
X_num_processed = X_num.to_numpy(dtype=np.float64, na_value=np.nan)
|
|
619
|
+
elif isinstance(X_num, pd.DataFrame):
|
|
620
|
+
X_num_processed = np.empty((X_num.shape[0], 0), dtype=np.float64)
|
|
621
|
+
elif isinstance(X_num, np.ndarray) and X_num.shape[1] > 0:
|
|
622
|
+
X_num_processed = X_num.astype(np.float64)
|
|
623
|
+
else:
|
|
624
|
+
X_num_processed = np.empty((X_num.shape[0], 0), dtype=np.float64)
|
|
625
|
+
X_all = np.hstack([X_cat_processed, X_num_processed])
|
|
626
|
+
|
|
627
|
+
# Impute
|
|
628
|
+
X_imputed_all_reduced = self.imputation_model_.transform(X_all)
|
|
629
|
+
|
|
630
|
+
# When keep_empty_features=False, the internal imputer drops all-NaN
|
|
631
|
+
# columns. We need to map the reduced output back to original columns.
|
|
632
|
+
n_features_full = X_all.shape[1]
|
|
633
|
+
if len(self._all_empty_features_) > 0:
|
|
634
|
+
# Build a map from output column index → original (full) column index
|
|
635
|
+
full_idx_from_reduced = {
|
|
636
|
+
out_idx: full_idx
|
|
637
|
+
for out_idx, full_idx in enumerate(self._all_valid_features_)
|
|
638
|
+
}
|
|
639
|
+
# Re-expand to full size with NaN in dropped positions
|
|
640
|
+
X_imputed_all = np.full(
|
|
641
|
+
(X_imputed_all_reduced.shape[0], n_features_full), np.nan
|
|
642
|
+
)
|
|
643
|
+
for out_idx, full_idx in full_idx_from_reduced.items():
|
|
644
|
+
X_imputed_all[:, full_idx] = X_imputed_all_reduced[:, out_idx]
|
|
645
|
+
|
|
646
|
+
# Drop dropped columns from the column name lists for reconstruction
|
|
647
|
+
empty_set = set(self._all_empty_features_)
|
|
648
|
+
n_cat = X_cat_processed.shape[1]
|
|
649
|
+
# Categorical columns occupy indices 0..n_cat-1
|
|
650
|
+
cat_idx_names_kept = [
|
|
651
|
+
name for i, name in enumerate(self._cat_idx_names)
|
|
652
|
+
if i not in empty_set
|
|
653
|
+
]
|
|
654
|
+
# Numeric columns occupy indices n_cat..n_features_full-1
|
|
655
|
+
num_idx_names_kept = [
|
|
656
|
+
name for i, name in enumerate(self._num_idx_names)
|
|
657
|
+
if (n_cat + i) not in empty_set
|
|
658
|
+
]
|
|
659
|
+
# Also update input_columns for the final reorder (DataFrame only)
|
|
660
|
+
if self._input_is_df:
|
|
661
|
+
dropped_col_names = set(
|
|
662
|
+
list(self._cat_idx_names) + list(self._num_idx_names)
|
|
663
|
+
) - set(cat_idx_names_kept + num_idx_names_kept)
|
|
664
|
+
input_columns_kept = [
|
|
665
|
+
c for c in self._input_columns if c not in dropped_col_names
|
|
666
|
+
]
|
|
667
|
+
else:
|
|
668
|
+
input_columns_kept = None
|
|
669
|
+
else:
|
|
670
|
+
X_imputed_all = X_imputed_all_reduced
|
|
671
|
+
cat_idx_names_kept = list(self._cat_idx_names)
|
|
672
|
+
num_idx_names_kept = list(self._num_idx_names)
|
|
673
|
+
input_columns_kept = (
|
|
674
|
+
list(self._input_columns) if self._input_is_df else None
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
# Separate imputed parts
|
|
678
|
+
n_cat = X_cat_processed.shape[1]
|
|
679
|
+
X_cat_imputed = X_imputed_all[:, :n_cat]
|
|
680
|
+
X_num_imputed = X_imputed_all[:, n_cat:]
|
|
681
|
+
|
|
682
|
+
# If features were dropped, keep only the surviving ones
|
|
683
|
+
if len(self._all_empty_features_) > 0:
|
|
684
|
+
empty_set = set(self._all_empty_features_)
|
|
685
|
+
cat_kept_indices = [
|
|
686
|
+
i for i in range(n_cat) if i not in empty_set
|
|
687
|
+
]
|
|
688
|
+
num_kept_indices = [
|
|
689
|
+
i for i in range(X_num_imputed.shape[1])
|
|
690
|
+
if (n_cat + i) not in empty_set
|
|
691
|
+
]
|
|
692
|
+
X_cat_imputed = X_cat_imputed[:, cat_kept_indices]
|
|
693
|
+
X_num_imputed = X_num_imputed[:, num_kept_indices]
|
|
694
|
+
|
|
695
|
+
# Decode categorical part if any categorical columns exist
|
|
696
|
+
if n_cat > 0:
|
|
697
|
+
# Round categorical imputations to integers (the classifier outputs discrete values)
|
|
698
|
+
X_cat_imputed = np.round(X_cat_imputed).astype(int)
|
|
699
|
+
|
|
700
|
+
# Replace any value that equals the missing-encoded integer with that integer
|
|
701
|
+
# (they shouldn't persist, but just in case)
|
|
702
|
+
# Then decode using inverse transform
|
|
703
|
+
X_cat_decoded = self.encoder_.inverse_transform(X_cat_imputed)
|
|
704
|
+
|
|
705
|
+
# If any decoded value is sentinel, replace with np.nan (shouldn't happen after imputation)
|
|
706
|
+
if hasattr(X_cat_decoded, "astype"):
|
|
707
|
+
X_cat_decoded = X_cat_decoded.astype(object)
|
|
708
|
+
mask_missing = X_cat_decoded == sentinel
|
|
709
|
+
if np.any(mask_missing):
|
|
710
|
+
X_cat_decoded[mask_missing] = np.nan
|
|
711
|
+
else:
|
|
712
|
+
# No categorical columns: create empty object array
|
|
713
|
+
X_cat_decoded = np.empty((X_all.shape[0], 0), dtype=object)
|
|
714
|
+
|
|
715
|
+
# Rebuild DataFrame if input was DataFrame
|
|
716
|
+
if self._input_is_df:
|
|
717
|
+
# Build a DataFrame with correct column order
|
|
718
|
+
result = pd.DataFrame(index=self._input_index)
|
|
719
|
+
# categorical columns first (original order)
|
|
720
|
+
for i, col_name in enumerate(cat_idx_names_kept):
|
|
721
|
+
result[col_name] = X_cat_decoded[:, i]
|
|
722
|
+
for i, col_name in enumerate(num_idx_names_kept):
|
|
723
|
+
result[col_name] = X_num_imputed[:, i]
|
|
724
|
+
# Ensure original column order (minus dropped columns)
|
|
725
|
+
result = result[input_columns_kept]
|
|
726
|
+
return result
|
|
727
|
+
|
|
728
|
+
# else return ndarray in the same column order as input
|
|
729
|
+
# Note: the original X was (cat then num)? No, the input order may differ.
|
|
730
|
+
# We need to reconstruct the original column order.
|
|
731
|
+
# For simplicity, we'll just return a concatenation of cat followed by num,
|
|
732
|
+
# but that might not match input order. To preserve, we must reorder.
|
|
733
|
+
# For array input, we assume user provided categorical_features as indices,
|
|
734
|
+
# and we'll arrange output in the original order (0..n_features-1).
|
|
735
|
+
# We'll build a full array with original column positions.
|
|
736
|
+
n_features = len(self._cat_idx_names) + len(self._num_idx_names)
|
|
737
|
+
X_out = np.empty((X_all.shape[0], n_features))
|
|
738
|
+
X_out[:, self._cat_idx_names] = X_cat_decoded
|
|
739
|
+
X_out[:, self._num_idx_names] = X_num_imputed
|
|
740
|
+
return X_out
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mixed-imputer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Mixed-type MICE imputer for scikit-learn — handles both numerical and categorical (string) columns in DataFrames.
|
|
5
|
+
Author: dnnsqe
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/dnnsqe/mixed-imputer
|
|
8
|
+
Project-URL: Issues, https://github.com/dnnsqe/mixed-imputer
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: numpy
|
|
23
|
+
Requires-Dist: pandas
|
|
24
|
+
Requires-Dist: scipy
|
|
25
|
+
Requires-Dist: scikit-learn>=1.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# MixedTypeImputer
|
|
32
|
+
|
|
33
|
+
**MixedTypeImputer** is a scikit-learn compatible transformer that performs iterative imputation (MICE) on DataFrames containing both numerical and categorical (string) columns.
|
|
34
|
+
|
|
35
|
+
It automatically detects column types, encodes categoricals with `OrdinalEncoder`, runs an iterative MICE‑style imputer that chooses a regressor or classifier per column, and decodes categoricals back to their original string values.
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- **Mixed-type support** — handles numeric and string categorical columns in the same DataFrame
|
|
40
|
+
- **Auto-detection** — automatically identifies categorical columns by dtype (object/category/string)
|
|
41
|
+
- **Iterative imputation (MICE)** — models each column as a function of all others
|
|
42
|
+
- **Posterior sampling** — supports stochastic imputation via `sample_posterior=True`
|
|
43
|
+
- **scikit-learn compatible** — `fit` / `transform` / `fit_transform` API, works in `Pipeline`
|
|
44
|
+
- **DataFrame-native** — input a DataFrame, get a DataFrame back
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install mixed-imputer
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Or for development:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
git clone https://github.com/dnnsqe/mixed-imputer.git
|
|
56
|
+
cd mixed-imputer
|
|
57
|
+
pip install -e ".[dev]"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Quick Start
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
import pandas as pd
|
|
64
|
+
import numpy as np
|
|
65
|
+
from mixed_imputer import MixedTypeImputer
|
|
66
|
+
|
|
67
|
+
# Create sample data with missing values
|
|
68
|
+
data = pd.DataFrame({
|
|
69
|
+
'age': [25, 30, np.nan, 40],
|
|
70
|
+
'city': ['paris', 'london', np.nan, 'paris'],
|
|
71
|
+
'income': [50000, np.nan, 70000, 60000],
|
|
72
|
+
'gender': ['M', 'F', 'M', np.nan],
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
# Auto-detect categorical columns (str, object, or category dtype)
|
|
76
|
+
# or specify them manually via categorical_features.
|
|
77
|
+
imputer = MixedTypeImputer(
|
|
78
|
+
max_iter=5,
|
|
79
|
+
random_state=42,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
imputed = imputer.fit_transform(data)
|
|
83
|
+
print(imputed)
|
|
84
|
+
# age city income gender
|
|
85
|
+
# 0 25.000 paris 50000.0 M
|
|
86
|
+
# 1 30.000 london 58750.0 F
|
|
87
|
+
# 2 32.500 london 70000.0 M
|
|
88
|
+
# 3 40.000 paris 60000.0 F
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Parameters
|
|
92
|
+
|
|
93
|
+
| Parameter | Type | Default | Description |
|
|
94
|
+
|-----------|------|---------|-------------|
|
|
95
|
+
| `categorical_features` | list of int/str or None | `None` | Column indices or names that are categorical. If `None` and input is a DataFrame, columns with `object`, `string`, or `category` dtype are auto-detected. |
|
|
96
|
+
| `numeric_features` | list of int/str or None | `None` | Numeric column indices or names. If `None`, auto-detected as all columns whose dtype passes `pd.api.types.is_numeric_dtype` and are not listed in `categorical_features`. |
|
|
97
|
+
| `regressor` | estimator or None | `HistGradientBoostingRegressor()` | Regressor for numerical target columns. |
|
|
98
|
+
| `classifier` | estimator or None | `HistGradientBoostingClassifier()` | Classifier for categorical target columns. |
|
|
99
|
+
| `max_iter` | int | `10` | Maximum number of imputation rounds. |
|
|
100
|
+
| `tol` | float | `1e-3` | Tolerance for early stopping. |
|
|
101
|
+
| `initial_strategy` | str | `"mean"` | Initial imputation strategy (`"mean"`, `"median"`, `"most_frequent"`, `"constant"`). |
|
|
102
|
+
| `sample_posterior` | bool | `False` | If `True`, sample from predictive posterior for stochastic imputation. |
|
|
103
|
+
| `random_state` | int, RandomState or None | `None` | Seed for reproducibility. |
|
|
104
|
+
| `verbose` | int | `0` | Verbosity level. |
|
|
105
|
+
| `add_indicator` | bool | `False` | If `True`, add missing indicator columns. |
|
|
106
|
+
| `keep_empty_features` | bool | `False` | If `True`, keep features that are all-missing at fit time. |
|
|
107
|
+
|
|
108
|
+
## How It Works
|
|
109
|
+
|
|
110
|
+
1. **Column detection** — object/string/category dtype columns are identified as categorical; the rest as numeric.
|
|
111
|
+
2. **Encoding** — categorical columns are encoded to integers using `OrdinalEncoder`, with NaN replaced by a sentinel value.
|
|
112
|
+
3. **Iterative imputation** — a modified `IterativeImputer` uses `HistGradientBoostingRegressor` for numeric columns and `HistGradientBoostingClassifier` for categorical columns.
|
|
113
|
+
4. **Decoding** — imputed integer values are inverse-transformed back to their original string categories.
|
|
114
|
+
|
|
115
|
+
## Requirements
|
|
116
|
+
|
|
117
|
+
- Python ≥ 3.9
|
|
118
|
+
- numpy
|
|
119
|
+
- pandas
|
|
120
|
+
- scipy
|
|
121
|
+
- scikit-learn ≥ 1.0
|
|
122
|
+
|
|
123
|
+
## Running Tests
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
pip install -e ".[dev]"
|
|
127
|
+
pytest
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Examples
|
|
131
|
+
|
|
132
|
+
See [`examples/examples.py`](examples/examples.py) for a runnable script that demonstrates all major
|
|
133
|
+
features including auto-detection, posterior sampling, custom estimators, array
|
|
134
|
+
input, pipeline integration, and edge cases.
|
|
135
|
+
|
|
136
|
+
## Known Limitations
|
|
137
|
+
|
|
138
|
+
- **Unseen categories at transform time** are encoded to the `unknown_value` of
|
|
139
|
+
the `OrdinalEncoder`. If the imputer assigns a value that decodes to an
|
|
140
|
+
unknown category it will appear as `NaN` in the output. Ensure the training
|
|
141
|
+
set covers the full vocabulary of each categorical column when possible.
|
|
142
|
+
- **`keep_empty_features=False`** (the default) drops columns that are entirely
|
|
143
|
+
`NaN` during `fit`. The dropped columns are removed from the output
|
|
144
|
+
DataFrame.
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
MIT — see [LICENSE](LICENSE) for details.
|
|
149
|
+
|
|
150
|
+
## Links
|
|
151
|
+
|
|
152
|
+
- [GitHub Repository](https://github.com/dnnsqe/mixed-imputer)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
mixed_imputer/__init__.py,sha256=BRegK1J5B0VqwBJ8aVnhjaA2G6XzuZF97jI7uX_1bdY,124
|
|
2
|
+
mixed_imputer/_imputer.py,sha256=lFl25yDEpPD33vzvRIoY7p5_Q0YJwUTKLPPdynaQ2FY,31452
|
|
3
|
+
mixed_imputer/_version.py,sha256=QTYqXqSTHFRkM9TEgpDFcHvwLbvqHDqvqfQ9EiXkcAM,23
|
|
4
|
+
mixed_imputer-0.1.0.dist-info/licenses/LICENSE,sha256=C_uaDFKjnVEDLb0t-Eh_6zuzqppQRSj5C_X0EWgnqLw,1084
|
|
5
|
+
mixed_imputer-0.1.0.dist-info/METADATA,sha256=VgCt1xIZifH_Acsrnv1vwk0uX2lSzkkT2Hdgx1iDaNU,6290
|
|
6
|
+
mixed_imputer-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
7
|
+
mixed_imputer-0.1.0.dist-info/top_level.txt,sha256=JXEP7aK-rVlCIWZqUiW4BSD2oe4jhHMIC6OOyrLJ_wI,14
|
|
8
|
+
mixed_imputer-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dnnsqe
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mixed_imputer
|