Tabular model data often contains blanks from optional fields, delayed measurements, or partially collected records. In scikit-learn, those gaps need to be converted into feature values before most estimators can fit or predict without rejecting the input matrix.
SimpleImputer learns replacement values during fit() and applies them during transform(). Numeric columns can use a median or mean, while categorical columns usually use the most frequent value or a fixed category before encoding.
A mixed table should keep numeric and categorical imputers in separate ColumnTransformer branches. Placing that transformer inside a Pipeline keeps imputation fitted on training rows only, then reuses the same fill values when holdout rows or future records reach the estimator.
Steps to impute missing values with scikit-learn:
- Create impute_missing_values.py with separate imputers for numeric and categorical columns.
- impute_missing_values.py
import numpy as np import pandas as pd import sklearn from sklearn.compose import ColumnTransformer from sklearn.impute import SimpleImputer from sklearn.linear_model import LogisticRegression from sklearn.pipeline import Pipeline from sklearn.preprocessing import OneHotEncoder numeric_features = ["age", "monthly_spend"] categorical_features = ["plan"] train = pd.DataFrame( { "age": [25, 32, np.nan, 45, 51, 28, np.nan, 39], "monthly_spend": [120, 210, 180, np.nan, 320, 150, 170, 260], "plan": [ "basic", "premium", "basic", np.nan, "premium", "basic", "standard", np.nan, ], "converted": [0, 1, 0, 1, 1, 0, 0, 1], } ) holdout = pd.DataFrame( { "age": [np.nan, 42], "monthly_spend": [190, np.nan], "plan": [np.nan, "premium"], } ) numeric_imputer = SimpleImputer(strategy="median") categorical_transformer = Pipeline( steps=[ ("imputer", SimpleImputer(strategy="most_frequent")), ("encoder", OneHotEncoder(handle_unknown="ignore", sparse_output=False)), ] ) preprocessor = ColumnTransformer( transformers=[ ("numeric", numeric_imputer, numeric_features), ("categorical", categorical_transformer, categorical_features), ], verbose_feature_names_out=False, ) model = Pipeline( steps=[ ("preprocess", preprocessor), ("classifier", LogisticRegression(max_iter=1000, random_state=0)), ] ) X_train = train[numeric_features + categorical_features] y_train = train["converted"] model.fit(X_train, y_train) fitted_preprocessor = model.named_steps["preprocess"] fitted_numeric = fitted_preprocessor.named_transformers_["numeric"] fitted_categorical = fitted_preprocessor.named_transformers_["categorical"] fitted_category_imputer = fitted_categorical.named_steps["imputer"] transformed_train = fitted_preprocessor.transform(X_train) transformed_holdout = fitted_preprocessor.transform(holdout) predictions = model.predict(holdout) print(f"scikit-learn {sklearn.__version__}") print("Input missing values:") print(X_train.isna().sum().to_string()) print() print("Numeric fill values:") for name, value in zip(numeric_features, fitted_numeric.statistics_): print(f"- {name}: {value:.1f}") print() print("Categorical fill values:") for name, value in zip(categorical_features, fitted_category_imputer.statistics_): print(f"- {name}: {value}") print() print("Transformed feature names:") print(", ".join(fitted_preprocessor.get_feature_names_out())) print() print( "Transformed training shape: " f"{transformed_train.shape[0]} rows x {transformed_train.shape[1]} columns" ) print(f"Missing values after transform: {int(np.isnan(transformed_train).sum())}") print() print("Transformed holdout rows:") holdout_table = pd.DataFrame( transformed_holdout, columns=fitted_preprocessor.get_feature_names_out(), ).round(2) print(holdout_table.to_string(index=False)) print(f"Holdout predictions: {predictions.tolist()}")
The numeric branch fills age and monthly_spend before model fitting. The categorical branch fills plan before OneHotEncoder expands it into binary features.
- Run the imputation smoke script.
$ python impute_missing_values.py scikit-learn 1.9.0 Input missing values: age 2 monthly_spend 1 plan 2 Numeric fill values: - age: 35.5 - monthly_spend: 180.0 Categorical fill values: - plan: basic Transformed feature names: age, monthly_spend, plan_basic, plan_premium, plan_standard Transformed training shape: 8 rows x 5 columns Missing values after transform: 0 Transformed holdout rows: age monthly_spend plan_basic plan_premium plan_standard 35.5 190.0 1.0 0.0 0.0 42.0 180.0 0.0 1.0 0.0 Holdout predictions: [1, 1]
- Confirm the training input contains missing values before imputation.
Input missing values: age 2 monthly_spend 1 plan 2
- Check the fitted imputer statistics.
Numeric fill values: - age: 35.5 - monthly_spend: 180.0 Categorical fill values: - plan: basic
The medians and most frequent category are learned during fit(). Fit the pipeline on training rows only so validation or production rows do not change these values.
- Verify the transformed matrix contains no missing values.
Transformed feature names: age, monthly_spend, plan_basic, plan_premium, plan_standard Transformed training shape: 8 rows x 5 columns Missing values after transform: 0
If missingness is itself a model signal, set add_indicator=True on the relevant imputer or add a MissingIndicator branch. Indicators are created for features that contain missing values during fit().
- Confirm prediction accepts holdout rows that still contain missing input values.
Transformed holdout rows: age monthly_spend plan_basic plan_premium plan_standard 35.5 190.0 1.0 0.0 0.0 42.0 180.0 0.0 1.0 0.0 Holdout predictions: [1, 1]
- Remove the scratch script after the smoke test.
$ rm impute_missing_values.py
Mohd Shakir Zakaria is a cloud architect with deep roots in software development and open-source advocacy. Certified in AWS, Red Hat, VMware, ITIL, and Linux, he specializes in designing and managing robust cloud and on-premises infrastructures.