Propensity Score for Matching, Step by Step: Treatment vs Control and Case-Control

On this page
อ่านฉบับภาษาไทย — the Thai version of this article
Abstract
Observational studies rarely hand us comparable groups. When clinicians treat the sickest patients first, the treated and the untreated differ before anyone measures an outcome — a problem called confounding by indication, and an unadjusted comparison of their outcomes is biased from the start. Propensity-score matching is the workhorse fix: it collapses every measured confounder into a single number — the modelled probability of being treated — and then pairs each treated patient with an untreated patient who shares that number. The same procedure answers a second, superficially different question — whether an exposure is associated with a disease — simply by relabelling the groups as cases and controls. Yet the mechanism is often taught as a black box, so its assumptions and limits stay hidden. Working through a simulated 400-patient teaching cohort, we fit the score, inspect overlap, match nearest-neighbour, and re-check balance by hand, then reproduce the identical result with MatchIt. This article walks through propensity-score matching one line of real R code at a time, for both treatment-versus-control and case–control designs.

Why a straight comparison is not enough
Suppose we have a cohort and we want to know whether a treatment works. The obvious move is to compare outcomes in the people who got it against the people who did not. In an observational dataset, that comparison is almost always wrong.
Why? Because nobody rolled a die to decide who got treated.
In real practice, sicker and older patients are treated more often — the very people who tend to do worse anyway. So the treated group starts out disadvantaged, and any honest outcome difference is tangled up with the reason people were treated in the first place. That tangle is confounding, and matching exists to cut it.
Everything below runs on a simulated teaching cohort — 400 fabricated patients, not real people — built with a fixed random seed so anyone can reproduce it. Treatment was handed out more often to older, sicker patients on purpose, so the confounding is baked in and we can watch matching reduce it. Nothing is computed in your browser; the interactive terminal further down simply replays a real R analysis captured offline, one line at a time.
Step 1 — The problem: the groups do not start equal
Of our 400 patients, 69 were treated and 331 were controls. Before we touch a model, how different are those two groups at baseline?
We measure the gap with the standardized mean difference (SMD): the difference in a covariate's mean between groups, divided by a standard deviation so that age (in years) and severity (on its own scale) become comparable. A common rule of thumb is that |SMD| > 0.1 signals meaningful imbalance.
On the raw, unmatched cohort:
All three are well over the 0.1 line, severity worst of all. The treated group really is older and sicker before we do anything. If we stopped here and compared outcomes, we would be measuring the disease as much as the drug.

Step 2 — The propensity score is a prediction score
Balancing three covariates one at a time is already awkward. Real studies have twenty. The propensity score's trick is to squeeze all of them into a single number per person.
That number is each person's model-predicted probability of being treated, given what we know about them:
$$\text{PS}_i = \Pr(\text{treated}=1 \mid X_i)$$
We get it by fitting an ordinary logistic regression — treatment on the left, confounders on the right:
glm(treat ~ age + sex + sev, family = binomial)
Each patient comes out with one number between 0 and 1. A score of 0.80 means "given this age, sex and severity, people like this were usually treated"; 0.20 means "usually not." 👉 The key idea is subtler than it first looks: among patients who share a propensity score, the treated and control groups have, on average, the same distribution of the measured covariates. This is a property of the groups, not of individuals. It does not mean any two matched people are alike — a young, severe patient and an older, milder one can land on the very same score; what balances is the distribution across the matched groups, not each pair.
Notice what the score is not. It does not model the outcome, and it never sees who did well or badly. It only models treatment assignment — how treatment was handed out in this dataset. Hold on to that; it is the single most common thing people get wrong.
One more warning while we are here: you never judge a propensity-score model by its AUC (C-statistic) or calibration. Its only job is to balance the covariates, so the diagnostic that matters is the SMD table further down — not discrimination. A score that separates treated from control too cleanly is a red flag, not a better model: it signals poor overlap and a possible positivity violation, meaning the two groups barely resemble each other.

Step 3 — Overlap: where comparable patients actually live
Before matching anyone, we plot the propensity score for both groups on the same axis.
Where the treated and control score-distributions overlap, comparable patients exist in both groups — a treated patient with a score of 0.6 has controls near 0.6 to be matched to. Where they do not overlap, one group has scores the other simply never reaches, and no honest match exists there. That non-overlap is a positivity (common-support) violation — a region where no comparable counterpart exists — the fingerprint of strong confounding-by-indication, made visible.
In our cohort the two curves overlap well across most of the range, which is exactly the condition that makes matching possible. If they barely overlapped, no clever algorithm could rescue the comparison — we would be out of comparable people. Here is the flip side of the point above: well-overlapping curves mean the score cannot cleanly tell treated from control (low discrimination) — which, for a propensity score, is exactly what you want.
Step 4 — Nearest-neighbour matching
Now the actual matching, with nothing hidden. We use greedy 1:1 nearest-neighbour matching without replacement, which is simpler than it sounds:
- Sort the treated patients by propensity score, largest first.
- Walk down the list. For each treated patient, pick the still-unused control whose score is nearest.
- Lock that pair in; that control cannot be used again.
The first treated patient has a score of 0.760; the nearest free control scores 0.753. Their gap is |0.760 − 0.753| = 0.007 — for matching purposes, practically the same patient. Repeat down all 69 treated patients and we get 69 matched pairs (138 people kept, the rest of the controls set aside).
Below you can run this yourself. The terminal replays the real R analysis line by line, from the raw cohort all the way to the case–control reframing. Pick By hand to watch the greedy loop spelled out step by step, or With MatchIt to run the same job in the one function everyone actually uses — the match underneath is identical. Type the highlighted line (or press Enter to autofill and run), and the Stage shows the table, number or plot each step produces. Remember: nothing is computed in the browser; every value was produced in R beforehand.
แนวคิด propensity score พูดง่ายแต่ เห็นภาพ ยาก เทอร์มินัลนี้รันการวิเคราะห์ R จริงทีละบรรทัดบนข้อมูล จำลอง (ผู้ป่วย 400 คน กลุ่มที่ได้รับการรักษามักอายุมากและป่วยหนักกว่า) เลือกแท็บได้: ทำเอง ไล่กลไกทีละขั้น — สร้างคะแนน วัดช่องว่าง จับคู่เพื่อนบ้านใกล้สุด แล้วตรวจสมดุลใหม่ ส่วน ใช้ MatchIt ทำงานเดียวกันด้วยฟังก์ชันที่ทุกคนใช้จริง คุณเป็นคนลงมือ: พิมพ์ตามบรรทัดที่ไฮไลต์ (หรือกด Enter เพื่อเติมและรัน) แล้วแผง Stage ด้านขวาจะแสดงตาราง ค่า หรือกราฟที่แต่ละขั้นสร้างขึ้น เครื่องเดียวกันนี้ใช้จับคู่ได้ทั้งแบบกลุ่มรักษาเทียบกลุ่มควบคุม และแบบ case-control — ขั้นสุดท้ายจะแสดงให้ดู ไม่มีการคำนวณในเบราว์เซอร์ ทุกค่าถูกคำนวณด้วย R ไว้ก่อนแล้ว
The idea of a propensity score is easy to say and hard to see. This terminal runs a real R analysis one line at a time on a simulated cohort (400 patients, treatment skewed toward older and sicker people). Pick a tab: By hand spells out the mechanism — fit the score, measure the gap, match nearest-neighbor, re-check balance — while With MatchIt does the same job in the one function everyone actually uses. You drive it: type the highlighted line (or press Enter to autofill and run), and the Stage on the right shows the table, number, or plot each step produces. The same machine matches treatment-vs-control and case-control studies — the last step shows how. Nothing runs in your browser; every value was computed in R beforehand.

Step 5 — Check balance, never assume
Matching is a proposal, not a guarantee. The only way to know it worked is to recompute the SMDs on the matched set and look.
We use the same yardstick throughout — the difference in means divided by the treated group's standard deviation, which is exactly the denominator MatchIt uses by default, so the two tabs report identical numbers:
$$\text{SMD}=\dfrac{\bar{x}_{1}-\bar{x}_{0}}{s_{\text{treated}}}$$
Every gap shrank. Severity — the worst offender — is now comfortably balanced at 0.04. Age and sex have moved most of the way but still sit a little above 0.1. Watch the direction, too: sex's SMD flipped sign (0.32 → −0.12), so the treated mean is now slightly below the matched controls' rather than above — it is the magnitude, not the direction, that still misses the 0.1 line. This is honest matching: better, not perfect. Greedy 1:1 nearest-neighbour matching without a caliper never rejects a match — it takes the closest remaining control even when the closest is not close — so it cannot force the tails into balance, which is exactly why age and sex remain slightly off. In a real analysis you might respond by tightening a caliper, adding an interaction to the score, or adjusting for the residual imbalance in the outcome model.
One detail worth pausing on: the hand-written greedy loop and MatchIt select the identical 69 controls. MatchIt is not doing anything magic — it fits the same logistic score and runs the same nearest-neighbour loop, then hands you the balance table for free.

The same machine: treatment vs case–control
Here is the payoff. Everything above was framed around a treatment. But the exact same machine matches a case–control study — you learn the mechanism once and use it for both designs.
Only the labels change:
In code, the only thing that moves is the left-hand side of the formula: case ~ age + sex + sev instead of treat ~ age + sex + sev. The same procedure — fit a score, check overlap, match, check balance — carries over unchanged; only the fitted score's numeric values move, because a different variable is now being modelled.
That difference is deeper than it looks. Modelling Pr(case | X) instead of Pr(treated | X) models the outcome, not the exposure — so, strictly speaking, this is a disease-risk (prognostic) score, not a propensity score: the same four-step machinery aimed at a different target. (This propensity-versus-prognostic-score distinction is its own topic — see Propensity Score vs Prognosis Summary Score.)
There is also a subtlety in what that score means here. Because case–control sampling fixes the case-to-control ratio by design, the fitted "probability of being a case" is scaled by that ratio — it is a within-sample quantity, not a population risk. That is precisely why the reportable estimate is an odds ratio: the odds ratio is the one measure that stays invariant under outcome-based sampling.
And one caveat is not optional: matched case–control data require a matched analysis. Because the cases and controls are now individually matched, the odds ratio must come from a conditional analysis — conditional logistic regression, or a Mantel–Haenszel / McNemar estimator — not an ordinary unmatched OR. Analysing matched pairs as if they were independent (overmatching) biases the odds ratio toward the null.
So what changes is the question, and therefore what the final estimate means: the average treatment effect on the treated (ATT) in one framing, an exposure–disease odds ratio — from a conditional, matched analysis — in the other.

Common mistakes
1. Treating the propensity score as if it predicts the outcome
It does not. The score models treatment assignment only — it never sees the outcome. A high score means "likely to be treated," not "likely to do well." Confusing the two quietly undoes the whole method.
2. Skipping the post-match balance check
Matching is a proposal, not a proof. If you do not recompute the SMDs afterwards, you have no idea whether it worked. Never assume — check.
3. Expecting perfect balance — or expecting too much of it
Our own age and sex SMDs landed near 0.1, not at zero, and that is normal. More importantly, matching only balances the confounders you measured and put in the model. It does nothing for unmeasured confounding — a variable you never recorded cannot be balanced, and no amount of matching will save you from it.
4. Choosing the method before the question
Whether your estimate should be an ATT or an odds ratio, whether you even want matching at all, follows from the question you are asking. Decide the question first; let it choose the method — not the other way around.
Final takeaway
Propensity-score matching is one small, honest idea repeated: turn every measured confounder into a single predicted probability, pair people who share it, and then check that the pairing actually made the groups comparable. Do that, and a biased head-to-head comparison becomes a fair one — for measured confounders, and only for those.
Key takeaways
- Confounding comes first. In our simulated cohort the treated were older and sicker (SMDs 0.62, 0.32, 0.71); an unadjusted comparison would measure the disease, not the drug.
- The propensity score is a prediction score — $\Pr(\text{treated} \mid X)$ from a logistic model — that collapses all measured confounders into one number. It models treatment, not outcome.
- Overlap makes matching possible; nearest-neighbour matching then pairs each treated patient with the closest unused control (69 pairs here).
- Always re-check balance. After matching, severity fell to 0.04 and age to 0.15 — better, not perfect — using the treated group's SD as the yardstick. The by-hand loop and MatchIt pick the identical 69 controls.
- The same machine serves case–control studies: relabel the groups and the estimate becomes an exposure–disease odds ratio instead of an ATT. Matching balances only what you measured — never unmeasured confounding.