Quant Memo
Core

Missing Data and Imputation

In finance a missing value is rarely an accident. It is usually a halt, a delisting, a failure to report or a stock nobody wanted to trade. Forward-filling deletes that message and replaces it with a fake calm that optimisers find irresistible.

Prerequisites: Point-in-Time Data

A NaN is a message. In most fields the message is "somebody forgot". In finance it is usually "something happened": the stock was halted, the company was acquired, the filing was late, or nobody wanted to trade it at any price all day.

That distinction matters because the standard reflexes — forward-fill, drop the row, fill with the column mean — all destroy the message and replace it with something reassuring. And the reassurance points the same way every time, because the reasons data goes missing in markets are overwhelmingly bad news.

Diagnose before you fill

Why it is missingWhat it actually meansReasonable handling
Illiquid, did not trade todayNo new information arrivedCarry the last price with a staleness flag; do not treat the 0% as an observation
Trading haltedMaterial news pending; a large move is comingExclude from the tradeable universe until it reopens; never fill
Delisted, acquired, bankruptThe position ended, usually badlyApply an explicit Delisting Returns value, then stop the series
Company not yet listedNothing is missingLeave empty. Never backfill
Feed outage or vendor gapThe value existed; you lost itCross-check a second source; if unresolved, mark the whole window unusable
Field not reported (no R&D line)Often a genuine zero, sometimes notDepends on the filing convention — check, do not guess
Fundamentals between reportsConstant by constructionForward-fill from the arrival date, which is correct here

The last row is the exception that shows the rule. Forward-filling quarterly fundamentals across the days between filings is right, because the underlying quantity really does not change. Forward-filling a price across a halt is wrong, because the underlying quantity is changing violently and you are recording that it did not.

Worked example: a minimum-variance book that was not

A minimum-variance portfolio over 500 US names. Two years of daily returns, sample covariance with mild shrinkage, long-only, 5% position cap, target volatility 9%.

The data has gaps — halts, thinly traded small caps, and a handful of names that stopped trading months before being formally delisted. The pipeline forward-fills prices, which turns every gap into a run of exact-zero returns. Thirty-eight names are affected.

Look at what the optimiser sees for those 38. Measured daily volatility 0.6% against a universe median of 1.6%. Measured correlation to the market 0.18 against a median of 0.55. On both criteria — low variance, low covariance — they are the best assets in the universe. They are, of course, the worst.

The optimiser puts 31% of the book into those 38 names, several at the 5% cap. Realised volatility of the resulting "minimum variance" portfolio: 19%, against the 9% target, with the overshoot arriving in a handful of days when the halted names reopened.

Verdict: nothing was wrong with the optimiser. It solved the problem it was handed exactly. The forward-fill wrote a false covariance matrix and the optimiser, doing its job, found and maximised the falsehood. This is the general shape of the failure: an optimiser is a machine for locating your worst data.

Missingness in markets is almost never random. It is caused by the same events you are trying to model, so any fill that makes the gap look ordinary biases the result in a predictable direction — lower measured risk, lower measured correlation, higher measured Sharpe. Name the mechanism before you choose the fill.

The other classic fills, and what each one costs

Column-mean or sector-median imputation. Filling a missing book-to-price with the sector median makes the stock look average. Companies with missing fundamentals are systematically not average — they are late filers, recent IPOs, or firms in distress. Worse, if the median is computed over the full sample you have also introduced Look-Ahead Bias, because the 2005 fill used 2020 data.

Dropping incomplete rows. Fine when missingness is unrelated to the outcome, which here it never is. Dropping every name with a gap systematically removes the failures, which is Survivorship Bias arriving through the back door of a dropna() call.

Interpolation. Linear interpolation across a gap uses the value at the end of the gap to fill its middle. That is a time machine, and it is the most common accidental look-ahead in time-series work.

The most expensive version of this bug is invisible because it does not create NaNs at all. Many vendors deliver a stale price rather than a null, so illiquid names arrive as a clean, gapless series with zeros in it. The dataset looks perfect. Count exact-zero daily returns per name: a liquid US large cap has essentially none over a year, so a name with thirty is not calm, it is not trading. See Stale Prices and Nonsynchronous Trading.

Keep two columns, always: the value and an is_filled boolean. It costs one bit and it lets you recompute any downstream statistic with fills excluded, which is the only way to know whether a result depends on them. Then add a daily monitor on missingness rate rather than count — a vendor silently changing a field's coverage from 98% to 74% is a far more common event than an outage, and far quieter.

Related concepts

Practice in interviews

Further reading

  • Little & Rubin, Statistical Analysis with Missing Data
  • CRSP Data Descriptions: Missing Prices and Delisting Codes
  • Lo & MacKinlay, An Econometric Analysis of Nonsynchronous Trading
ShareTwitterLinkedIn