← All Posts
Deep Learning · Data Preparation and Preprocessing · Handling Missing Data

Mean, Median, and Mode Imputation

Simple imputation stores one replacement value per column, learned from the observed training entries. When a later row has a gap, it receives that stored value. Observed entries are left unchanged.

This is a useful predictive baseline because the transformation is cheap and easy to inspect. It does not recover the original missing values, and a filled table does not carry the same statistical information as a fully observed one.

Work through one column before choosing a summary

Take the training column [1, 2, missing, 4, 100, missing]. There are four observed values. The mean adds those four and divides by four; the missing entries do not count as zero. For the median, sort the observed values and average the two middle ones when the count is even:

$$\mathrm{mean}=\frac{1+2+4+100}{4}=26.75,\qquad \mathrm{median}=\frac{2+4}{2}=3.$$

The value 100 pulls the mean upward. The median remains between 2 and 4. That does not tell us that 100 is an error; it explains how the summaries respond to a large observation.

Change the largest observed training value

Observed values: 1, 2, 4, 100.

Mean fill: 26.751, 2, 26.75, 4, 100, 26.75
Median fill: 31, 2, 3, 4, 100, 3
Both gaps receive the same fitted value. The median stays 3 while the largest value remains at least 4.
Each slider position represents a different training column. With the other values fixed, the mean changes as the largest observation moves; the two middle values defining the median stay the same.

For the observed sample, the mean minimizes total squared error among constant predictions; the median minimizes total absolute error. This helps explain their behavior, but the best fill for a downstream model still needs to be evaluated as part of that model’s complete pipeline.

For categories, decide what a replacement means

The mode is the most frequent observed category. For [Basic, Pro, Basic, missing], mode imputation fills Basic. This increases the count of an already common category; it is not evidence that the missing account truly belongs to it.

If several categories tie, record a deterministic tie rule. Scikit-learn’s most_frequent strategy chooses the smallest tied value. A dedicated category such as Missing is another legitimate design, covered in constant imputation. It preserves a distinction between observed categories and absent information.

Do not average arbitrary integer category codes: the average of code 1 and code 3 need not represent category 2. Also distinguish a missing entry from a previously unseen but observed category; categorical encoding handles the latter problem separately.

A repeated fill changes the distribution

For a smaller example, the observed values 1, 3, and 5 have mean 3. Their squared deviations from 3 are 4, 0, and 4, totaling 8. Fill two gaps with 3, and the two added deviations are both zero:

01Observed entries: 1, 3, 5Three entries; average squared deviation from 3 is 8 / 3 ≈ 2.67.
02After fill: 1, 3, 5, 3, 3Five entries; the same squared-deviation total divided by five is 8 / 5 = 1.60.
The spread shrinks without learning the missing values. We use variance with denominator equal to the number of entries here. This comparison describes the observed and filled columns; it does not reveal the variance of the unknown complete data.

Single-value filling creates a concentration at the replacement value and can distort relationships with other columns. A downstream predictor may still use that representation effectively, but treating the filled values as measured observations for standard errors or scientific inference can understate uncertainty. The multiple-imputation chapter addresses that different objective.

Fit once on training data, then transform

For our original training column, store the median 3. If a test batch is [missing, 1000], transform it to [3, 1000]. The new value 1000 does not update the stored median, and it is not itself replaced.

In cross-validation, each training fold gets its own fitted imputer. Put the imputer inside the pipeline passed to the splitter or search. The shared-fold tuning example explains the boundary; the same rule applies to learned medians and modes.

Python: inspect the fitted median and transformed values
import numpy as np
from sklearn.impute import SimpleImputer

train = np.array([[1.], [2.], [np.nan], [4.], [100.], [np.nan]])
test = np.array([[np.nan], [1000.]])
imputer = SimpleImputer(strategy="median")
imputer.fit(train)
assert imputer.statistics_.tolist() == [3.0]
assert imputer.transform(test).ravel().tolist() == [3.0, 1000.0]
print("Filled training column:", imputer.transform(train).ravel())
print("Filled test column:", imputer.transform(test).ravel())

Decide what to do with an all-missing training column

No mean, median, or mode can be estimated when the training column has no observed entries. Choose a policy: remove the feature with a recorded schema change, preserve it using an explicit constant, or reject the fit and investigate the collection problem.

Scikit-learn exposes this choice through keep_empty_features. In its current API, retaining an empty feature with a nonconstant strategy uses 0 as the replacement; that zero is a fallback, not a learned median. Decide whether that representation is appropriate and preserve the same feature ordering at serving time.

The C++ implementation below instead raises an error for an all-missing column. This avoids indexing an empty vector or silently assigning a statistic that the data cannot support.

C++: separate the learned statistic from filling

Estimate the median from present training values, then pass that stored value to every later transformation. The sample also demonstrates the all-missing error path.

Compile the median imputer
#include <algorithm>
#include <cmath>
#include <iostream>
#include <limits>
#include <stdexcept>
#include <vector>

double columnMedian(const std::vector<double>& column) {
    std::vector<double> observed;
    for (double x : column) {
        if (std::isnan(x)) continue;
        if (!std::isfinite(x)) throw std::invalid_argument("nonfinite value");
        observed.push_back(x);
    }
    if (observed.empty())
        throw std::domain_error("no observed values: choose a fallback policy");
    std::sort(observed.begin(), observed.end());
    const std::size_t n = observed.size();
    if (n % 2) return observed[n / 2];
    return observed[n / 2 - 1] / 2.0 + observed[n / 2] / 2.0;
}

void fillMissing(std::vector<double>& column, double fittedValue) {
    for (double& x : column) if (std::isnan(x)) x = fittedValue;
}

int main() {
    const double NA = std::numeric_limits<double>::quiet_NaN();
    std::vector<double> train = {1, 2, NA, 4, 100, NA};
    std::vector<double> test = {NA, 1000};
    const double fittedMedian = columnMedian(train);
    fillMissing(train, fittedMedian);
    fillMissing(test, fittedMedian);
    std::cout << "stored median: " << fittedMedian << "\n";
    std::cout << "test: " << test[0] << " " << test[1] << "\n";
    try { columnMedian({NA, NA}); }
    catch (const std::domain_error&) {
        std::cout << "all-missing column: explicit fallback required\n";
    }
    return 0;
}
stored median: 3
test: 3 1000
all-missing column: explicit fallback required

Judge the baseline by the job it must do

Use simple filling when you need a transparent first predictive pipeline, then compare alternatives on the same held-out examples. Inspect errors separately for complete and incomplete inputs, and for important groups. A global score can hide a poor result on the rows most affected by the imputer.

When other features carry useful information about the absent value, KNN or iterative imputation can model that structure. More complicated filling is not guaranteed to improve the downstream predictor; assess the full recipe rather than how plausible the filled table looks.

A missingness indicator can retain the distinction between an observed median-valued entry and an imputed one. Its usefulness depends on whether that missingness pattern remains informative at deployment. The mechanism chapter explains why it does not establish that the missing values have been recovered.

References and further reading