Skip to main content

From single columns to relationships

Look at each column alone, then at pairs, then inside subgroups. On the diabetes table: a right-skewed progression score, body mass index leading at r = 0.59, a near-duplicate pair explained by a formula, and a relationship that holds in every group.
S01.8.3L1 Core25 minIn review

Before this lesson

S01.8.2 Audit before you analyse S01.3 Center, spread and shape S01.4 Distributions and outliers S01.5 Relationships between variables S01.7 Simpson's paradox and confounding, a first look

After this lesson you can

  • Summarise every column of a table with a centre, a spread, a shape and its flagged values, choosing resistant summaries where the shape calls for them.
  • Rank the measurements by their correlation with an outcome and read the scatter plots behind the strongest.
  • Find a near-duplicate pair of columns and explain it before treating the two as separate findings.
  • Recompute a leading relationship inside subgroups and split its sum of products into a within-group and a between-group part.

In review. The maths and code on this page are checked by automated tests on every change, but an independent reader has not signed it off yet. Spot a mistake or a confusing step? Tell us.

The question

Ten measurements, one progression score, 442 patients. Which measurement goes most closely with the score, how closely, and would the answer survive if you looked only at the women, or only at the over-sixties?

From the last lesson

1. What does s4 turn out to be, and how did the audit find out?

Show answer

s1 / s3, rounded to a whole number for most patients. The distinct-value check showed that 380 of 442 values are whole numbers, and dividing s1 by s3 matched them.

2. Why is a table with no missing values a finding to report?

Show answer

Real medical data usually have holes, so the table was probably cleaned, perhaps by dropping incomplete rows. If so, the rows are a selection, fair only if the holes fell completely at random.

Intuition

Steps 4, 5 and 6 of the workflow widen the view one notch at a time. First one column: what is typical, how spread out, what shape, which values are unusual. Then pairs: which columns rise and fall together. Then traps: does a pair’s relationship still hold when the patients are split into groups?

Two panels. Left, titled One column: a right tail, a blue histogram of the 442 progression scores in bins of 25 from 25 to 350, tallest between 50 and 100 and tapering slowly to the right; a solid vermillion line marks the median, 140.5, and a dashed one the mean, 152.1. Right, titled One pair: bmi and score, a blue scatter of the score against body mass index from 18 to 42, rising from lower left to upper right with much scatter, and a vermillion least-squares line; r equals 0.59.
S01.8.3-F1

The two views of this lesson. Left: one column, the score, with a long right tail that pulls the mean above the median. Right: one pair, body mass index and the score, the strongest relationship in the table.

The order matters. A correlation between two columns is only as sound as each column on its own: a skewed column, a column with outliers or a column that is a copy of another all change what the correlation means. And a relationship found in all 442 patients may be built from differences between groups rather than from anything that happens inside them, as in S01.7.

Build it up

Step 1. The score alone. It runs from 25 to 346. Its mean is 152.1 and its median 140.5; the quartiles are 87 and 211.5, so the interquartile range is 124.5. The skewness is 0.44: a right tail, as the histogram shows. The fences of S01.4.3 sit at \({87 - 1.5 \times 124.5 = -99.75}\) and \({211.5 + 1.5 \times 124.5 = 398.25}\), so no score is flagged. The score’s shape is mildly skewed, so the report gives the median and quartiles first.

Step 2. The ten measurements alone. The same five numbers for each column, in one table:

Column Median IQR Flagged
age 50 20.75 0
bmi 25.7 6.075 3
bp 93 21 0
s1 186 45.5 8
s2 113 38.45 7
s3 48 17.5 7
s4 4 2 2
s5 4.62 0.721 4
s6 91 14.75 9

“Flagged” counts values beyond the fences. None is impossible, and 40 flags among 3,978 measurements are what tails look like, not errors. The most skewed columns are s3 (0.80), s4 (0.73) and bmi (0.60). sex is a code and gets counts, not a median.

Step 3. Every measurement against the score. One correlation per measurement.

A horizontal bar chart of Pearson's r between each of the ten measurements and the progression score, sorted from the top: bmi 0.59, s5 0.57 and bp 0.44 in vermillion, marked by a bracket as the three strongest; then in blue s4 0.43, s6 0.38, s1 0.21, age 0.19, s2 0.17, sex 0.04; and at the bottom a sky-blue bar to the left for s3, minus 0.39.
S01.8.3-F4

Each measurement’s correlation with the score. Body mass index leads, with s5 close behind; HDL cholesterol (s3) is the only one that falls as the score rises.

Body mass index leads at \({r = 0.59}\), then s5 at 0.57 and blood pressure at 0.44. s3, HDL cholesterol, is negative at \({-0.39}\): higher HDL goes with lower scores. sex is at 0.04, so close to zero that lesson S01.8.4 asks whether it could be chance. Spearman’s rank correlation (S01.5.3) puts s5 first (0.59) and bmi second (0.56): the two leaders are about equally strong, and which one “wins” depends on the measure. That is worth one sentence in the report.

Step 4. Look at the scatter plots behind the numbers. Anscombe’s lesson (S01.5.3): a correlation can hide a curve, a cluster or one far point.

Three scatter plots side by side, each of the progression score against one measurement, with a vermillion least-squares line. bmi, body mass index from 18 to 42, r equals 0.59. s5, possibly log triglycerides from 3.3 to 6.1, r equals 0.57. bp, blood pressure from 62 to 133, r equals 0.44. All three show a broad cloud rising to the right with no curve and no single far point.
S01.8.3-F5

The three strongest pairs. Each is a broad cloud that rises to the right: no curve, no cluster, and no single point that makes the correlation.

None of the three clouds bends, splits or hangs on one point. A straight-line summary is fair for each, and each is loose: a patient’s body mass index leaves most of the score’s spread unexplained.

Step 5. Pairs of measurements. Among the 45 pairs of measurements one stands out: s1 (total cholesterol) and s2 (LDL) at \({r = 0.90}\). Two columns that nearly repeat each other should not be reported as two findings, so ask why.

Two panels. Left, titled s2 against s1: a tight blue cloud rising from lower left to upper right, r equals 0.90. Right, titled s2 rebuilt from s1, s3, s5: s2 plotted against s1 minus s3 minus e to the s5 over 5; the blue points lie exactly on a vermillion diagonal line. Four hollow purple points mark the patients that miss by 0.2 to 0.4, too little to see apart from the line at this scale. A label reads 438 of 442 within 0.01.
S01.8.3-F6

Why s1 and s2 move together. Right: s2 is, to within 0.01 for 438 of 442 patients, total cholesterol minus HDL minus a fifth of the triglycerides, the Friedewald formula for LDL.

A common way to estimate LDL in the clinic is the Friedewald formula, LDL = total cholesterol − HDL − triglycerides / 5, with every value in mg/dL. If s5 is the natural log of the triglycerides, then \({e^{s5}}\) is the triglycerides, and s1 − s3 − e^s5 / 5 should equal s2. For 438 of the 442 patients it does, to within 0.01. For patient 1,

\[ \begin{aligned} &157 - 38 - e^{4.8598}/5 \\ &\quad = 157 - 38 - 25.80 \\ &\quad = 93.20 , \end{aligned} \]

and s2 is 93.2. So s2 was most likely computed from three other columns, not measured. The check also supports the reading of s5 as a log, and of the cholesterol units as mg/dL. It does not prove them, and the report says “consistent with”.

Step 6. Traps: the leading relationship inside subgroups. Split by the sex code and by three age bands, and recompute the correlation of body mass index with the score in each group.

The formal statement

NoteThe subgroup check

For a relationship between columns \(x\) and \(y\) and a grouping of the rows into groups \(g\) of size \(n_g\), recompute \(r\) inside each group. The relationship holds in every subgroup when each group’s \(r\) has the same sign as the pooled \(r\) and a size of the same order.

Why the check can fail, and how to see that it has not, comes from the sum of products of S01.7.2. Write \({S_{xy} = \sum_i (x_i - \bar{x})(y_i - \bar{y})}\) over all rows, and \(\bar{x}_g\), \(\bar{y}_g\) for the means inside group \(g\). Inside each bracket, add and take away the group mean:

\[ \begin{aligned} x_i - \bar{x} &= (x_i - \bar{x}_g) + (\bar{x}_g - \bar{x}), \\ y_i - \bar{y} &= (y_i - \bar{y}_g) + (\bar{y}_g - \bar{y}). \end{aligned} \]

Multiply the two and add over the rows of group \(g\). The two cross terms each contain a sum of deviations from the group’s own mean, which is zero (S01.3.1), so they vanish. What is left, added over the groups, is

\[ \begin{aligned} S_{xy} &= \sum_g \sum_{i \in g} (x_i - \bar{x}_g)(y_i - \bar{y}_g) \\ &\quad + \sum_g n_g (\bar{x}_g - \bar{x})(\bar{y}_g - \bar{y}) . \end{aligned} \]

The first line is the within-group part: how \(x\) and \(y\) move together inside each group. The second is the between-group part: how the group means line up. Simpson’s reversal in S01.7.2 happened because the between part was large and of the opposite sign. If the within part carries nearly all of \(S_{xy}\), the groups did not make the relationship.

Worked example

Does the link between body mass index and the score hold in every group?

A dot chart of the correlation r of bmi with the score, on an axis from 0.3 to 0.8. Top row, shaded, all patients: 0.59, with n equals 442, and a dashed vertical line at that value. Then sex coded 1: 0.53, n 235; sex coded 2: 0.65, n 207; age under 40: 0.55, n 117; age 40 to 59: 0.61, n 222; age 60 and over: 0.54, n 103. Title: Positive in every subgroup.
S01.8.3-F2

The correlation of body mass index with the score, overall (top) and in five subgroups. Every group gives a clearly positive \(r\) between 0.53 and 0.65.

By group. The correlation is 0.53 for the 235 patients coded 1 and 0.65 for the 207 coded 2. In the age bands it is 0.55 under 40 (117 patients), 0.61 from 40 to 59 (222) and 0.54 at 60 and over (103). No group reverses the sign, and none comes close to zero.

By the split of the sum of products, for the sex code. The groups’ means are close to the overall ones. Body mass index averages 26.011 and 26.790 against 26.376 overall, so the groups sit \({-0.365}\) and \({+0.414}\) from it; the score averages 149.021 and 155.667 against 152.133, so they sit \({-3.112}\) and \({+3.534}\) from it. The between part is therefore small:

\[ \begin{aligned} &235 \times (-0.365) \times (-3.112) \\ &\quad + 207 \times 0.414 \times 3.534 \\ &\quad = 266.9 + 302.9 = 569.8 . \end{aligned} \]

The within parts are 43,186.5 and 44,332.3, so the pooled sum of products is 88,088.6 (88,089 with unrounded means), and 99.4 percent of it comes from inside the two groups. The relationship is not a by-product of the sex code.

The same check on s5 and blood pressure also gives a positive \(r\) in every group (from 0.43 to 0.61 for s5, from 0.33 to 0.53 for blood pressure). Fifteen correlations computed, none reversed.

Code

From scratch: a correlation function and a loop over the columns.

import numpy as np
import pandas as pd
from scipy import stats
from sklearn.datasets import load_diabetes

table = load_diabetes(as_frame=True, scaled=False).frame
y = table["target"].to_numpy()

def corr(a, b):
    a, b = np.asarray(a, dtype=float), np.asarray(b, dtype=float)
    da, db = a - a.mean(), b - b.mean()
    return float((da * db).sum() / np.sqrt((da ** 2).sum() * (db ** 2).sum()))

q1, q3 = np.quantile(y, [0.25, 0.75])
print("score: mean", round(y.mean(), 1), " median", np.median(y), " quartiles", q1, q3,
      " skew", round(stats.skew(y), 2))
with_score = {name: round(corr(table[name], y), 2) for name in table.columns[:-1]}
print(dict(sorted(with_score.items(), key=lambda kv: -kv[1])))
score: mean 152.1  median 140.5  quartiles 87.0 211.5  skew 0.44
{'bmi': 0.59, 's5': 0.57, 'bp': 0.44, 's4': 0.43, 's6': 0.38, 's1': 0.21, 'age': 0.19, 's2': 0.17, 'sex': 0.04, 's3': -0.39}

The column table, the pair s1 and s2, and the Friedewald check.

def describe(column):
    lo, hi = np.quantile(column, [0.25, 0.75])
    fence = 1.5 * (hi - lo)
    flagged = int(((column < lo - fence) | (column > hi + fence)).sum())
    return {"median": round(float(np.median(column)), 2), "iqr": round(float(hi - lo), 3), "flagged": flagged}

columns = {name: describe(table[name]) for name in table.columns[:-1] if name != "sex"}
print(pd.DataFrame(columns).T)
rebuilt = table["s1"] - table["s3"] - np.exp(table["s5"]) / 5
close = int((abs(rebuilt - table["s2"]) < 0.01).sum())
print("r(s1, s2) =", round(corr(table["s1"], table["s2"]), 2), "  s2 rebuilt within 0.01:", close)
     median     iqr  flagged
age   50.00  20.750      0.0
bmi   25.70   6.075      3.0
bp    93.00  21.000      0.0
s1   186.00  45.500      8.0
s2   113.00  38.450      7.0
s3    48.00  17.500      7.0
s4     4.00   2.000      2.0
s5     4.62   0.721      4.0
s6    91.00  14.750      9.0
r(s1, s2) = 0.9   s2 rebuilt within 0.01: 438

The subgroup check, then the split of the sum of products.

bands = pd.cut(table["age"], [0, 39, 59, 120], labels=["under 40", "40 to 59", "60 and over"])
for name, key in (("sex", table["sex"]), ("age band", bands)):
    print(name, table.groupby(key, observed=True)[["bmi", "target"]].corr().xs("target", level=1)["bmi"].round(2).to_dict())

x = table["bmi"]
within = sum(((g.bmi - g.bmi.mean()) * (g.target - g.target.mean())).sum() for _, g in table.groupby("sex"))
between = sum(len(g) * (g.bmi.mean() - x.mean()) * (g.target.mean() - y.mean()) for _, g in table.groupby("sex"))
pooled = ((x - x.mean()) * (y - y.mean())).sum()
print("within", round(within), " between", round(between), " pooled", round(pooled),
      " share within", round(100 * within / pooled, 1))
sex {1.0: 0.53, 2.0: 0.65}
age band {'under 40': 0.55, '40 to 59': 0.61, '60 and over': 0.54}
within 87519  between 570  pooled 88089  share within 99.4

The library does the loop in one call: table.corr()["target"] gives every correlation with the score, table.corr(method="spearman") the rank versions, and groupby(...).corr() the subgroup check. The scatter plots are one Axes.scatter call each; the figures above draw them.

Explore

This module’s interactive, the audit trail, lives in lesson S01.8.2. Run its checks with the duplicate and the rent in euros left unfixed, and watch the correlation of area with rent: a relationship is only as sound as the columns under it.

Pitfalls

Two panels, each with a blue dot strip of the 442 progression scores from 25 to 346. Left, marked with a cross and titled Mean plus or minus 2 sd: a vermillion band from minus 2.1 to 306.3 around the mean, which runs into a purple-shaded region below the lowest score; the caption reads the band reaches below the lowest score, 25. Right, marked with a tick and titled Five-number summary: a vermillion box from 87 to 211.5 with the median at 140.5 and whiskers to 25 and 346; the caption reads median and quartiles follow the tail.
S01.8.3-F3

Wrong beside right. Mean ± 2 standard deviations treats the score as symmetric and runs below zero. The five-number summary follows the data’s own shape.

“Mean ± 2 standard deviations covers the typical range.”

Only for a roughly symmetric column (S01.3.4). For the score it gives \({152.1 \pm 154.2}\), from \({-2.1}\) to 306.3: the lower end is below every score in the table. Report the median and quartiles for a skewed column.

“s1 and s2 both correlate with the score: two findings.”

They are one finding told twice. s2 is computed from s1, s3 and s5, and s1 and s2 have \({r = 0.90}\). In machine learning, a feature built from others adds no new information and can make a linear model’s weights unstable (ML01.9).

“r = 0.59 is strong, so body mass index explains the score.”

A correlation measures how well a straight line fits, not why. Here the cloud is broad, the patients were not assigned their body mass index, and S01.7.3 lists the rival explanations. The report says “goes with”, not “causes”.

Check yourself

Recall

1. In what order does the workflow look at the data, and why that order?

Show answer

One column at a time, then pairs, then pairs inside subgroups. Each step’s conclusions rest on the one before: a correlation is only as sound as the columns, and a pooled relationship may be built from group differences.

2. Which measurement correlates most with the score, and how strongly?

Show answer

Body mass index, \({r = 0.59}\), with s5 close behind at 0.57 (and ahead under Spearman’s rank correlation, 0.59 against 0.56).

3. Why do s1 and s2 correlate at 0.90?

Show answer

s2 (LDL) is, for 438 of 442 patients, s1 − s3 − e^s5 / 5: the Friedewald formula. It was computed from total cholesterol, so it moves with it.

4. What are the two parts of a pooled sum of products?

Show answer

The within-group part, \({\sum_g \sum_{i \in g} (x_i - \bar{x}_g)(y_i - \bar{y}_g)}\), and the between-group part, \({\sum_g n_g (\bar{x}_g - \bar{x})(\bar{y}_g - \bar{y})}\).

5. Which summaries suit the progression score, and why?

Show answer

The median (140.5) and the quartiles (87 and 211.5), because the score is skewed to the right; mean ± 2 standard deviations would run below zero.

Exercises

1. ★ The score’s quartiles are 87 and 211.5. Compute the interquartile range and the two fences. The largest score is 346. Is any score flagged?

Show solution

\({\text{IQR} = 211.5 - 87 = 124.5}\), and \({1.5 \times 124.5 = 186.75}\). The fences are

\[ \begin{aligned} 87 - 186.75 &= -99.75, \\ 211.5 + 186.75 &= 398.25 . \end{aligned} \]

The scores run from 25 to 346, inside both fences, so none is flagged.

2. ★★ Patient 2 has s1 = 183, s3 = 70 and s5 = 3.8918, and a stored s2 of 103.2. Check the Friedewald reading of s2.

Show solution

\({e^{3.8918} = 49.00}\), so the triglycerides would be 49 mg/dL and a fifth of them 9.80. Then \({183 - 70 - 9.80 = 103.20}\), which matches the stored 103.2.

3. ★★★ Split the same sum of products by the three age bands. The bands have 117, 222 and 103 patients. Their body mass index means sit \({-1.145}\), \({+0.286}\) and \({+0.684}\) from the overall mean, and their score means \({-17.860}\), \({+0.952}\) and \({+18.235}\) from it. The within-group sums of products add up to 84,350.6. Find the between-group part, the pooled sum, and the share that comes from within the bands. Why is the between part larger than for the sex code?

Show solution

The between part is

\[ \begin{aligned} &117 \times (-1.145) \times (-17.860) \\ &\quad + 222 \times 0.286 \times 0.952 \\ &\quad + 103 \times 0.684 \times 18.235 \\ &\quad = 2{,}392.6 + 60.4 + 1{,}284.7 \\ &\quad = 3{,}737.7 . \end{aligned} \]

The pooled sum is \({84{,}350.6 + 3{,}737.7 = 88{,}088.3}\), the same total as before up to rounding, since the split changes and the total does not. The share within the bands is \({84{,}350.6 / 88{,}088.3 = 95.8}\) percent. The between part is larger because age lines up with both columns: older bands have a higher mean body mass index and a higher mean score, so the band means rise together. It is still small, and the relationship holds inside every band.

Summary card

One column, then pairs, then pairs inside groups.

The anchor figure again: a histogram of the progression score with its median, 140.5, and mean, 152.1, beside a scatter of the score against body mass index with a least-squares line and r equal to 0.59.
S01.8.3-F1

One column, then one pair.

  • One column. Median and IQR for a skewed column; flags from the fences, judged, not deleted.
  • Pairs. Rank by \(r\), then look at the scatter plots; explain near-duplicates before reporting them.
  • Subgroups. \(S_{xy}\) is a within part plus a between part; here 99.4 percent within.
  • The table’s leaders. bmi 0.59, s5 0.57, bp 0.44; s1 and s2 0.90, because s2 is computed.

Sources and next steps

Sources. Each was opened and checked against this lesson on 12 November 2026.

Next. S01.8.4 Report what you found and what you cannot claim turns these findings into one honest page.

This lesson unlocks

S01.8.4 Report what you found and what you cannot claim

Your progress is saved in this browser only. No account, no tracking.
Back to top