The first time I tried to build a recession scorecard I did the dumb thing and just averaged the inputs. Yield spread in basis points, unemployment rate in percent, a couple of PMIs on their diffusion scale, some credit spread, a housing number. I threw them in a bucket, took a mean, and got a composite that moved almost entirely with whichever series had the biggest raw numbers. The spread was in the tens or low hundreds, the PMI sat around fifty, unemployment floated in the low single digits. Averaging those is like averaging your height in centimeters with your age in years and calling it a life score. The units are doing all the talking.
So the whole problem, before you get to anything clever, is that you cannot add or average things that live on different scales. You have to strip the units out first and put every gauge on the same footing. That is what normalization is for, and the workhorse is the z-score.
Z-scores put everything in units of surprise
A z-score answers one question for each series. How far is today's reading from its own normal, measured in its own standard deviations. You take the value, subtract the mean, divide by the standard deviation. A z of zero means the series is sitting exactly at its average. A z of plus two means it is unusually high by its own history, roughly a two standard deviation stretch. Minus one and a half means unusually low.
The nice thing is that once every input is a z-score, the units are gone. A yield spread at minus two z and a PMI at minus two z are now saying the same thing with the same loudness, that both are two standard deviations into territory they rarely visit. Now you can average them, or weight them, and the composite actually reflects breadth of stress rather than which series happened to carry big numbers.
The catch is the word normal. Normal relative to what window. If you compute the mean and standard deviation over the entire history of a series, you are measuring today against decades that may not resemble the current regime at all. A rate series from a high inflation era and a rate series from a zero rate decade do not share a mean in any useful sense. So most of the time you do not want a static z-score, you want a rolling one.
Rolling windows, and the tradeoff nobody mentions
A rolling z-score recomputes the mean and standard deviation over a trailing window, say the last few years of monthly data, and scores today against that. This keeps the reference point moving with the regime, so a spread that has been grinding tighter for years gets judged against its recent self rather than against some ancient average.
There is a real tradeoff in the window length and it is worth being honest about it.
- A short window, maybe a year or two, is responsive. It flags a sudden move quickly. It also gets fooled easily, because a couple of quiet months can shrink the standard deviation and make the next normal wiggle look like a huge z.
- A long window, five years or more, is stable and hard to spook. It also reacts slowly, and it can carry a stale idea of normal into a regime that has clearly changed.
I usually land somewhere in the three to five year range for monthly macro data and I try not to pretend that number is precise. It is a judgment call about how much you trust the recent past to define the present. One thing that helps is to keep the window the same across all seven inputs where you can, so a rate cut in one series and a jobs print in another are being judged over comparable spans. Mismatched windows are a quiet way to bias the composite toward whichever series has the twitchier reference.
One more detail that bites people. Some series need to be flipped before they mean anything as risk. A rising unemployment rate is bad, so a high positive z there is risk-on for recession. A rising PMI is good, so a high positive z there is risk-off. If you do not align the signs so that positive always means more recession risk, your composite will have inputs fighting each other and you will spend an afternoon confused about why the number will not move.
Fat tails, winsorizing, and percentile ranks
Macro series do not behave like textbook bell curves. They have fat tails and occasional prints that are genuinely absurd, sometimes real, sometimes a data revision or a reporting artifact. A single monster reading will inflate the standard deviation and quietly flatten every other z-score in the series, or it will produce a z of plus six that dominates the whole composite for a month. Neither is what you want.
Winsorizing is the blunt fix and it works. You cap each z-score at some limit, commonly around plus or minus three, so anything beyond that just gets clipped to the cap. You are not deleting the observation, you are refusing to let one outlier speak six times louder than everything else. The composite still registers stress, it just does not get hijacked.
If a series is so skewed that even winsorized z-scores feel wrong, the alternative is a percentile rank. Instead of asking how many standard deviations from the mean, you ask where today sits in the sorted history of the series, from zero to one. A reading in the ninety fifth percentile is high regardless of the shape of the distribution, and percentiles are immune to fat tails by construction because they only care about ordering, not magnitude. The cost is that you lose the sense of how extreme the extreme is. The ninety ninth and the hundredth percentile look nearly identical even if the actual gap is enormous. For most scorecard work I lean on winsorized rolling z-scores and keep percentile ranks in my pocket for the two or three series that are hopelessly skewed.
A workflow that survives contact with real data
Here is the sequence I actually follow when I collapse a handful of unlike gauges into one comparable risk number.
- Line up all series on the same calendar and frequency. Resample everything to monthly if some are weekly or daily, and decide upfront how you handle a series that reports late so you are not silently comparing this month to last month.
- Flip signs so that for every input, higher means more recession risk. Write the direction down next to each series so future you can audit it.
- Compute a rolling z-score per series over a shared trailing window. Same window everywhere unless a series genuinely demands otherwise.
- Winsorize each z-score to about plus or minus three. Swap in a percentile rank for any series too skewed to trust.
- Combine into one composite, usually a weighted average. If you have no strong prior, equal weight is an honest default and it is harder to overfit than weights you tuned to look good on the last cycle.
The failure mode to watch for is a composite that looks calm only because one input is masking another. If your credit spread is screaming at plus two and a half while a lagging housing series sits at minus one, the average looks reassuring and it is lying to you. So I never look at only the composite. I keep the individual z-scores visible next to it, because the spread of the inputs tells you whether the stress is broad or just one gauge having a moment. This is roughly how the market scorecards inside Blockcircle are built, one comparable number on top, the component reads still exposed underneath so you can see what is actually driving it.
None of this makes a scorecard predictive on its own. Normalization does not add information, it just stops the units from destroying the information you already have. Get that part right and the composite becomes something you can reason about. Skip it and you are averaging centimeters with years and wondering why the number never says anything useful.