Skip to content

Commit 67bdf4e

Browse files
Merge pull request #523 from WilliamK112/codex/docs-imputation-strategies-513
docs(cookbook): choose and audit imputation strategies
2 parents 1e69d44 + 4974db7 commit 67bdf4e

4 files changed

Lines changed: 273 additions & 1 deletion

File tree

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
---
2+
title: Choose and audit missing-value imputation
3+
description: Compare simple and MissForest imputation, configure column rules, protect declared roles, and inspect actual repairs.
4+
---
5+
6+
# Choose and audit missing-value imputation
7+
8+
Missing values need a decision: retain the gap, fill it from the observed
9+
column, or estimate it from other features. An imputed value is an estimate,
10+
not a recovered observation. Start with the simplest strategy that fits your
11+
data and inspect its effects before using the result downstream.
12+
13+
## Choose a strategy
14+
15+
| `impute` value | Behavior | Tradeoff |
16+
| --- | --- | --- |
17+
| `"mean"` | Fill numeric columns with their observed mean. Non-numeric columns are skipped. | Sensitive to extreme values; repeated fills reduce variation. |
18+
| `"median"` | Fill numeric columns with their observed median. Non-numeric columns are skipped. | More resistant to extreme values, but still a constant estimate. |
19+
| `"mode"` | Fill from the most frequent observed value, including text columns. | Can reinforce the majority category; inspect ties and rare categories. |
20+
| `"auto"` | Use median for numeric columns and mode for other columns. | A simple per-column rule, not model selection. |
21+
| `"missforest"` | Use iterative random-forest regression/classification with other columns as predictors. | Ineligible data can take an audited simple fallback; model-eligible data requires the ML dependencies. |
22+
23+
Simple imputation uses only the observed values of the column being filled.
24+
FreshData's MissForest-inspired implementation initializes gaps, fits a model
25+
for each eligible column, and iterates up to `missforest_max_iter`, stopping
26+
earlier if it converges. More complexity does not guarantee better estimates.
27+
Compare choices on representative data using validation suited to your task.
28+
29+
`strategy` controls the broader cleaning engine; `impute` and `impute_strategy`
30+
are explicit missing-value overrides:
31+
32+
| Combination | What to expect |
33+
| --- | --- |
34+
| `strategy="conservative"`, `impute=None` | Representation repairs; no decision-engine imputation. |
35+
| `strategy="balanced"`, `impute=None` | Accuracy-first decisions based on column roles, missingness, and available evidence. Gaps may remain. |
36+
| `strategy="aggressive"`, `impute=None` | More active filling and column-drop decisions, with default outlier flagging. Capping requires `outlier_action="cap"`. |
37+
| Any strategy with an explicit `impute` choice | Apply that override to eligible columns; declared ID/target protection still applies. |
38+
| Any strategy with `impute_strategy={...}` | Rules for the named columns override the global `impute` choice. |
39+
40+
The examples use `"conservative"` to make explicit imputation easy to inspect.
41+
Other strategies can make additional decisions; none guarantees that every
42+
gap will be filled. See the [cleaning engine](../cleaning-engine.md).
43+
44+
## Apply global and per-column rules
45+
46+
Fill age with its median and group with its mode. Deliberately request fills
47+
for declared ID and target columns to verify that those requests are ignored
48+
and audited. One age and one group are missing at `row-c`.
49+
50+
```python
51+
import pandas as pd
52+
import freshdata as fd
53+
54+
frame = pd.DataFrame(
55+
{
56+
"record_key": ["A", None, "C", "D", "E"],
57+
"age": [20.0, 30.0, None, 40.0, 90.0],
58+
"group": ["red", "blue", None, "red", "red"],
59+
"outcome": [1.0, 0.0, 1.0, None, 0.0],
60+
},
61+
index=["row-a", "row-b", "row-c", "row-d", "row-e"],
62+
)
63+
64+
cleaned, report = fd.clean(
65+
frame,
66+
strategy="conservative",
67+
impute="median", # global numeric rule
68+
impute_strategy={
69+
"group": "mode", # overrides the global rule
70+
"record_key": "mode", # ignored: declared identifier
71+
"outcome": "mean", # ignored: declared target
72+
},
73+
id_columns=("record_key",),
74+
target_column="outcome",
75+
fix_dtypes=False,
76+
drop_empty_rows=False,
77+
drop_empty_columns=False,
78+
return_report=True,
79+
verbose=False,
80+
)
81+
82+
assert cleaned.loc["row-c", "age"] == 35.0
83+
assert cleaned.loc["row-c", "group"] == "red"
84+
pd.testing.assert_series_equal(cleaned["record_key"], frame["record_key"])
85+
pd.testing.assert_series_equal(cleaned["outcome"], frame["outcome"])
86+
print(cleaned[["age", "group"]])
87+
print(report.warnings)
88+
```
89+
90+
The warnings identify ignored rules for `record_key` and `outcome`. Declare
91+
important roles explicitly: explicit simple imputation protects `id_columns`
92+
and `target_column`, so do not rely on name heuristics to protect arbitrary
93+
keys or labels. Context-protected columns also remain untouched. All-missing
94+
columns have no observed fill value, and numeric rules do not apply to text;
95+
those gaps can remain.
96+
97+
Use the post-normalization names for `impute_strategy`, `id_columns`, and
98+
`target_column` when the default `column_names=True` renames input headers.
99+
For `"Customer Age"`, use `"customer_age"`. This is essential for MissForest:
100+
its role gate matches the normalized column name exactly; declaring a custom
101+
target under its original spaced name can leave it unprotected. Alternatively,
102+
use `column_names=False` and declarations matching the unchanged headers.
103+
Configure these options through `fd.clean` keywords or `fd.CleanConfig`.
104+
105+
## Inspect the audit and the changed cells
106+
107+
Continue with `frame`, `cleaned`, and `report` from the previous example:
108+
109+
```python
110+
for action in report.actions:
111+
if action.step == "impute":
112+
print(action.column, action.count, action.description, action.metadata)
113+
114+
# Actions report a column and affected count, not every cell's location.
115+
# This example keeps the index, columns, and row order, so the frames align.
116+
pd.testing.assert_index_equal(cleaned.index, frame.index)
117+
assert all(column in cleaned.columns for column in frame.columns)
118+
filled = frame.isna() & cleaned[frame.columns].notna()
119+
repairs = [
120+
{
121+
"row_position": position,
122+
"row_label": frame.index[position],
123+
"column": column,
124+
"value": cleaned[column].iloc[position],
125+
}
126+
for column in frame.columns
127+
for position in range(len(frame))
128+
if filled[column].iloc[position]
129+
]
130+
print(repairs)
131+
assert [(item["row_label"], item["column"]) for item in repairs] == [
132+
("row-c", "age"),
133+
("row-c", "group"),
134+
]
135+
136+
# Persist the structured aggregate audit when you need a portable record.
137+
audit_json = report.to_json()
138+
```
139+
140+
The filled cells are age `35.0` and group `"red"` at `row-c`; ID and target gaps
141+
remain. Imputation actions report one affected cell per fill and zero-count
142+
notes for protected columns. `CleanReport` also captures warnings and overall
143+
before/after missing counts. `columns_imputed` is a decision-engine summary;
144+
use the actual `impute` actions for explicit overrides.
145+
146+
This comparison identifies missing-to-present repairs. If your plan drops or
147+
reorders rows or renames columns, align the frames using a stable record
148+
identity first. It is not a history of every non-missing value change. See
149+
[audit automated cleaning](audit-automated-data-cleaning.md) and the
150+
[audit schema](../audit-trail.md) for supported report fields.
151+
152+
## Try model-based imputation
153+
154+
Install the optional ML dependencies:
155+
156+
```bash
157+
pip install "freshdata-cleaner[ml]"
158+
```
159+
160+
This separate example has 80 rows and related numeric/categorical predictors.
161+
It fills eight missing charges with a small forest and at most two iterations.
162+
No identifier or target column is included in this model example.
163+
164+
```python
165+
import pandas as pd
166+
import freshdata as fd
167+
168+
training = pd.DataFrame(
169+
{
170+
"hours": [float(i % 12) for i in range(80)],
171+
"charges": [10 + 2 * (i % 12) + 0.5 * (i % 5) for i in range(80)],
172+
"kind": ["standard", "priority"] * 40,
173+
}
174+
)
175+
training.loc[training.index[::11], "charges"] = float("nan")
176+
177+
estimated, model_report = fd.clean(
178+
training,
179+
strategy="conservative",
180+
impute_strategy={"charges": "missforest"},
181+
missforest_n_estimators=10,
182+
missforest_max_iter=2,
183+
missforest_random_state=42,
184+
missforest_add_indicators=False,
185+
fix_dtypes=False,
186+
return_report=True,
187+
verbose=False,
188+
)
189+
190+
assert estimated["charges"].notna().all()
191+
for action in model_report.actions:
192+
if action.step == "impute" and action.column == "charges":
193+
print(action.count, action.metadata)
194+
assert action.count == 8
195+
```
196+
197+
Inspect `selected_model_type`, `iterations`, `converged`, and `fallback_reason`
198+
in action metadata. With ML dependencies installed, this example reports
199+
`selected_model_type="regressor"` and `fallback_reason=None`. Reaching the
200+
iteration limit is recorded separately from convergence. Numerical estimates
201+
and model diagnostics can vary with dependency versions.
202+
203+
`missforest_min_rows_for_model` defaults to 50. Smaller inputs take a recorded
204+
simple-imputation fallback. Use the earlier five-row `frame`:
205+
206+
```python
207+
small, small_report = fd.clean(
208+
frame,
209+
strategy="conservative",
210+
impute_strategy={"age": "missforest"},
211+
id_columns=("record_key",),
212+
target_column="outcome",
213+
missforest_add_indicators=False,
214+
fix_dtypes=False,
215+
drop_empty_rows=False,
216+
drop_empty_columns=False,
217+
return_report=True,
218+
verbose=False,
219+
)
220+
221+
assert small.loc["row-c", "age"] == 35.0
222+
fallback_actions = [
223+
action for action in small_report.actions
224+
if action.step == "impute" and action.column == "age"
225+
]
226+
assert len(fallback_actions) == 1
227+
print(fallback_actions[0].metadata["fallback_reason"])
228+
assert "fewer rows" in fallback_actions[0].metadata["fallback_reason"]
229+
```
230+
231+
Insufficient predictors and ineligible columns can also lead to fallback or
232+
preservation; inspect the audit rather than assuming a requested model ran.
233+
If a column is model-eligible but scikit-learn is absent, cleaning raises
234+
`ImportError` with the ML installation hint. No result or report is returned
235+
for that failed call; install `freshdata-cleaner[ml]` before retrying.
236+
237+
The following example protects a custom target whose input header is renamed.
238+
The declaration and imputation rule both use its post-normalization name:
239+
240+
```python
241+
raw = pd.DataFrame(
242+
{
243+
"Observed Hours": [float(i % 12) for i in range(80)],
244+
"Future Sales": [10 + 2 * (i % 12) + 0.5 * (i % 5) for i in range(80)],
245+
}
246+
)
247+
raw.loc[raw.index[::11], "Future Sales"] = float("nan")
248+
249+
protected, protected_report = fd.clean(
250+
raw,
251+
strategy="conservative",
252+
impute_strategy={"future_sales": "missforest"},
253+
target_column="future_sales", # matches after header normalization
254+
fix_dtypes=False,
255+
return_report=True,
256+
verbose=False,
257+
)
258+
259+
pd.testing.assert_series_equal(
260+
protected["future_sales"], raw["Future Sales"].rename("future_sales")
261+
)
262+
assert protected["future_sales"].isna().sum() == 8
263+
```
264+
265+
With matching declarations, ID and target gaps are preserved. Role protection
266+
is not a train/test leakage guarantee: cleaning learns from the supplied frame.
267+
Isolate training data and validate your downstream workflow before estimating
268+
values in evaluation data.

‎docs/cookbook/index.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ Every recipe is self-contained with runnable code, realistic sample outputs, and
2424
Role-aware imputation: why identifiers and targets must be preserved while numerics are safely imputed.
2525
*(Search intent: safe missing value imputation, fillna without corruption)*
2626

27+
- :material-help-box: **[Choose and audit missing-value imputation](imputation-strategies-guide.md)**
28+
Compare simple and model-based fills, declare protected roles, and inspect actual repairs.
29+
*(Search intent: mean median mode imputation, missforest, per-column imputation)*
30+
2731
- :material-content-copy: **[How to detect and resolve duplicate rows](detect-and-resolve-duplicates.md)**
2832
Report duplicate clusters without silent row drops, and apply controlled deduplication policies.
2933
*(Search intent: detect duplicate rows pandas, safe deduplication python)*

‎docs/fallback-matrix.md‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ from pprint import pprint
8484

8585
import polars as pl
8686
import freshdata as fd
87-
from freshdata.execution import PlanGenerator
8887

8988
frame = pl.DataFrame({"value": [1, 2]})
9089
backend = "polars"

‎mkdocs.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ nav:
146146
- Overview: cookbook/index.md
147147
- Clean pandas DataFrame: cookbook/clean-pandas-dataframe.md
148148
- Safely handle missing values: cookbook/safely-handle-missing-values.md
149+
- Imputation strategies: cookbook/imputation-strategies-guide.md
149150
- Detect & resolve duplicates: cookbook/detect-and-resolve-duplicates.md
150151
- Prepare data for ML: cookbook/prepare-data-for-machine-learning.md
151152
- Clean Polars DataFrames: cookbook/clean-polars-dataframes.md

0 commit comments

Comments
 (0)