Skip to content

Choose and audit missing-value imputation

Missing values need a decision: retain the gap, fill it from the observed column, or estimate it from other features. An imputed value is an estimate, not a recovered observation. Start with the simplest strategy that fits your data and inspect its effects before using the result downstream.

Choose a strategy

impute value Behavior Tradeoff
"mean" Fill numeric columns with their observed mean. Non-numeric columns are skipped. Sensitive to extreme values; repeated fills reduce variation.
"median" Fill numeric columns with their observed median. Non-numeric columns are skipped. More resistant to extreme values, but still a constant estimate.
"mode" Fill from the most frequent observed value, including text columns. Can reinforce the majority category; inspect ties and rare categories.
"auto" Use median for numeric columns and mode for other columns. A simple per-column rule, not model selection.
"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.

Simple imputation uses only the observed values of the column being filled. FreshData's MissForest-inspired implementation initializes gaps, fits a model for each eligible column, and iterates up to missforest_max_iter, stopping earlier if it converges. More complexity does not guarantee better estimates. Compare choices on representative data using validation suited to your task.

strategy controls the broader cleaning engine; impute and impute_strategy are explicit missing-value overrides:

Combination What to expect
strategy="conservative", impute=None Representation repairs; no decision-engine imputation.
strategy="balanced", impute=None Accuracy-first decisions based on column roles, missingness, and available evidence. Gaps may remain.
strategy="aggressive", impute=None More active filling and column-drop decisions, with default outlier flagging. Capping requires outlier_action="cap".
Any strategy with an explicit impute choice Apply that override to eligible columns; declared ID/target protection still applies.
Any strategy with impute_strategy={...} Rules for the named columns override the global impute choice.

The examples use "conservative" to make explicit imputation easy to inspect. Other strategies can make additional decisions; none guarantees that every gap will be filled. See the cleaning engine.

Apply global and per-column rules

Fill age with its median and group with its mode. Deliberately request fills for declared ID and target columns to verify that those requests are ignored and audited. One age and one group are missing at row-c.

import pandas as pd
import freshdata as fd

frame = pd.DataFrame(
    {
        "record_key": ["A", None, "C", "D", "E"],
        "age": [20.0, 30.0, None, 40.0, 90.0],
        "group": ["red", "blue", None, "red", "red"],
        "outcome": [1.0, 0.0, 1.0, None, 0.0],
    },
    index=["row-a", "row-b", "row-c", "row-d", "row-e"],
)

cleaned, report = fd.clean(
    frame,
    strategy="conservative",
    impute="median",                  # global numeric rule
    impute_strategy={
        "group": "mode",              # overrides the global rule
        "record_key": "mode",         # ignored: declared identifier
        "outcome": "mean",            # ignored: declared target
    },
    id_columns=("record_key",),
    target_column="outcome",
    fix_dtypes=False,
    drop_empty_rows=False,
    drop_empty_columns=False,
    return_report=True,
    verbose=False,
)

assert cleaned.loc["row-c", "age"] == 35.0
assert cleaned.loc["row-c", "group"] == "red"
pd.testing.assert_series_equal(cleaned["record_key"], frame["record_key"])
pd.testing.assert_series_equal(cleaned["outcome"], frame["outcome"])
print(cleaned[["age", "group"]])
print(report.warnings)

The warnings identify ignored rules for record_key and outcome. Declare important roles explicitly: explicit simple imputation protects id_columns and target_column, so do not rely on name heuristics to protect arbitrary keys or labels. Context-protected columns also remain untouched. All-missing columns have no observed fill value, and numeric rules do not apply to text; those gaps can remain.

Use the post-normalization names for impute_strategy, id_columns, and target_column when the default column_names=True renames input headers. For "Customer Age", use "customer_age". This is essential for MissForest: its role gate matches the normalized column name exactly; declaring a custom target under its original spaced name can leave it unprotected. Alternatively, use column_names=False and declarations matching the unchanged headers. Configure these options through fd.clean keywords or fd.CleanConfig.

Inspect the audit and the changed cells

Continue with frame, cleaned, and report from the previous example:

for action in report.actions:
    if action.step == "impute":
        print(action.column, action.count, action.description, action.metadata)

# Actions report a column and affected count, not every cell's location.
# This example keeps the index, columns, and row order, so the frames align.
pd.testing.assert_index_equal(cleaned.index, frame.index)
assert all(column in cleaned.columns for column in frame.columns)
filled = frame.isna() & cleaned[frame.columns].notna()
repairs = [
    {
        "row_position": position,
        "row_label": frame.index[position],
        "column": column,
        "value": cleaned[column].iloc[position],
    }
    for column in frame.columns
    for position in range(len(frame))
    if filled[column].iloc[position]
]
print(repairs)
assert [(item["row_label"], item["column"]) for item in repairs] == [
    ("row-c", "age"),
    ("row-c", "group"),
]

# Persist the structured aggregate audit when you need a portable record.
audit_json = report.to_json()

The filled cells are age 35.0 and group "red" at row-c; ID and target gaps remain. Imputation actions report one affected cell per fill and zero-count notes for protected columns. CleanReport also captures warnings and overall before/after missing counts. columns_imputed is a decision-engine summary; use the actual impute actions for explicit overrides.

This comparison identifies missing-to-present repairs. If your plan drops or reorders rows or renames columns, align the frames using a stable record identity first. It is not a history of every non-missing value change. See audit automated cleaning and the audit schema for supported report fields.

Try model-based imputation

Install the optional ML dependencies:

pip install "freshdata-cleaner[ml]"

This separate example has 80 rows and related numeric/categorical predictors. It fills eight missing charges with a small forest and at most two iterations. No identifier or target column is included in this model example.

import pandas as pd
import freshdata as fd

training = pd.DataFrame(
    {
        "hours": [float(i % 12) for i in range(80)],
        "charges": [10 + 2 * (i % 12) + 0.5 * (i % 5) for i in range(80)],
        "kind": ["standard", "priority"] * 40,
    }
)
training.loc[training.index[::11], "charges"] = float("nan")

estimated, model_report = fd.clean(
    training,
    strategy="conservative",
    impute_strategy={"charges": "missforest"},
    missforest_n_estimators=10,
    missforest_max_iter=2,
    missforest_random_state=42,
    missforest_add_indicators=False,
    fix_dtypes=False,
    return_report=True,
    verbose=False,
)

assert estimated["charges"].notna().all()
for action in model_report.actions:
    if action.step == "impute" and action.column == "charges":
        print(action.count, action.metadata)
        assert action.count == 8

Inspect selected_model_type, iterations, converged, and fallback_reason in action metadata. With ML dependencies installed, this example reports selected_model_type="regressor" and fallback_reason=None. Reaching the iteration limit is recorded separately from convergence. Numerical estimates and model diagnostics can vary with dependency versions.

missforest_min_rows_for_model defaults to 50. Smaller inputs take a recorded simple-imputation fallback. Use the earlier five-row frame:

small, small_report = fd.clean(
    frame,
    strategy="conservative",
    impute_strategy={"age": "missforest"},
    id_columns=("record_key",),
    target_column="outcome",
    missforest_add_indicators=False,
    fix_dtypes=False,
    drop_empty_rows=False,
    drop_empty_columns=False,
    return_report=True,
    verbose=False,
)

assert small.loc["row-c", "age"] == 35.0
fallback_actions = [
    action for action in small_report.actions
    if action.step == "impute" and action.column == "age"
]
assert len(fallback_actions) == 1
print(fallback_actions[0].metadata["fallback_reason"])
assert "fewer rows" in fallback_actions[0].metadata["fallback_reason"]

Insufficient predictors and ineligible columns can also lead to fallback or preservation; inspect the audit rather than assuming a requested model ran. If a column is model-eligible but scikit-learn is absent, cleaning raises ImportError with the ML installation hint. No result or report is returned for that failed call; install freshdata-cleaner[ml] before retrying.

The following example protects a custom target whose input header is renamed. The declaration and imputation rule both use its post-normalization name:

raw = pd.DataFrame(
    {
        "Observed Hours": [float(i % 12) for i in range(80)],
        "Future Sales": [10 + 2 * (i % 12) + 0.5 * (i % 5) for i in range(80)],
    }
)
raw.loc[raw.index[::11], "Future Sales"] = float("nan")

protected, protected_report = fd.clean(
    raw,
    strategy="conservative",
    impute_strategy={"future_sales": "missforest"},
    target_column="future_sales",  # matches after header normalization
    fix_dtypes=False,
    return_report=True,
    verbose=False,
)

pd.testing.assert_series_equal(
    protected["future_sales"], raw["Future Sales"].rename("future_sales")
)
assert protected["future_sales"].isna().sum() == 8

With matching declarations, ID and target gaps are preserved. Role protection is not a train/test leakage guarantee: cleaning learns from the supplied frame. Isolate training data and validate your downstream workflow before estimating values in evaluation data.