Statistics for Traders #25: Semi-partial (part) correlation on today's slot-1 CAD Retail × CADJPY sample — raw surprise_z adds only 0.7 percentage points of R² over the 5-bucket assignment (0.02 pp if the COVID outlier drops)
Semi-partial (or “part”) correlation is the sibling of Stats #23’s partial correlation: partial removes Z from BOTH X and Y, semi-partial removes Z from X ONLY. Its distinguishing property: r_Y(X-Z)² is exactly the ΔR² added by X over Z when both predict Y. On today’s slot-1 CAD Retail × CADJPY 15m sample (n=197), semi-partial r_Y(X-Z_ord) = -0.082, squared 0.68 percent. Bucket_ord already explains 17.6% of move-pips variance; adding raw surprise_z lifts the two-predictor R² by 0.68 percentage points.
Drop the 2020-06-19 COVID Q2 print (z = -20.42, +6.6p move) and the semi-partial collapses further to +0.013, ΔR² = 0.02pp. Buckets carry the signal on this event; surprise magnitude beyond bucket adds essentially nothing.
![Horizontal-bar chart with Fisher 95% CI whiskers. Top row: 'r(X,Y) overall' = +0.209 (or +0.392 without outlier) — solid positive Pearson r drawn in dark. Middle rows: 'Partial r_XY|Z_ord (n=197)' = -0.091, CI [-0.228, +0.050]; 'Semi-partial r_Y(X-Z_ord) (n=197)' = -0.082, CI [-0.220, +0.058]; 'Partial r_XY|Z_ord (n=196, no COVID)' = +0.014; 'Semi-partial r_Y(X-Z_ord) (n=196, no COVID)' = +0.013 — all coral, all crossing the r=0 reference line. Bottom rows: ΔR² decomposition of Y ~ X + Z_ord: R²(Y~Z_ord) 17.60%, R²(Y~X) 4.39%, R²(Y~X+Z_ord) 18.27% — the ΔR² added by X over Z_ord is 0.68 percentage points (matches semi-partial squared exactly). Coral vertical line at r=0 makes the semi-partial-crosses-zero result visible at a glance.](/insights/stats-for-traders-semi-partial-correlation/comparison.png)
The formula and how it differs from partial
Both partial and semi-partial correlations use the same numerator:
Numerator = r_XY − r_XZ · r_YZ
They differ in denominator only:
| Statistic | Denominator | Reading |
|---|---|---|
| Partial r_XY|Z | sqrt((1 − r_XZ²)(1 − r_YZ²)) | correlation of residuals; within-strata reading |
| Semi-partial r_Y(X-Z) | sqrt(1 − r_XZ²) | correlation of X-residual with raw Y; ΔR² reading |
Since sqrt(1 − r_XZ²) > sqrt((1 − r_XZ²)(1 − r_YZ²)) whenever |r_YZ| > 0, semi-partial is always closer to zero (smaller in magnitude) than partial. They agree in sign because the numerator is shared. Both collapse to r_XY when Z is uncorrelated with both X and Y.
The key property: semi-partial² is ΔR²
If you regress Y on Z alone and then on both X and Z:
r_Y(X-Z)² = R²(Y ~ X + Z) − R²(Y ~ Z)
That’s the reason semi-partial is the right statistic for the question “does adding X to my model actually help, given I already have Z?”. It maps directly to raw variance-explained. Partial correlation squared has a different interpretation — it’s the coefficient of partial determination, the fraction of RESIDUAL Y-variance (after subtracting Z’s contribution) explained by the residual of X. Semi-partial keeps the full Y-variance denominator; that’s what makes it the ΔR² sibling.
Worked example on today’s slot-1 sample
| n | 197 |
| r(X = surprise_z, Y = move_pips) | +0.2095 |
| r(X, Z_ord) | +0.6488 |
| r(Y, Z_ord) | +0.4195 |
| r_XY − r_XZ·r_YZ (numerator) | −0.0627 |
| sqrt(1 − 0.6488²) (semi-partial denom) | 0.7610 |
| Semi-partial r_Y(X-Z_ord) | −0.0822 |
| Semi-partial squared = ΔR² | 0.00676 (0.68 pp) |
| Fisher 95% CI (dof = n − 1 − 2 = 194) | [−0.220, +0.058] |
Cross-check via two regressions (sklearn LinearRegression):
| R²(Y ~ X only) | 0.0439 |
| R²(Y ~ Z_ord only) | 0.1760 |
| R²(Y ~ X + Z_ord) | 0.1827 |
| ΔR² of X over Z_ord | 0.00676 |
The ΔR² matches the semi-partial squared to 5 decimals. That’s the identity — semi-partial correlation IS the square root of ΔR² (sign preserved from the numerator).
The COVID Q2 outlier and its sensitivity
The r(X, Z_ord) = 0.6488here is meaningfully lower than the ~0.90 you’d expect for a 5-quantile-based bucket variable. The 2026-06-19 COVID Q2 print explains it: actual Canada Retail Sales −26.4% MoM vs consensus −15.0%, surprise_z = −20.42. That’s 10x the magnitude of any other big_miss in the sample. Ordinal bucket encoding assigns it Z = −2, identical to a big_miss at z = −1.6, but the raw z carries 10x the leverage. That single point drags r(X, Z_ord) down by 25 percentage points.
Drop it and redo:
| Metric | n = 197 (with) | n = 196 (without) |
|---|---|---|
| r(X, Y) | +0.210 | +0.392 |
| r(X, Z_ord) | +0.649 | +0.903 |
| r(Y, Z_ord) | +0.420 | +0.429 |
| Semi-partial r_Y(X-Z_ord) | −0.082 | +0.013 |
| Semi-partial² = ΔR² | 0.68 pp | 0.02 pp |
Two features of the sensitivity check: (1) the sign of the semi- partial flips from negative (with COVID) to positive (without), and (2) the magnitude collapses toward zero either way — the pedagogic point survives. On a leverage-heavy sample, a single outlier can flip semi-partial’s sign while leaving the qualitative reading (“X adds ~zero unique variance beyond Z”) unchanged. Both Fisher CIs cross zero.
The categorical (4-dummy) semi-partial
Replace Z_ord (a single ordinal) with Z_cat (four dummies for the five buckets, with in_line as reference), and the same machinery gives ΔR² over a stronger baseline:
| R²(Y ~ Z_cat) | 0.1997 |
| R²(Y ~ X + Z_cat) | 0.2022 |
| ΔR² of X over Z_cat | 0.00252 (0.25 pp) |
| Semi-partial r_Y(X-Z_cat) | -0.0502 |
Even smaller than the ordinal version (0.25 pp vs 0.68 pp), for the right reason: Z_cat is a stronger baseline (5 bucket means fitted vs 1 slope), leaving less unique variance for X to explain. As always, the categorical is the more conservative choice for asking “does X buy me anything beyond bucket?”.
Cross-links to Stats #23 and Stats #24
Stats #23 introduced partial correlation on the CAD CPI × USDCAD sample and got a partial r of −0.05 (Fisher CI crosses zero). The semi-partial on THAT same sample would be r_Y(X-Z_ord) = (−0.4256 − 0.9488·(−0.4327)) / sqrt(1 − 0.9488²) = −0.0151 / 0.3162 = −0.0478, squared 0.23 pp — telling the same story (X adds ~zero unique variance beyond Z) via a slightly different denominator. The two statistics are close in absolute value here because |r_YZ| = 0.43 makes the two denominators similar in size.
Stats #24 introduced one-way ANOVA and R² on the same USDCAD sample and got η² = R²(Y ~ Z_cat) = 0.1952. Adding X to that model would give ΔR² of exactly the categorical semi-partial squared for that sample. The three installments are three views of the same underlying decomposition: partial (Stats #23) answers “is there a within-strata correlation?”; ANOVA / η² (Stats #24) answers “what fraction of Y-variance is between vs within?”; today’s semi-partial answers “does the fine-grained predictor buy me anything the coarse one doesn’t?”.
Verification note
All r values computed in a scratch Python script against the 197-release sample from /api/v1/news-impact/releases?event=FF:CAD_RETAIL_MOM&instrument=CADJPY&window=15m&limit=500 on 2026-08-25. Cross-verified against sklearn.linear_model.LinearRegression for the ΔR² identity (semi-partial squared = ΔR² of X over Z), matching to 5 decimal places. Chart written via a one-off script reusing scripts/insights-charts/svg.ts and theme.ts primitives; not committed under scripts/ since the horizontal-bar-with-whiskers layout is single-use.