Correlation Field Guide
A coefficient is a summary, and every summary throws something away. Which correlation measure sees which kind of dependence, and what to actually do once two features turn out to carry the same information.
Every figure on this page is computed in your browser from a seeded generator: nothing is pre-computed, nothing is sent anywhere.
Contents
Part one
Measuring dependence
Every coefficient below answers a slightly different question. Pearson asks how well a straight line fits. Spearman asks whether the ordering agrees. Distance correlation asks whether the two variables are independent at all. Give them the same data and they will disagree — that disagreement is itself the diagnostic.
Five measures, one sample
Pick a shape. All five coefficients are recomputed on the same 200 points, so you can watch them come apart. Switch to ranks to see the plot exactly as Spearman and Kendall see it — the values are replaced by their positions in the ordering.
n = 200, drawn from a
seeded generator so every reading is reproducible. Mutual information uses 7
equal-frequency bins per axis, normalised by
√H(X)H(Y).
The two unsigned measures are biased upward at finite n: on
genuinely independent data at this sample size, distance correlation still averages
0.12 and normalised MI 0.05. The baselines quoted on the tiles are the 95th
percentiles under independence, measured by simulation — a reading below them means
nothing found, not weak dependence.
Choosing by the type of the two variables
Most of the choice is settled before you look at the data at all: the measurement scales of the two variables rule out most of the options.
| Variable A | Variable B | Default choice | Reach for instead when |
|---|---|---|---|
| Continuous | Continuous | Pearson r | Heavy tails, outliers, or a curved-but-monotonic relation → Spearman |
| Ordinal | Ordinal | Kendall τb | Few ties and n comfortably above ~50 → Spearman reads more familiarly |
| Continuous | Binary | Point-biserial rpb | The binary split is very unbalanced → report the group means and an effect size |
| Binary | Binary | Phi φ | You want an interpretable ratio → odds ratio or lift |
| Nominal (k levels) | Nominal (m levels) | Cramér's V | Many rare levels — V is biased upward → use the bias-corrected variant |
| Continuous | Nominal (k levels) | Correlation ratio η² | Strongly non-normal groups → ε² from a Kruskal–Wallis test |
| Anything | Anything | Distance correlation | You want to see where the dependence lives → the binned joint table behind mutual information |
| Two variables, with a third to hold fixed | Partial correlation rxy·z | You want the effect of z removed from one side only → semi-partial | |
The coefficients
Range, assumptions, and the failure mode that actually bites in practice.
Spearman correlation
rank-orderKendall tau-b
concordancescipy.stats.kendalltau uses.Point-biserial
continuous ↔ binaryCramér's V
nominal ↔ nominalCorrelation ratio
continuous ↔ nominalDistance correlation
Székely–RizzoMutual information
information gainPartial correlation
controlling for zPhi coefficient
binary ↔ binaryFour things a coefficient will not tell you
The shape
Anscombe's quartet and the Datasaurus dozen are the standard demonstrations: wildly different point clouds sharing the same means, variances and r to two decimals. Plot the pair before you act on the number.
The direction of causation
A coefficient is symmetric — rxy = ryx — so it cannot contain directional information even in principle. Anything causal comes from the design, not from the statistic.
Whether the sample is one population
Simpson's paradox: a relation that is negative inside every subgroup can be positive overall. Try the two clusters shape in the sandbox. Always ask what the rows are.
How much you should believe it
With n = 20, the 95% interval around an r of 0.5 spans roughly 0.07 to 0.77. Bootstrap the coefficient before you build a decision on the third decimal.
Part two
Handling correlated features
Collinearity is not a data-quality defect to be scrubbed. It is a statement about what your data can and cannot identify. Before reaching for a fix, be precise about which of the two things it actually breaks — because it reliably breaks one and reliably leaves the other alone.
What it breaks, and what it does not
Breaks: the individual coefficients
When two features move together, many different weightings of them fit the data almost equally well. The standard errors inflate, signs flip between refits, and "feature x is worth 0.4" stops being a defensible sentence.
Leaves alone: the predictions
The fitted surface is stable even when the coefficients are not, so held-out error
barely moves. If the model only ever gets asked for
y_hat, collinearity may cost you nothing at all.
Breaks: attribution and importance
Tree ensembles split arbitrarily among correlated features, splitting the credit and making each look weak. Permutation importance understates all of them, because the information survives in the twin.
Leaves alone: identifiable combinations
Even at r = 0.99 the sum of two coefficients is estimated precisely; only the split between them is unstable. That is often the quantity you wanted anyway.
What collinearity does to a coefficient
A textbook setup, simulated live: y = x₁ + x₂ + ε,
so both true coefficients are exactly 1. Each dot is one refit of the same model on a
fresh sample of 60 rows. Raise the correlation between x₁ and x₂ and watch
the cloud stretch along the line where the sum stays at 2 — the data pins down
the total and says almost nothing about the split.
Dashed line: all coefficient pairs summing to 2. The cloud collapses onto it as the correlation rises.
n = 60,
σ = 1. Test error is measured on 200 fresh rows
per fit. The ridge penalty adds λ · n to the
diagonal of X′X on the centred predictors, which have unit variance by
construction, so λ = 0 is ordinary least squares
and λ is on the per-observation scale.
Detecting it — pairwise r is not enough
A dozen features can each be only weakly correlated in pairs and still be almost perfectly linearly dependent together. Pairwise correlation cannot see that; the diagnostics below can.
| Diagnostic | Definition | Rough threshold | Catches |
|---|---|---|---|
| Pairwise |r| | Correlation matrix of the features | > 0.8 worth a look | Two-way redundancy only. Cheap, and the right first pass. |
| VIF | VIFj = 11 − Rj² — regress feature j on all the others | > 5 suspicious, > 10 severe | Multi-way dependence. √VIF is the factor by which the standard error is inflated. |
| Condition index | κ = √λmax / λmin of X′X on standardised columns | 10–30 moderate, > 30 severe | The overall conditioning in one number. Standardise first, or it measures your units; on the correlation matrix that is already done. |
| Eigenvalue spectrum | Eigenvalues of the correlation matrix | Any near zero | Near-zero eigenvalues, whose eigenvectors name the offending combination directly. |
| Refit variance | Coefficient spread across bootstrap resamples or CV folds | Sign flips | The problem as it will actually reach you. Works for any model, no assumptions. |
Pruning a correlation matrix
Eleven features of a property dataset, generated from three latent factors — size, location and age — plus per-feature noise, which is why the block structure is real rather than decorative. Two pairs are exact duplicates by construction: a unit conversion, and a variable stored both as a year and as an age. Move the threshold to see the greedy pruner work.
Redundancy groups at this threshold
Result
caret::findCorrelation): take the most
correlated surviving pair, and drop whichever of the two has the higher
average correlation with everything else still in the set. Every number in
the matrix is printed, so the colour is a reading aid rather than the only encoding.
The slider goes down to 0.50 so you can watch the rule over-prune; in practice
anything below ~0.85 is throwing away real information.
Eight treatments
Ordered from least to most invasive. The first one is not a joke — it is the correct answer more often than the rest combined.
Do nothing
Cost: none
Leave it alone
If the model exists to produce predictions and nobody will read its coefficients, correlated features are not a problem to solve. Held-out error is essentially unaffected — the second lab shows the test RMSE holding flat while the coefficient spread grows by an order of magnitude.
Dropping features always throws away some information. Doing it to satisfy an assumption your use case never depended on is a pure loss.
Select
Cost: information loss
Drop one of each redundant pair
The greedy threshold rule in the lab above. Simple, reproducible, and it keeps the surviving features exactly as interpretable as they were.
Choose the survivor on grounds the correlation matrix does not know about: which is cheaper to collect, which is measured more reliably, which is available at prediction time, which a domain expert can explain. Picking the one with the higher correlation to the target is the obvious rule and the weakest one — it overfits the training sample and ignores interactions.
Group
Cost: a tuning choice
Cluster the features, keep one per cluster
Run hierarchical clustering on the distance
1 − |r| (Spearman if the features are
skewed), cut the dendrogram at a chosen height, and take one representative — or
the cluster mean — per group.
This scales where pairwise pruning does not, and it produces a stable,
reviewable artefact: a named list of redundancy groups that you can hand to a
domain expert. FeatureAgglomeration in
scikit-learn and VarClusHi in Python both
implement this directly.
Engineer
Cost: domain effort
Combine them into the quantity you meant
Height and weight are collinear; BMI is not. Total spend and transaction count are collinear; average basket size is not. Price and area are collinear; price per square metre is not.
This is the same insight as the second lab, from the other end: the sum was always the identifiable quantity, so estimate the sum. A ratio, a difference or an index makes the stable direction explicit and drops the unstable one on purpose.
Regularise
Cost: bias, a hyperparameter
Ridge, lasso, elastic net
Ridge is the direct treatment: the L2 penalty makes the design matrix invertible again and shrinks correlated features together, splitting the shared effect between them instead of letting the fit pick a random split. Both stay in the model. Move the ridge slider in the second lab and watch the cloud contract.
Lasso does the opposite: among a correlated group it keeps roughly one and zeroes the rest — but which one changes from resample to resample. It is a sparsity tool, not a collinearity tool, and its selections should never be read as importances. Elastic net is the compromise: the L2 part restores the grouping effect so correlated features enter or leave together.
Project
Cost: interpretability
PCA, PLS and friends
PCA replaces the features with orthogonal components, so collinearity is gone by construction. The price is total: the model no longer has features with names, and every downstream explanation has to be given in terms of loadings.
PCA is also unsupervised — it ranks directions by variance in X, with no reference to y, so a low-variance direction that happens to predict the target can be discarded. Partial least squares fixes exactly that by extracting components that covary with y, and usually needs fewer of them.
Residualise
Cost: an arbitrary ordering
Regress one out of the other
Keep x₁ as it is and replace x₂ with its residual after regressing it on x₁. The two are now exactly uncorrelated and both stay in the model.
The catch is in the meaning: the second feature is now "the part of x₂ that x₁ does not explain", and all of the shared effect has been assigned to x₁ by your choice of ordering. That is a defensible move when there is a real hierarchy — a baseline and an adjustment, a control and a treatment — and pure sleight of hand otherwise.
Attribute
Cost: compute
Fix the importance measure instead of the features
If collinearity is only hurting your explanations, treat the explanation. Permuting one of two correlated features leaves the information intact in its twin, so both look unimportant — and the permutation also creates rows that could not exist, which pushes the model off its training distribution.
Permute the whole correlated group at once and report one number for the group; or use conditional permutation, which resamples within strata of the correlated features. The honest output is "these three features jointly carry 12% of the signal", not three separate small numbers.
Picking a treatment by what you owe the reader
| Your goal | First move | Fallback | Skip |
|---|---|---|---|
| Predictive accuracy, black box | Nothing | Ridge or a tree ensemble | Truncated PCA — you would lose signal for no gain |
| Interpretable coefficients | Combine into a meaningful quantity | Ridge, then report the group effect | Lasso — its selection among a correlated group is not stable |
| Causal or policy inference | Fix the design — more variation, or a different identification strategy | Report what is identified: the sum, the contrast | Any automatic pruner. Dropping a confounder to reduce VIF creates bias |
| Feature importance for a stakeholder | Grouped permutation importance | Cluster first, then report per cluster | Raw gain-based importances from a boosted tree |
| Cutting cost — fewer columns to collect | Cluster, keep the cheapest per cluster | Greedy threshold pruning | Ridge — it keeps everything, which is the opposite of the goal |
| Wide data, p greater than n | PLS or elastic net | PCA with cross-validated component count | VIF — it is undefined when the design matrix is singular |
Traps
The dummy variable trap
A full set of one-hot columns sums to a constant, so with an intercept the design matrix is exactly singular. Drop one level, or fit without an intercept. This is perfect collinearity, not near-collinearity, and no pairwise threshold rule will surface it — VIF and the eigenvalue spectrum will.
Pruning on the whole dataset
Selecting features by correlation before splitting leaks the test set into the choice. Feature selection is part of the pipeline and belongs inside the cross-validation loop.
Correlation is not redundancy
Two features at r = 0.9 can still be jointly necessary if a non-linear model uses their difference. Confirm redundancy by removing the feature and measuring the change in validation error, not by reading the matrix.
A very high correlation with the target
An r of 0.98 against the label is usually not a great feature — it is leakage. Check whether the value was actually available at prediction time before celebrating.
Notes
Read the numbers as illustrations of the mechanism, not as measurements of anything real: every sample on this page is drawn from a seeded generator when you open it.
Implementations worth knowing: scipy.stats for
Pearson, Spearman and Kendall; dcor for distance
correlation;
sklearn.feature_selection.mutual_info_regression
for the k-nearest-neighbour MI estimator;
statsmodels.stats.outliers_influence.variance_inflation_factor
for VIF; sklearn.inspection.permutation_importance
for the attribution fix.