Skip to content

How to calculate a time-based rolling mean in Pandas (with Code)

The Short Answer (TL;DR)

Use rolling with a fixed time span such as "2h" on a sorted DatetimeIndex to average observations in a trailing interval. State the boundary and minimum-observation policies, because a time window contains a variable number of observations and is not the same as a fixed row count.

import pandas as pd
s = pd.Series([2.,4.,10.],index=pd.to_datetime(["2026-11-01T00:00Z","2026-11-01T01:00Z","2026-11-01T02:00Z"],utc=True))
print(s.rolling("2h",closed="right",min_periods=2).mean())

Detailed Implementation

This example uses synthetic data and does not need a network connection. Install the recorded package versions in a separate Python environment before running it.

python -m pip install numpy==2.5.4 pandas==3.0.6
from pathlib import Path
import numpy as np
import pandas as pd

def trailing_two_hour_mean(series,min_observed=2):
    if not isinstance(series.index,pd.DatetimeIndex) or series.index.tz is None or series.index.hasnans:
        raise ValueError("Use a timezone-aware DatetimeIndex without missing timestamps.")
    if not series.index.is_unique:
        raise ValueError("Resolve duplicate instants before applying this example's policy.")
    if isinstance(min_observed,bool) or not isinstance(min_observed,int) or min_observed<1:
        raise ValueError("The minimum observed count must be a positive integer.")
    if not pd.api.types.is_numeric_dtype(series) or pd.api.types.is_bool_dtype(series):
        raise ValueError("Use numeric measurements.")
    if not np.isfinite(series.dropna().to_numpy(dtype=float)).all():
        raise ValueError("Observed values must be finite.")
    ordered = series.sort_index()
    means = ordered.rolling("2h",closed="right",min_periods=min_observed).mean()
    counts = ordered.rolling("2h",closed="right",min_periods=0).count().astype("int64")
    return pd.DataFrame({"value":ordered,"observed_in_window":counts,"rolling_mean":means})

instants = pd.to_datetime(["2026-11-01T00:00Z","2026-11-01T00:30Z","2026-11-01T01:00Z",
    "2026-11-01T02:00Z","2026-11-01T02:30Z","2026-11-01T04:30Z"],utc=True)
series = pd.Series(pd.array([2,None,4,10,8,20],dtype="Float64"),index=instants,name="reading")
result = trailing_two_hour_mean(series)
report = result.to_string(na_rep="NA") + "\n"
Path("trailing-time-mean.txt").write_text(report,encoding="utf-8")
print(report)

Observed window counts are 1, 1, 2, 2, 3 and 1. Available means are 3 at 01:00, 7 at 02:00 and 22/3 at 02:30; the other three estimates remain missing under the minimum of two observed readings.

Which observations enter each window?

At timestamp t, the example includes observed values later than t minus two elapsed hours and no later than t. The current value is included, while a value exactly two hours old is excluded. At 02:00, the 00:00 reading of two has left the window, leaving four and ten with mean seven. At 02:30, four, ten and eight give 22/3. The independent validation checks membership directly for every output timestamp.

Why is this different from rolling over two rows?

The source readings are irregularly spaced. The two-hour intervals contain one, one, two, two, three and one observed values. A rolling(2) calculation instead uses a row-based window and would answer a different question, especially across the two-hour gap before 04:30. The fixed string "2h" describes an elapsed duration, not two calendar-hour labels or a business-calendar period.

What happens when there are too few observed values?

The explicit minimum is two nonmissing observations. Missing readings do not contribute to the mean denominator. The first two estimates and the last estimate are therefore missing. A separate zero-minimum count calculation makes coverage visible even when the mean is suppressed. Raising the minimum to three leaves only the 02:30 estimate available in this synthetic series; lowering it changes the reporting policy rather than recovering lost data.

Why sort timestamps and reject duplicate instants?

The helper sorts a copy into chronological order and returns that order. It retains the original values and never changes the input. Its application policy rejects duplicate instants so that a timestamp defines one reading; another application could deliberately retain repeated events with a documented weighting rule. Missing timestamps, timezone-naive indexes, nonnumeric data and infinite observed measurements are also rejected. All instants here are UTC, so no ambiguous local-time conversion is needed.

Is the mean weighted by elapsed time?

Each observed reading receives equal weight in its included window. A burst of dense readings can therefore dominate an observation mean even if it covers little elapsed time. A time-weighted mean would require a separate model for how values evolve between readings, such as explicitly justified stepwise or interpolated intervals. These trailing estimates use no future readings, but they are descriptive smoothing results and do not by themselves establish predictive accuracy, causal effects or measurement completeness.

Mathematical Context

For each output instant t, define O(t) as the observations with t minus two hours < t_i <= t and an observed x_i. The mean is the sum of those values divided by their observed count, provided that count meets the stated minimum of two. The result has the original measurement units. The denominator changes as irregular observations enter and leave the time interval. At 04:30 the 02:30 reading lies exactly on the excluded boundary, leaving only one observed value, so no mean is reported under the two-observation policy.

\[ \bar{x}(t)=\frac{1}{|O(t)|}\sum_{i\in O(t)}x_i,\quad O(t)=\{i:t-2\mathrm{h}<t_i\leq t,\ x_i\ \mathrm{observed}\} \]

Reproducibility

Executed with Python 3.12.14; numpy 2.5.4, pandas 3.0.6. Automated checks confirm the stated output properties.

Primary documentation

Related guides