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

Backward Fill

Backward fill copies the next observed value into an earlier gap. The algorithm is useful when a later observation is an acceptable source for the task. Its name describes the direction of copying, not evidence that the value really held earlier.

The distinction from forward fill is the source: an earlier observation versus a later one. We will keep the mechanics short and focus on what that later source means for reconstruction and prediction.

Trace each replacement to its next observation

Consider [missing, 3, missing, missing, 5, missing] at minutes 0 through 5. Backward fill returns [3, 3, 5, 5, 5, missing]. The first gap borrows from minute 1; the two interior gaps borrow from minute 4. The final gap has no later observation.

Click a time to see where its value came from
Minute 2 borrows 5 from minute 4: a two-minute lookahead. That reading was unavailable at minute 2.
Dashed outline: selected time. Solid green border: the observed source, if one exists. Assume measurements become available at their displayed timestamps. This figure uses unlimited backward fill.

Scanning the array from right to left makes this easy to implement: remember the next real observation, then copy it into earlier gaps. An all-missing series stays missing, as does any trailing stretch after the last observation.

A reconstruction needs a reason to extend values backward

Suppose a device’s setting is 3 at minute 1 and 5 at minute 4. Those two readings alone do not tell us when it changed. It might have changed at minute 2, at minute 4, or moved through other values between the observations.

01Changed earlyThe device may already have been at 5 during both missing minutes.
02Changed lateIt may have stayed at 3 until the minute-4 observation.
03Changed graduallyA continuously varying measurement may have taken intermediate values.
The same two observations allow different histories. Backward fill selects the first interpretation for the gaps. The endpoints alone do not establish that it is correct.

An explicit validity interval is stronger evidence. A record received at 09:10 might state that a setting has been valid since 09:00. A retrospective state table can use that interval, but should not extend it to 08:55 merely because an earlier cell is empty. Prefer interval-aware joins when that is the source contract.

For a descriptive plot, a backward-filled step function may be an acceptable visual convention. Mark the filled regions so readers can distinguish observations from reconstruction. Using the final dataset offline does not make arbitrary fills appropriate for estimating trends or uncertainty.

The cutoff is when the prediction was made

If a prediction at minute 2 uses a value first measured at minute 4, its feature contains future information. This can inflate a backtest even when both timestamps fall inside the training period. A train/test split does not repair information leaking between rows within a split.

The relevant question is whether the source was available at the decision cutoff. A schedule published yesterday for an event tomorrow can be a legitimate feature today; an event’s realized value measured tomorrow cannot. A future effective date and future availability are different things. The forward-fill chapter’s arrival-time example covers the complementary late-record case.

If the task deliberately waits until minute 4 to estimate what happened at minute 2, then future observations are available to that delayed reconstruction. Evaluate it as such, with the same delay in validation and deployment. Do not describe it as an immediate minute-2 forecast.

Bound the lookahead and keep groups separate

Even for retrospective work, a distant successor can be a poor substitute. A maximum lookahead rejects sources too far after the gap. Pandas’ bfill(limit=1) limits the number of consecutive missing rows filled before an observation; it does not impose one minute of lookahead on irregular timestamps.

Python: a one-minute limit within each device

This offline example keeps the next source timestamp. Unlike the unlimited figure, minute 2 remains missing because its source at minute 4 is two minutes later. Device B has no observations, so it remains entirely missing.

import numpy as np
import pandas as pd

df = pd.DataFrame({
    "device": ["A"] * 6 + ["B"] * 2,
    "minute": [0, 1, 2, 3, 4, 5, 0, 1],
    "value": [np.nan, 3, np.nan, np.nan, 5, np.nan, np.nan, np.nan],
}).sort_values(["device", "minute"])
assert not df.duplicated(["device", "minute"]).any()

observed_at = df["minute"].where(df["value"].notna())
source_time = observed_at.groupby(df["device"]).bfill()
df["lookahead"] = source_time - df["minute"]
candidate = df.groupby("device")["value"].bfill()
# Offline reconstruction policy: permit a source at most one minute later.
df["filled"] = candidate.where(df["lookahead"] <= 1)
assert np.allclose(df.loc[df.device == "A", "filled"],
                   [3, 3, np.nan, 5, 5, np.nan], equal_nan=True)
assert df.loc[df.device == "B", "filled"].isna().all()
print(df.loc[df.device == "A", "filled"].tolist())
[3.0, 3.0, nan, 5.0, 5.0, nan]

Sort within entities and resolve duplicate timestamps before filling. Applying a global fill after a grouped operation can accidentally cross entity boundaries; keep each directional pass grouped. The combined-fill chapter shows why the order of those passes also matters.

Leave a gap when the source does not justify a fill

Backward fill is a reasonable candidate for a deliberately retrospective representation when the next value is a defensible stand-in. Compare it with preserving gaps or using an interpolation model when the quantity is continuous and both endpoints are available.

It is not a general solution for the leading gaps left by forward fill. In a live predictor, use available history, an explicit missing-input policy, or a delayed decision if the application permits it. In a statistical analysis, keep track of how much of a reported pattern depends on carried values.

C++: the reverse scan

This version demonstrates unlimited backward fill on one already ordered series of finite values or NaNs. It returns a copy, leaving the raw data unchanged. The reverse loop safely handles an empty vector; each element is visited once, so the scan takes linear time.

Compile the backward fill
#include <cmath>
#include <iostream>
#include <limits>
#include <vector>

std::vector<double> backwardFill(const std::vector<double>& raw) {
    auto filled = raw;
    double next = std::numeric_limits<double>::quiet_NaN();
    // This reverse-loop form also handles an empty vector.
    for (std::size_t i = raw.size(); i > 0;) {
        --i;
        if (!std::isnan(raw[i])) next = raw[i];
        else if (!std::isnan(next)) filled[i] = next;
    }
    return filled;
}

int main() {
    const double NA = std::numeric_limits<double>::quiet_NaN();
    const auto filled = backwardFill({NA, 3, NA, NA, 5, NA});
    for (std::size_t i = 0; i < filled.size(); ++i) {
        if (i) std::cout << " ";
        if (std::isnan(filled[i])) std::cout << "NA";
        else std::cout << filled[i];
    }
    std::cout << "\n";
}
3 3 5 5 5 NA

References and further reading