Resampling and Frequency Conversion
Resampling converts a time series from one frequency to another, like turning tick data into 1-minute bars, and the direction of conversion determines whether you're summarizing data or inventing it.
Prerequisites: Rolling and Expanding Windows
Raw market data rarely arrives at the frequency you want to analyze it. Exchanges emit trade ticks at irregular, sub-millisecond intervals; a researcher usually wants clean 1-minute or daily bars. Converting between the two is called resampling, and it comes in two directions that behave completely differently.
Downsampling goes from high frequency to low frequency — ticks into 1-minute bars, minutes into days. This is a summarizing operation: you have more raw data than you need, so you aggregate it (take the last price, the sum of volume, the max and min). Upsampling goes the other way, low frequency to high — turning daily closes into hourly points. This is a fabricating operation: there is no real data at the new, finer timestamps, so you have to decide how to fill the gaps.
Downsampling aggregates real observations into fewer, coarser ones — information is genuinely lost but nothing is invented. Upsampling creates new timestamps that never had real data, so every value placed there is a synthetic fill choice, not a measurement.
Downsampling: OHLC bars from ticks
The canonical downsampling operation in finance is turning a stream of trade prices into open-high-low-close (OHLC) bars. For each output interval, you need the first price (open), the max price (high), the min price (low), the last price (close), and the summed volume.
Worked example
Trade prices arriving inside one minute, in order: 100.10, 100.15, 100.05, 100.20, 100.12. Downsampling to a 1-minute OHLC bar gives: open = 100.10 (first), high = 100.20 (max), low = 100.05 (min), close = 100.12 (last). If total traded size over those five prints was 4,200 shares, the bar's volume field is the sum: 4,200. In pandas, this is ticks["price"].resample("1min").ohlc() joined with ticks["size"].resample("1min").sum().
Now go the other way. Suppose you only have that one bar's close, 100.12, and you need an hourly series aligned to some other dataset. Upsampling from 1-minute to something finer, say 15 seconds, gives four new timestamps inside that minute with no real trades in them. Filling them with the last known value (resample("15s").ffill()) is a modeling assumption — "the price didn't move" — not a fact recovered from data.
What this means in practice
Downsampling is mostly safe: choosing sum vs. mean vs. last is a real decision, but you are not inventing information, only compressing it. Upsampling is where bugs hide, because the fill method silently encodes an assumption. Forward-filling a stale price into a gap can make a strategy look like it traded on information that did not exist yet, which is exactly the kind of point-in-time violation that inflates backtested returns.
Resampling with the wrong boundary convention is a classic silent bug: resample("1D") labels each daily bar by default with the start of the interval, but some data feeds expect the label at the end. Mismatched labeling can shift an entire series by one period, which quietly misaligns a signal against its execution price without ever throwing an error.
Related concepts
Practice in interviews
Further reading
- McKinney, Python for Data Analysis (ch. 11, 'Resampling')