จับคู่ด้วย Propensity score แบบเห็นทุกขั้น: ทั้งกลุ่มรักษาและ case-control

On this page
Read in English — อ่านฉบับภาษาอังกฤษ
บทคัดย่อ
ในงานวิจัยเชิงสังเกต เราแทบไม่เคยได้กลุ่มที่ “เทียบกันได้” มาให้ตั้งแต่ต้น เพราะเมื่อแพทย์เลือกรักษาผู้ป่วยที่หนักที่สุดก่อน กลุ่มที่ได้รับการรักษาและกลุ่มที่ไม่ได้รับก็ต่างกันตั้งแต่ก่อนจะวัด outcome — ปัญหานี้เรียกว่า confounding by indication และการเอา outcome มาเทียบกันตรง ๆ จะมี bias ตั้งแต่แรก Propensity score matching คือเครื่องมือหลักที่ใช้แก้: มันบีบตัวแปรกวน (confounders) ที่วัดได้ทั้งหมดให้เหลือเลขเดียว คือความน่าจะเป็นที่จะได้รับการรักษาที่โมเดลทำนายไว้ แล้วจับคู่ผู้ป่วยกลุ่มรักษากับกลุ่มควบคุมที่มีเลขนี้ใกล้กัน กลไกเดียวกันนี้ยังตอบคำถามที่ดูต่างกันได้ด้วย — ว่า exposure สัมพันธ์กับโรคหรือไม่ — เพียงเปลี่ยนป้ายกำกับกลุ่มเป็น case และ control แต่กลไกนี้มักถูกสอนเป็นกล่องดำ ทำให้ข้อสมมติและข้อจำกัดถูกซ่อนไว้ เราจะเดินผ่านข้อมูล จำลอง 400 คน สร้างคะแนน ดู overlap จับคู่ nearest-neighbour และตรวจ balance ด้วยมือ แล้วทำซ้ำให้ได้ผลเดียวกันด้วย MatchIt บทความนี้พาไล่ propensity score matching ทีละบรรทัดของโค้ด R จริง ทั้งแบบกลุ่มรักษาเทียบกลุ่มควบคุม และแบบ case–control

ทำไมการเทียบกันตรง ๆ ถึงไม่พอ
สมมติเรามี cohort หนึ่ง และอยากรู้ว่า treatment ได้ผลไหม สิ่งที่ดูจะทำได้ทันทีคือเอา outcome ของคนที่ได้รับ treatment ไปเทียบกับคนที่ไม่ได้รับ แต่ในข้อมูลเชิงสังเกต การเทียบแบบนี้แทบจะผิดเสมอ
ทำไม? เพราะไม่มีใครโยนเหรียญเพื่อตัดสินว่าใครจะได้รับ treatment
ในเวชปฏิบัติจริง ผู้ป่วยที่ป่วยหนักและอายุมากกว่ามักได้รับ treatment บ่อยกว่า — ซึ่งเป็นกลุ่มที่มีแนวโน้มจะมี outcome แย่กว่าอยู่แล้ว ดังนั้นกลุ่ม treated จึงเสียเปรียบตั้งแต่ต้น และผลต่างของ outcome ที่เห็นก็พันกันอยู่กับ “เหตุผลที่ทำให้คนคนนั้นได้รับ treatment” ตั้งแต่แรก ความพันกันนี้คือ confounding และการจับคู่ (matching) มีไว้เพื่อตัดมันออก
ทุกอย่างด้านล่างรันบน ข้อมูลจำลองเพื่อการสอน — ผู้ป่วยสมมติ 400 คน ไม่ใช่คนจริง — สร้างด้วย random seed ที่ตายตัวเพื่อให้ทุกคนทำซ้ำได้ เราตั้งใจแจก treatment ให้ผู้ป่วยอายุมากและป่วยหนักบ่อยกว่า เพื่อฝัง confounding ไว้ตั้งแต่ต้น แล้วดูว่าการจับคู่ช่วยลดมันได้อย่างไร ไม่มีการคำนวณในเบราว์เซอร์ของคุณ เทอร์มินัลที่อยู่ถัดลงไปเพียงเล่นซ้ำการวิเคราะห์ R จริงที่คำนวณไว้ก่อนแล้ว ทีละบรรทัด
ขั้นที่ 1 — ปัญหา: สองกลุ่มไม่ได้เริ่มต้นเท่ากัน
ในผู้ป่วย 400 คน มี 69 คนได้รับการรักษา และ 331 คนเป็นกลุ่มควบคุม ก่อนจะแตะโมเดลใด ๆ สองกลุ่มนี้ต่างกันแค่ไหนที่ baseline?
เราวัดช่องว่างด้วย standardized mean difference (SMD): ผลต่างของค่าเฉลี่ยของตัวแปรระหว่างสองกลุ่ม หารด้วยค่าเบี่ยงเบนมาตรฐาน (standard deviation) เพื่อให้อายุ (หน่วยปี) กับความรุนแรง (severity บนสเกลของมันเอง) เทียบกันได้ กฎง่าย ๆ ที่ใช้กันคือ |SMD| > 0.1 บ่งชี้ว่ามี imbalance ที่มีนัยสำคัญ
บน cohort ดิบที่ยังไม่จับคู่:
ทั้งสามค่าเกินเส้น 0.1 ไปมาก โดย severity แย่ที่สุด กลุ่ม treated อายุมากและป่วยหนักกว่าจริง ๆ ตั้งแต่ก่อนเราทำอะไร ถ้าหยุดตรงนี้แล้วเอา outcome มาเทียบกัน เราจะกำลังวัด “ตัวโรค” พอ ๆ กับ “ตัวยา”

ขั้นที่ 2 — Propensity score คือ “คะแนนทำนาย”
การจัด balance ตัวแปรสามตัวทีละตัวก็ยุ่งพออยู่แล้ว งานวิจัยจริงมีตั้งยี่สิบตัว เคล็ดของ propensity score คือบีบ ทุก ตัวให้เหลือเลขเดียวต่อคน
เลขนั้นคือความน่าจะเป็นที่โมเดลทำนายว่าแต่ละคนจะได้รับ treatment จากสิ่งที่เรารู้เกี่ยวกับเขา:
$$\text{PS}_i = \Pr(\text{treated}=1 \mid X_i)$$
เราได้มันมาโดยการ fit logistic regression ธรรมดา — treatment อยู่ทางซ้าย confounders อยู่ทางขวา:
glm(treat ~ age + sex + sev, family = binomial)
ผู้ป่วยแต่ละคนจะได้เลขหนึ่งค่าระหว่าง 0 ถึง 1 ค่า 0.80 หมายถึง “คนที่อายุ เพศ severity แบบนี้ มักได้รับการรักษา” ส่วน 0.20 หมายถึง “มักไม่ได้รับ” 👉 หัวใจของเรื่องละเอียดกว่าที่เห็นตอนแรก: ในกลุ่มคนที่มี propensity score เท่ากัน กลุ่ม treated และ control จะมีการกระจาย (distribution) ของตัวแปรที่วัดได้ “โดยเฉลี่ย” เหมือนกัน นี่คือคุณสมบัติของ กลุ่ม ไม่ใช่ของรายบุคคล มันไม่ได้แปลว่าคนสองคนที่จับคู่กันจะเหมือนกัน — คนอายุน้อยแต่ป่วยหนัก กับคนอายุมากแต่ป่วยเบา อาจได้คะแนนเท่ากันเป๊ะ สิ่งที่ balanced คือการกระจายของทั้งกลุ่มที่จับคู่แล้ว ไม่ใช่แต่ละคู่
สังเกตว่าคะแนนนี้ ไม่ใช่ อะไร มันไม่ได้โมเดล outcome และไม่เคยเห็นว่าใครมีผลลัพธ์ดีหรือแย่ มันโมเดลเพียง treatment assignment เท่านั้น — คือ “กลไกการได้รับ treatment” ในชุดข้อมูลนี้ จำจุดนี้ไว้ให้ดี เพราะเป็นสิ่งที่คนเข้าใจผิดบ่อยที่สุด
อีกข้อเตือนหนึ่งตรงนี้: เรา ไม่เคยตัดสิน propensity score model จากค่า AUC (C-statistic) หรือ calibration ของมัน หน้าที่เดียวของมันคือ balance ตัวแปร ดังนั้นตัวชี้วัดที่สำคัญคือตาราง SMD ด้านล่าง ไม่ใช่ discrimination โมเดลที่แยก treated ออกจาก control ได้ สะอาดเกินไป คือสัญญาณอันตราย ไม่ใช่โมเดลที่ดีกว่า — มันบ่งว่า overlap แย่และอาจมี positivity violation คือสองกลุ่มแทบไม่คล้ายกันเลย

ขั้นที่ 3 — Overlap: คนที่เทียบกันได้อยู่ตรงไหน
ก่อนจะจับคู่ใคร เราวาด propensity score ของทั้งสองกลุ่มบนแกนเดียวกัน
ตรงที่การกระจายคะแนนของกลุ่ม treated และ control ทับกัน (overlap) คือมีคนที่เทียบกันได้อยู่ทั้งสองกลุ่ม — ผู้ป่วย treated ที่คะแนน 0.6 มี control ที่คะแนนใกล้ 0.6 ให้จับคู่ด้วย ส่วนตรงที่ไม่ทับกัน กลุ่มหนึ่งมีคะแนนที่อีกกลุ่มไปไม่ถึงเลย ตรงนั้นจึงไม่มีคู่ที่ซื่อสัตย์ให้จับ ช่องว่างที่ไม่ทับกันนี้คือ positivity (common-support) violation — บริเวณที่ไม่มีคนเทียบกันได้อยู่เลย — คือลายนิ้วมือของ confounding-by-indication ที่รุนแรง ถูกทำให้มองเห็นได้
ใน cohort ของเรา เส้นสองเส้นทับกันดีตลอดช่วงส่วนใหญ่ ซึ่งเป็นเงื่อนไขที่ทำให้จับคู่ได้พอดี ถ้าเส้นแทบไม่ทับกันเลย ไม่มี algorithm ฉลาดแค่ไหนจะกู้การเปรียบเทียบกลับมาได้ — เพราะเราหมดคนที่เทียบกันได้ นี่คืออีกด้านของประเด็นข้างบน: เส้นที่ทับกันดีแปลว่าคะแนน แยก treated ออกจาก control ได้ไม่ชัด (discrimination ต่ำ) — ซึ่งสำหรับ propensity score แล้ว นี่คือสิ่งที่เราต้องการพอดี
ขั้นที่ 4 — Nearest-neighbour matching
ทีนี้ถึงการจับคู่จริง โดยไม่ซ่อนอะไร เราใช้ greedy 1:1 nearest-neighbour matching without replacement ซึ่งง่ายกว่าชื่อของมัน:
- เรียงผู้ป่วยกลุ่ม treated ตาม propensity score จากมากไปน้อย
- ไล่ลงมาทีละคน สำหรับ treated แต่ละคน เลือก control ที่ ยังไม่ถูกใช้ ซึ่งคะแนนใกล้ที่สุด
- ล็อกคู่นั้นไว้ control คนนั้นจะถูกใช้ซ้ำไม่ได้อีก
ผู้ป่วย treated คนแรกมีคะแนน 0.760 ส่วน control ที่ว่างและใกล้ที่สุดคะแนน 0.753 ช่องว่างของทั้งคู่คือ |0.760 − 0.753| = 0.007 — สำหรับการจับคู่แล้ว แทบเป็นคนเดียวกัน ทำซ้ำจนครบทั้ง 69 คนในกลุ่ม treated เราจะได้ 69 คู่ที่จับได้ (เก็บคนไว้ 138 คน ที่เหลือของกลุ่ม control ถูกวางไว้ข้าง ๆ)
ด้านล่างนี้คุณลองรันเองได้ เทอร์มินัลเล่นซ้ำการวิเคราะห์ R จริงทีละบรรทัด ตั้งแต่ cohort ดิบไปจนถึงการ reframe เป็น case–control เลือกแท็บ ทำเอง (By hand) เพื่อดู greedy loop ที่ไล่ทีละขั้น หรือ ใช้ MatchIt (With MatchIt) เพื่อรันงานเดียวกันด้วยฟังก์ชันที่ทุกคนใช้จริง — การจับคู่เบื้องหลังเหมือนกันเป๊ะ พิมพ์ตามบรรทัดที่ไฮไลต์ (หรือกด Enter เพื่อเติมและรัน) แล้วแผง Stage จะแสดงตาราง ค่า หรือกราฟที่แต่ละขั้นสร้างขึ้น จำไว้ว่าไม่มีการคำนวณในเบราว์เซอร์ ทุกค่าถูกคำนวณด้วย R ไว้ก่อนแล้ว
แนวคิด 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.

ขั้นที่ 5 — ตรวจ balance เสมอ อย่าเดา
การจับคู่เป็นเพียง “ข้อเสนอ” ไม่ใช่ “การรับประกัน” วิธีเดียวที่จะรู้ว่ามันได้ผลคือ คำนวณ SMD ใหม่บนกลุ่มที่จับคู่แล้ว แล้วดู
เราใช้ไม้บรรทัดเดียวกันตลอด — ผลต่างของค่าเฉลี่ยหารด้วย standard deviation ของ กลุ่ม treated ซึ่งเป็นตัวหารเดียวกับที่ MatchIt ใช้เป็นค่าตั้งต้นพอดี เลขของสองแท็บจึงตรงกันเป๊ะ:
$$\text{SMD}=\dfrac{\bar{x}_{1}-\bar{x}_{0}}{s_{\text{treated}}}$$
ทุกช่องว่างลดลง severity — ตัวที่แย่ที่สุด — ตอนนี้ balanced สบาย ๆ ที่ 0.04 ส่วน age กับ sex ขยับมาเกือบสุดทางแต่ยังอยู่สูงกว่า 0.1 นิดหน่อย สังเกตทิศทางด้วย: SMD ของ sex กลับเครื่องหมาย (0.32 → −0.12) คือค่าเฉลี่ยของกลุ่ม treated ตอนนี้ ต่ำกว่า กลุ่ม control ที่จับคู่แล้วเล็กน้อย แทนที่จะสูงกว่า — สิ่งที่ยังพลาดเส้น 0.1 คือ ขนาด (magnitude) ไม่ใช่ทิศทาง นี่คือการจับคู่ที่ซื่อสัตย์: ดีขึ้น แต่ไม่สมบูรณ์ Greedy 1:1 nearest-neighbour matching ที่ไม่มี caliper ไม่เคยปฏิเสธคู่ไหนเลย — มันหยิบ control ที่ใกล้ที่สุดที่ยังเหลือ แม้ตัวที่ใกล้ที่สุดจะยังไม่ใกล้ — จึงบังคับหางของการกระจายให้ balanced ไม่ได้ ซึ่งเป็นเหตุผลว่าทำไม age กับ sex จึงยังเหลื่อมอยู่นิดหน่อย ในการวิเคราะห์จริง คุณอาจตอบสนองด้วยการรัด caliper ให้แคบลง เพิ่ม interaction เข้าไปในโมเดลคะแนน หรือปรับ imbalance ที่เหลือใน outcome model
มีรายละเอียดหนึ่งที่ควรหยุดดู: greedy loop ที่เขียนเองกับ MatchIt เลือก control ชุดเดียวกันเป๊ะ 69 คน MatchIt ไม่ได้ทำอะไรวิเศษ — มัน fit logistic score ตัวเดิม วน nearest-neighbour loop แบบเดียวกัน แล้วแถมตาราง balance มาให้ฟรี ๆ

เครื่องเดียวกัน: treatment เทียบกับ case–control
นี่คือของแถมที่คุ้มค่า ทุกอย่างข้างบนถูกวางกรอบรอบ ๆ treatment แต่เครื่องเดียวกันนี้จับคู่การศึกษาแบบ case–control ได้เลย — เรียนกลไกครั้งเดียว ใช้ได้ทั้งสอง design
เปลี่ยนแค่ป้ายกำกับ:
ในโค้ด สิ่งเดียวที่ขยับคือฝั่งซ้ายของสูตร: case ~ age + sex + sev แทน treat ~ age + sex + sev ขั้นตอน เดิม — สร้างคะแนน ดู overlap จับคู่ ตรวจ balance — ยกมาใช้ได้เหมือนเดิมทั้งหมด มีเพียงค่าตัวเลขของคะแนนที่ fit ได้เท่านั้นที่เปลี่ยน เพราะกำลังโมเดลตัวแปรคนละตัว
ความต่างนี้ลึกกว่าที่เห็น การโมเดล Pr(case | X) แทน Pr(treated | X) คือการโมเดล outcome ไม่ใช่ exposure ดังนั้นพูดอย่างเคร่งครัดแล้ว นี่คือ disease-risk (prognostic) score ไม่ใช่ propensity score: กลไกสี่ขั้นตอนเดียวกันแต่เล็งไปที่เป้าหมายคนละตัว (ความต่างระหว่าง propensity score กับ prognostic score เป็นอีกหัวข้อหนึ่งโดยเฉพาะ — ดู Propensity Score vs Prognosis Summary Score)
ยังมีอีกจุดละเอียดว่าคะแนนนี้ หมายถึงอะไร ตรงนี้ เพราะการสุ่มตัวอย่างแบบ case–control กำหนดอัตราส่วน case ต่อ control ไว้ตายตัวโดยการออกแบบ ค่า “ความน่าจะเป็นที่จะเป็นเคส” ที่ fit ได้จึงถูกสเกลด้วยอัตราส่วนนั้น — เป็นค่าภายในกลุ่มตัวอย่าง (within-sample) ไม่ใช่ population risk และนี่คือเหตุผลว่าทำไมค่าที่รายงานได้จึงเป็น odds ratio: odds ratio คือค่าเดียวที่คงที่ไม่เปลี่ยนภายใต้การสุ่มตามผลลัพธ์ (outcome-based sampling)
และมีข้อควรระวังที่ละเว้นไม่ได้: ข้อมูล case–control ที่จับคู่แล้วต้องใช้ การวิเคราะห์แบบจับคู่ (matched analysis) เพราะ case และ control ถูกจับคู่กันแบบรายบุคคลแล้ว odds ratio จึงต้องมาจาก conditional analysis — conditional logistic regression หรือ Mantel–Haenszel / McNemar estimator — ไม่ใช่ OR แบบไม่จับคู่ธรรมดา การวิเคราะห์คู่ที่จับแล้วเสมือนว่าเป็นอิสระต่อกัน (overmatching) จะทำให้ odds ratio เอนเข้าหา null
ดังนั้นสิ่งที่เปลี่ยนคือ คำถาม และความหมายของค่าที่ประมาณสุดท้าย: average treatment effect on the treated (ATT) ในกรอบหนึ่ง และ odds ratio ของ exposure–disease — จาก conditional matched analysis — ในอีกกรอบหนึ่ง

ความเข้าใจผิดที่พบบ่อย
1. คิดว่า propensity score ทำนาย outcome
มันไม่ได้ทำนาย คะแนนนี้โมเดล treatment assignment เท่านั้น — ไม่เคยเห็น outcome เลย คะแนนสูงแปลว่า “มีแนวโน้มจะได้รับการรักษา” ไม่ใช่ “มีแนวโน้มจะมีผลลัพธ์ดี” การสับสนสองอย่างนี้ทำลายทั้งวิธีการอย่างเงียบ ๆ
2. ข้ามการตรวจ balance หลังจับคู่
การจับคู่เป็นข้อเสนอ ไม่ใช่ข้อพิสูจน์ ถ้าคุณไม่คำนวณ SMD ใหม่หลังจากนั้น คุณจะไม่มีทางรู้เลยว่ามันได้ผลไหม อย่าเดา — ตรวจ
3. คาดหวัง balance ที่สมบูรณ์แบบ — หรือคาดหวังจากมันมากเกินไป
SMD ของ age และ sex ของเราลงมาใกล้ 0.1 ไม่ใช่ศูนย์ และนั่นเป็นเรื่องปกติ ที่สำคัญกว่านั้น การจับคู่ balance ได้เฉพาะ confounders ที่คุณวัดและใส่เข้าโมเดล เท่านั้น มันไม่ช่วยอะไรกับ unmeasured confounding เลย — ตัวแปรที่คุณไม่เคยบันทึกไว้ย่อม balance ไม่ได้ และไม่มีการจับคู่ใดจะกู้คุณจากมันได้
4. เลือกวิธีก่อนตั้งคำถาม
ว่าค่าประมาณของคุณควรเป็น ATT หรือ odds ratio หรือคุณควรใช้ matching หรือไม่ตั้งแต่แรก ล้วนตามมาจากคำถามที่คุณถาม ตั้งคำถามก่อน แล้วให้คำถามเลือกวิธี — ไม่ใช่กลับกัน
บทสรุป
Propensity score matching คือแนวคิดเล็ก ๆ ที่ซื่อสัตย์อันเดียว ทำซ้ำ ๆ: เปลี่ยน confounder ที่วัดได้ทุกตัวให้เป็นความน่าจะเป็นที่ทำนายไว้ค่าเดียว จับคู่คนที่มีเลขนี้ตรงกัน แล้ว ตรวจ ว่าการจับคู่ทำให้สองกลุ่มเทียบกันได้จริงหรือไม่ ทำแบบนั้น การเปรียบเทียบตัวต่อตัวที่เคยมี bias ก็กลายเป็นการเปรียบเทียบที่แฟร์ — สำหรับ confounders ที่วัดได้ และเฉพาะตัวเหล่านั้นเท่านั้น
Key takeaways
- Confounding มาก่อน ใน cohort จำลองของเรา กลุ่ม treated อายุมากและป่วยหนักกว่า (SMD 0.62, 0.32, 0.71) การเทียบตรง ๆ จะวัดตัวโรค ไม่ใช่ตัวยา
- Propensity score คือคะแนนทำนาย — $\Pr(\text{treated} \mid X)$ จาก logistic model — ที่บีบ confounders ที่วัดได้ทั้งหมดให้เหลือเลขเดียว มันโมเดล treatment ไม่ใช่ outcome
- Overlap ทำให้จับคู่ได้ จากนั้น nearest-neighbour matching จับคู่ treated แต่ละคนกับ control ที่ว่างและใกล้ที่สุด (ที่นี่ได้ 69 คู่)
- ตรวจ balance เสมอ หลังจับคู่ severity ลงมาที่ 0.04 และ age ที่ 0.15 — ดีขึ้น แต่ไม่สมบูรณ์ — โดยใช้ SD ของกลุ่ม treated เป็นไม้บรรทัด greedy loop ที่ทำเองกับ MatchIt เลือก control ชุดเดียวกันเป๊ะ 69 คน
- เครื่องเดียวกันใช้กับ case–control ได้ เปลี่ยนป้ายกำกับกลุ่ม แล้วค่าประมาณกลายเป็น odds ratio ของ exposure–disease แทน ATT การจับคู่ balance ได้เฉพาะสิ่งที่คุณวัด — ไม่มีทาง balance unmeasured confounding