การวัดซ้ำ: เลือกวิธีตามชนิดของผลลัพธ์ และตามว่าต้องการคำตอบระดับประชากรหรือระดับผู้ป่วย

Clinical Epidemiology ResearchMethodology and Research Design THUniqcret doctor knowledges TH
การวัดซ้ำ: เลือกวิธีตามชนิดของผลลัพธ์ และตามว่าต้องการคำตอบระดับประชากรหรือระดับผู้ป่วย
On this page

Read the English version

บทคัดย่อ

เมื่อผู้ป่วยกลุ่มเดียวกันถูกวัดผลหลายครั้ง ค่าที่วัดของผู้ป่วยคนเดียวกันจะสัมพันธ์กัน และการวิเคราะห์ต้องเลือกตระกูลของแบบจำลองก่อน คำถามสองข้อเป็นตัวตัดสิน คือชนิดของผลลัพธ์ และการทดลองต้องการผลระดับประชากร (population-averaged effect) หรือผลระดับบุคคล (subject-specific effect) ผลระดับบุคคลเปรียบเทียบผู้ป่วยที่ได้รับการรักษากับผู้ป่วยที่ไม่ได้รับ โดยทั้งสองมีโอกาสตอบสนองพื้นฐานเท่ากัน สำหรับผลลัพธ์ต่อเนื่อง (continuous outcome) ที่วิเคราะห์ด้วย identity link (สร้างแบบจำลองให้ค่าเฉลี่ยโดยตรง) เป้าหมายทั้งสองตรงกัน linear mixed model (การถดถอยที่มีผลสุ่มหรือ random effect ระดับผู้ป่วย) กับ generalized estimating equations (GEE) จึงประมาณสัมประสิทธิ์ของการรักษาตัวเดียวกัน แต่สำหรับผลลัพธ์ทวิภาค (binary outcome) ที่วิเคราะห์ด้วยแบบจำลองลอจิสติก ทั้งสองต่างกัน ในการทดลองจำลองผู้ป่วย 300 คน GEE ให้ odds ratio เท่ากับ 1.48 ส่วน mixed logistic model ที่มี random intercept (การเลื่อนโอกาสตอบสนองพื้นฐานระดับผู้ป่วย) ให้ 1.85 แต่ละค่าประมาณปริมาณที่ถูกต้องคนละปริมาณ บทความนี้อธิบายตารางตัดสินใจ สูตรประมาณที่เชื่อม odds ratio ทั้งสองค่า ข้อสมมติเรื่องข้อมูลที่หายไปของแต่ละแบบจำลอง และโค้ด


ภาพสรุป ข้อมูลจำลอง

สามครั้งที่วัด odds ratio สองค่า

คณะกรรมการกำกับการทดลองกำลังทบทวนการทดลองแบบสุ่มในผู้ใหญ่ 300 คนที่มีอาการเรื้อรังชนิดหนึ่ง ครึ่งหนึ่งได้รับการรักษาใหม่และอีกครึ่งหนึ่งเป็นกลุ่มควบคุม ในสัปดาห์ที่ 4, 8 และ 12 แพทย์ประเมินผู้ป่วยแต่ละคนว่าเป็นผู้ที่ตอบสนอง (responder) หรือไม่ การทดลองยังบันทึกคะแนนอาการแบบต่อเนื่อง ซึ่งยิ่งต่ำยิ่งดี ในสัปดาห์ที่ 0, 4, 8 และ 12

ในการทดลองจำลองนี้ นักสถิติรายงาน odds ratio ของการรักษาต่อผลลัพธ์การตอบสนองสองค่า คือ 1.48 จากแบบจำลองหนึ่ง และ 1.85 จากอีกแบบจำลองหนึ่ง ประธานถามว่าค่าไหนควรอยู่ในรายงาน

ไม่มีค่าใดผิด ค่าแรกมาจาก สมการประมาณค่าวางนัยทั่วไป (generalized estimating equations, GEE) ซึ่งเปรียบเทียบการตอบสนองเฉลี่ยของประชากรที่ได้รับการรักษาทั้งหมดกับประชากรกลุ่มควบคุมทั้งหมด ค่าที่สองมาจาก แบบจำลองผสมเชิงเส้นวางนัยทั่วไป (generalized linear mixed model, GLMM) ซึ่งเปรียบเทียบผู้ป่วยสองคนที่มีโอกาสตอบสนองพื้นฐานเท่ากัน คนหนึ่งได้รับการรักษาและอีกคนไม่ได้รับ ก่อนอ่าน odds ratio ทั้งสองค่า ทีมต้องตัดสินใจก่อนว่าการทดลองนี้ต้องการตอบคำถามแบบใด

การวัดซ้ำ และเหตุผลที่ต้องใส่เวลาไว้ในแบบจำลอง

ค่าที่วัดสองครั้งจากผู้ป่วยคนเดียวกันมักคล้ายกันมากกว่าค่าจากผู้ป่วยต่างคน เพราะผู้ป่วยแต่ละคนมีระดับพื้นฐานและการตอบสนองเป็นของตนเอง การถดถอยแบบธรรมดาสมมติว่าทุกแถวของข้อมูลเป็นอิสระต่อกัน จึงมองคะแนนอาการ 1,200 ค่า ซึ่งมาจากการวัดสี่ครั้งในผู้ป่วย 300 คน เป็นคน 1,200 คนที่แยกจากกัน ค่าคลาดเคลื่อนมาตรฐาน (standard error) จึงผิด และสำหรับการเปรียบเทียบระหว่างผู้ป่วย เช่นการรักษา มักเล็กเกินจริง

เวลาต้องเข้าสู่แบบจำลองอย่างชัดเจนด้วย โดยมักใช้ตัวบ่งชี้สำหรับแต่ละครั้งที่วัด ปฏิกิริยาสัมพันธ์ (interaction) ระหว่างกลุ่มการรักษากับครั้งที่วัดทำให้ผลของการรักษาต่างกันได้ตามครั้งที่วัด การอ่านสัมประสิทธิ์ปฏิกิริยาสัมพันธ์เหล่านั้นเป็นหัวข้อของตอนถัดไปในชุดนี้ ตอนนี้เลือกตระกูลของแบบจำลองที่รองรับสัมประสิทธิ์เหล่านั้น

RM-ANOVA เทียบกับ linear mixed model

เครื่องมือดั้งเดิมสำหรับผลลัพธ์ต่อเนื่อง (continuous outcome) คือ การวิเคราะห์ความแปรปรวนแบบวัดซ้ำ (repeated-measures analysis of variance, RM-ANOVA) ซึ่งทดสอบกลุ่มการรักษา ครั้งที่วัด และปฏิกิริยาสัมพันธ์ของทั้งสองด้วย F test ที่แยกความแปรปรวนระหว่างผู้ป่วยออกจากความแปรปรวนภายในผู้ป่วย วิธีนี้ต้องการข้อมูลครบและสมดุล ผู้ป่วยที่ขาดการวัดเพียงครั้งเดียวจึงถูกตัดออกทั้งคน และยังสมมติ ความเท่ากันของความแปรปรวนของผลต่างระหว่างครั้งที่วัด (sphericity) คือความแปรปรวนของผลต่างระหว่างสองครั้งที่วัดใดๆ เท่ากันทุกคู่ [1]

sphericity ไม่ค่อยเป็นจริงในการทดลอง เพราะคะแนนมักกระจายกว้างขึ้นเมื่อติดตามนานขึ้น ค่า epsilon ของ Greenhouse-Geisser วัดระดับที่เบี่ยงเบนไป โดยเท่ากับ 1 เมื่อมี sphericity และลดลงเมื่อเบี่ยงเบนมากขึ้น สำหรับคะแนนอาการในการทดลองจำลอง ค่า epsilon ของ Greenhouse-Geisser ที่ประมาณได้คือ 0.87 แสดงว่า sphericity ไม่เป็นจริง F test ที่ไม่ปรับสำหรับครั้งที่วัดและสำหรับปฏิกิริยาสัมพันธ์ระหว่างกลุ่มการรักษากับครั้งที่วัด ซึ่งเป็นการทดสอบภายในผู้ป่วย จึงให้ค่า P ที่เล็กเกินจริง และต้องปรับองศาอิสระ

แบบจำลองผสมเชิงเส้น (linear mixed model, LMM) ใช้วิธีต่างออกไปโดยสร้างแบบจำลองเส้นทางของผู้ป่วยแต่ละคน [2] แบบจำลองเพิ่มผลสุ่ม (random effect) ซึ่งคือส่วนเบี่ยงเบนระดับผู้ป่วยจากเส้นเฉลี่ย เช่น ค่าตัดแกนสุ่ม (random intercept) คือระดับเริ่มต้นของผู้ป่วยแต่ละคน และความชันสุ่ม (random slope) คืออัตราการเปลี่ยนแปลงของผู้ป่วยแต่ละคน แบบจำลองนี้ประมาณค่าด้วยความควรจะเป็น (likelihood) ใช้ทุกค่าที่ผู้ป่วยแต่ละคนมี และยอมให้เวลานัดต่างกันได้ เมื่อขาดการวัดบางครั้ง แบบจำลองยังใช้ได้หากถูกต้องและข้อมูลเป็น ข้อมูลขาดหายแบบสุ่ม (missing at random, MAR) หมายความว่าการขาดการวัดขึ้นกับข้อมูลที่สังเกตแล้วเท่านั้น

Intercept และ slope แบบ fixed และแบบ random

fixed intercept และ fixed slope บังคับให้ผู้ป่วยทุกคนในกลุ่มเดียวกันอยู่บนเส้นเดียว random intercept ทำให้ระดับเริ่มต้นต่างกันได้ และ random slope ทำให้อัตราการเปลี่ยนแปลงต่างกันได้ด้วย คู่มือชุด mixed model อธิบายทางเลือกเหล่านี้และรูปแบบความแปรปรวนร่วมที่ตามมา

คำถามสองข้อที่ตัดสินแบบจำลอง และสิ่งที่ GEE กับ GLMM แต่ละวิธีมุ่งประมาณ

คำถามแรกคือชนิดของผลลัพธ์ ได้แก่ผลลัพธ์ต่อเนื่อง ผลลัพธ์ทวิภาค (binary outcome) หรือผลลัพธ์เรียงอันดับ (ordinal outcome) ซึ่งหมายถึงหมวดหมู่ที่เรียงลำดับ เช่น ไม่มี เล็กน้อย ปานกลาง และรุนแรง RM-ANOVA รองรับเฉพาะผลลัพธ์ต่อเนื่อง และการถดถอยลอจิสติกธรรมดารองรับเพียงสองหมวด ผลลัพธ์เรียงอันดับที่วัดซ้ำจึงต้องใช้แบบจำลองสำหรับข้อมูลเรียงอันดับ แบบจำลองแต่ละแบบยังมี ฟังก์ชันเชื่อมโยง (link) คือสเกลที่ตัวแปรร่วม (covariate) ออกฤทธิ์ identity link สร้างแบบจำลองให้ค่าเฉลี่ยโดยตรง และ logit link สร้างแบบจำลองให้ log odds

คำถามที่สองคือการทดลองต้องการผลแบบใด ผลระดับประชากร (population-averaged effect) ซึ่งเรียกอีกชื่อหนึ่งว่าผลแบบเฉลี่ยข้ามประชากร (marginal) เปรียบเทียบผลลัพธ์เฉลี่ย เช่นสัดส่วนของผู้ที่ตอบสนอง ระหว่างประชากรที่ได้รับการรักษากับประชากรกลุ่มควบคุม ผลระดับบุคคล (subject-specific effect) เปรียบเทียบผู้ป่วยที่ได้รับการรักษากับผู้ป่วยที่ไม่ได้รับ โดยทั้งสองมี random effect เท่ากัน คือมีโอกาสตอบสนองพื้นฐานเท่ากัน

การรักษาถูกสุ่มระหว่างผู้ป่วย จึงไม่มีผู้ป่วยคนใดถูกสังเกตภายใต้ทั้งสองกลุ่ม การเปรียบเทียบระดับบุคคลจึงถูกสร้างขึ้นโดยแบบจำลอง ซึ่งตรึง random effect ไว้ GEE มุ่งไปที่ผลระดับประชากร และ mixed model มุ่งไปที่ผลระดับบุคคล [3]

GEE สร้างแบบจำลองค่าเฉลี่ยของประชากรในแต่ละครั้งที่วัด และจัดการความสัมพันธ์ด้วย ความสัมพันธ์สมมติ (working correlation) ซึ่งเป็นรูปแบบที่สมมติว่าค่าที่วัดของผู้ป่วยคนเดียวกันสัมพันธ์กันอย่างไร [4] รูปแบบ exchangeable ที่ใช้ด้านล่างสมมติว่าทุกคู่ของครั้งที่วัดสัมพันธ์กันเท่ากัน

ค่าคลาดเคลื่อนมาตรฐานมาจาก ตัวประมาณแบบแซนด์วิช (sandwich estimator) ซึ่งยังใช้ได้เมื่อ working correlation ผิด หากแบบจำลองค่าเฉลี่ยถูกและมีผู้ป่วยจำนวนมาก ทั้งนี้ sandwich estimator เรียกอีกชื่อหนึ่งว่า robust standard error ดังนั้น working correlation ที่ไม่ดีจึงทำให้เสียความแม่นยำ แต่ไม่ทำให้เสียความถูกต้อง

GLMM เพิ่ม random effect เข้าไปใน generalized linear model เช่นการถดถอยลอจิสติก สัมประสิทธิ์ของแบบจำลองนี้เปรียบเทียบผู้ป่วยที่มี random effect เท่ากัน ไม่ใช่ผู้ป่วยคนเดียวกันก่อนและหลังการรักษา

ตารางตัดสินใจ: ชนิดของผลลัพธ์เทียบกับคำถาม

แต่ละช่องระบุแบบจำลองที่เป้าหมายตรงกับแถวและคอลัมน์ของมัน RM-ANOVA ใช้ได้เฉพาะแถวผลลัพธ์ต่อเนื่อง และเฉพาะเมื่อข้อมูลครบและสมดุล
ผลลัพธ์ในแต่ละครั้งที่วัดคำถามระดับประชากรคำถามระดับบุคคล
ผลลัพธ์ต่อเนื่อง เช่นคะแนนอาการGEE ที่ใช้ identity link หรือ GLS (แบบจำลองเชิงเส้นแบบ marginal ที่สร้างแบบจำลองความแปรปรวนร่วมระหว่างครั้งที่วัดโดยตรง)Linear mixed model (LMM)
ผลลัพธ์ทวิภาค เช่นตอบสนองหรือไม่GEE ที่ใช้ logit linkLogistic GLMM ที่มี random intercept
ผลลัพธ์เรียงอันดับ เช่น ไม่มี เล็กน้อย ปานกลาง รุนแรงOrdinal GEE (cumulative logit คือ log odds ของการอยู่ในหมวดนั้นหรือต่ำกว่า)Ordinal mixed model: meologit ใน Stata, clmm ในแพ็กเกจ ordinal ของ R

เหตุใดคะแนนจึงตรงกัน แต่ผู้ที่ตอบสนอง (responder) ไม่ตรงกัน

เมื่อใช้ identity link การเฉลี่ยเส้นของผู้ป่วยแต่ละคนทั่วทั้งประชากรให้เส้นของผู้ป่วยที่ random effect เป็นศูนย์ random effect เฉลี่ยได้ศูนย์ สัมประสิทธิ์ fixed ของ LMM จึงเป็นทั้งระดับบุคคลและระดับประชากร GEE ที่ใช้ identity link มุ่งไปที่สัมประสิทธิ์ชุดเดียวกัน และการเลือกระหว่างสองวิธีขึ้นกับประสิทธิภาพและข้อมูลที่หายไป ไม่ได้ขึ้นกับความหมาย

logit link ทำให้ความเท่ากันนี้เสียไป ผู้ป่วยแต่ละคนมีเส้นโค้งรูปตัว S ที่เปลี่ยน log odds เป็นความน่าจะเป็นของการตอบสนอง การเฉลี่ยเส้นโค้งเช่นนี้จำนวนมากที่เลื่อนไปด้วย random intercept ต่างกันให้เส้นโค้งที่แบนกว่า และ odds ratio ที่อ่านจากเส้นนั้นใกล้ 1 มากกว่า odds ratio ที่เปรียบเทียบผู้ป่วยสองคนที่มี random intercept เท่ากัน นี่คือ ความไม่ยุบรวมได้ (non-collapsibility) ของ odds ratio และเกิดขึ้นได้แม้ในการทดลองแบบสุ่มที่สมบูรณ์แบบ

ปริมาณเป้าหมายของการประมาณ (estimand) คือปริมาณที่แน่ชัดซึ่งการทดลองตั้งใจจะประมาณ แนวทาง ICH E9(R1) อธิบาย estimand ด้วยคุณลักษณะห้าประการ ได้แก่ การรักษา ประชากร ตัวแปรผลลัพธ์ วิธีจัดการเหตุการณ์ระหว่างการศึกษา (intercurrent event) และค่าสรุประดับประชากร เช่น odds ratio เหตุการณ์ระหว่างการศึกษาคือเหตุการณ์หลังการสุ่ม เช่นการหยุดการรักษา ที่มีผลต่อการตีความผลลัพธ์

odds ratio สองค่าในที่นี้มีคุณลักษณะสี่ประการแรกเหมือนกัน และต่างกันเพียงที่ค่าสรุป

odds ratio ระดับบุคคลเป็น odds ratio แบบมีเงื่อนไข (conditional) เพราะตรึง random intercept ของผู้ป่วยไว้ ส่วน odds ratio ระดับประชากรเป็น odds ratio แบบเฉลี่ยข้ามประชากร (marginal) เพราะเฉลี่ยค่าทั่วทั้งผู้ป่วย

non-collapsibility ไม่ได้จำกัดอยู่เฉพาะ odds ratio เท่านั้น hazard ratio ซึ่งเปรียบเทียบว่าเหตุการณ์ เช่นการเสียชีวิต เกิดเร็วเพียงใดในสองกลุ่มตลอดช่วงติดตาม ก็มีคุณสมบัตินี้เช่นกัน

odds ratio แบบมีเงื่อนไขกับแบบระดับประชากร รวมทั้ง hazard ratio ทั้งสองแบบ อาจต่างกันได้แม้ไม่มีตัวกวนเลย ในการทดลองแบบสุ่ม การปรับด้วยปัจจัยพยากรณ์โรคที่มีผลแรงมักทำให้ odds ratio ขยับห่างจาก 1 มากขึ้น นั่นคือการเปลี่ยน estimand ไม่ใช่การกำจัดความลำเอียง

random intercept ทำหน้าที่คล้ายปัจจัยพยากรณ์โรคที่แรงซึ่งไม่เคยถูกวัด และ GLMM ปรับตามมัน สำหรับรายละเอียดเพิ่มเติม ดูบทความเรื่องความยุบรวมได้ (collapsibility) (บทความภาษาอังกฤษ) และเรื่องความแปรปรวนร่วมแบบมีเงื่อนไขเทียบกับแบบ marginal

ห่างกันเท่าไร: สูตรประมาณการลดทอน

odds ratio ระดับประชากรถูกลดทอน (attenuated) หมายถึงถูกดึงเข้าหา 1 เมื่อเทียบกับ odds ratio ระดับบุคคล สำหรับแบบจำลองลอจิสติกที่มี random intercept log odds ratio ทั้งสองเชื่อมกันด้วยสูตรประมาณ [3]:

$$\beta_{PA} \approx \frac{\beta_{SS}}{\sqrt{1 + c^2\,\sigma^2}}$$

ในสูตรนี้ $\beta_{SS}$ คือ log odds ratio ระดับบุคคลจาก GLMM, $\beta_{PA}$ คือ log odds ratio ระดับประชากร และ $\sigma^2$ คือความแปรปรวนของ random intercept บนสเกล log odds ค่าคงที่คือ $c^2 = 0.346$ ซึ่งมาจากการประมาณเส้นโค้งลอจิสติกด้วยเส้นโค้งปกติที่ปรับสเกลแล้ว สูตรนี้จึงเป็นการประมาณ ไม่ใช่เอกลักษณ์ที่เป็นจริงเสมอ

ความแปรปรวนระหว่างผู้ป่วยยิ่งมาก ช่องว่างยิ่งกว้าง และเมื่อ $\sigma^2$ เป็น 0 ค่าทั้งสองจะตรงกัน สคริปต์ Stata และ R ด้านล่างคำนวณตัวหารด้วยค่าคงที่เดียวกัน ในบรรทัด sqrt(1 + 0.346*var_u) โดย var_u คือ $\sigma^2$

ตัวอย่างคำนวณ: สูตรนี้กับค่าที่ใช้สร้างข้อมูลจำลองของการทดลอง

ตัวอย่างคำนวณที่มีขั้นตอนเชิงตัวเลข การตอบสนองในการทดลองจำลองถูกสร้างด้วย odds ratio ระดับบุคคลเท่ากับ 2.5 และความแปรปรวนของ random intercept เท่ากับ 4 บนสเกล log odds สำหรับผู้ป่วยกลุ่มควบคุมที่ random intercept เท่ากับ 0 ค่า log odds ของการตอบสนองคือ -1.5, -1.0 และ -0.5 ในสัปดาห์ที่ 4, 8 และ 12 สูตรแปลง odds ratio ระดับบุคคลเป็นค่าประมาณของ odds ratio ระดับประชากร และการเฉลี่ยข้ามผู้ป่วยให้ค่าที่แน่นอนไว้เทียบ

  1. ขั้นที่ 1: ย้ายไปบนสเกลลอการิทึม

    \[ \beta_{SS} = \ln(2.5) = 0.916 \]

    log odds ratio ระดับบุคคลเท่ากับ 0.916

  2. ขั้นที่ 2: คำนวณตัวหาร

    \[ \sqrt{1 + 0.346 \times 4} = \sqrt{2.384} = 1.544 \]

    เมื่อ c² = 0.346 และความแปรปรวนของ random intercept เท่ากับ 4 ตัวหารเท่ากับ 1.544

  3. ขั้นที่ 3: หาร แล้วกลับไปเป็น odds ratio

    \[ \exp(0.916 / 1.544) = \exp(0.593) = 1.81 \]

    ค่าประมาณจากสูตรของ odds ratio ระดับประชากรเท่ากับ 1.81

  4. ขั้นที่ 4: เฉลี่ยข้ามผู้ป่วยเพื่อหาค่าที่แน่นอน

    \[ \frac{0.412 / (1 - 0.412)}{0.285 / (1 - 0.285)} = 1.76 \]

    การเฉลี่ยความน่าจะเป็นของการตอบสนองในแต่ละกลุ่มตามการแจกแจงปกติของ random intercept ที่มีความแปรปรวน 4 ให้ค่า 0.285 ในกลุ่มควบคุม และ 0.412 ในกลุ่มที่ได้รับการรักษา ในสัปดาห์ที่ 4 คิดเป็น odds ratio เท่ากับ 1.76 การคำนวณแบบเดียวกันให้ 1.75 ในสัปดาห์ที่ 8 และ 1.74 ในสัปดาห์ที่ 12 ความน่าจะเป็นสองค่านี้ได้จากการอินทิเกรตเชิงตัวเลข (numerical integration) เช่นการเรียกฟังก์ชัน integrate() เพียงบรรทัดเดียวใน R ไม่ได้คำนวณด้วยมือ

ผลลัพธ์: สูตรประมาณให้ 1.81 ส่วน odds ratio ระดับประชากรที่แน่นอนเท่ากับ 1.76, 1.75 และ 1.74 ในสัปดาห์ที่ 4, 8 และ 12 หรือราว 1.75 ตลอดสามครั้งที่วัด ค่าที่แน่นอนยังขึ้นกับโอกาสตอบสนองพื้นฐาน จึงต่างกันเล็กน้อยตามสัปดาห์ ขณะที่ค่าจากสูตรไม่เปลี่ยน สูตรให้ค่าใกล้เคียงแต่ไม่แม่นตรง จึงใช้บอกขนาดของช่องว่าง ไม่ใช่แปลง odds ratio ค่าหนึ่งเป็นอีกค่าหนึ่ง

การทดลองจำลอง: GEE และ GLMM กับผู้ที่ตอบสนองกลุ่มเดียวกัน

การทดลองจำลองมีผู้ป่วย 300 คน กลุ่มละ 150 คน ประเมินในสัปดาห์ที่ 4, 8 และ 12 รวมเป็นผลลัพธ์ทวิภาค 900 ค่า การตอบสนองถูกสร้างจากแบบจำลองลอจิสติกที่มี random intercept ของผู้ป่วยแต่ละคน ผู้ป่วยจึงมีโอกาสตอบสนองพื้นฐานต่างกัน แบบจำลองที่ประมาณทั้งสองปรับด้วยครั้งที่วัดเป็นตัวแปรหมวดหมู่ และรายงาน odds ratio ของการรักษาหนึ่งค่าตลอดสามครั้งที่วัด

odds ratio ค่าเดียวตลอดสามครั้งที่วัดสมมติว่าผลของการรักษาคงที่ทุกครั้งที่วัด ปฏิกิริยาสัมพันธ์ระหว่างกลุ่มการรักษากับครั้งที่วัด ซึ่งเป็นหัวข้อของตอนที่ 2 ผ่อนข้อสมมตินี้ได้ ในการจำลองนี้ odds ratio ระดับบุคคลเท่ากันทุกครั้งที่วัด และตัวอย่างคำนวณข้างต้นแสดงว่า odds ratio ระดับประชากรที่แน่นอนเปลี่ยนเพียงจาก 1.76 เป็น 1.74

ข้อมูลจำลอง ค่าประมาณจากการรัน Stata ผลจากการรัน R ต่างไม่เกิน 0.01 ช่องผลลัพธ์ด้านล่างแสดงการประมาณ GEE ของ Stata และการประมาณ GLMM ของ R และ odds ratio แต่ละค่าคือ exponential ของสัมประสิทธิ์ของการรักษาที่พิมพ์ไว้ คอลัมน์สุดท้ายคือค่าที่ใช้สร้างข้อมูลการตอบสนอง และช่วงความเชื่อมั่น 95% แต่ละช่วงครอบคลุมค่าของตน CI คือช่วงความเชื่อมั่น (confidence interval)
ปริมาณค่าประมาณ (CI 95%)ค่าที่ใช้สร้างข้อมูลการตอบสนอง
odds ratio ระดับประชากร: GEE, logit link, exchangeable working correlation, sandwich standard error1.48 (1.04 ถึง 2.10)ราว 1.75 (ค่าที่แน่นอน ตลอดสามครั้งที่วัด)
odds ratio ระดับบุคคล: logistic GLMM ที่มี random intercept1.85 (1.07 ถึง 3.21)2.5
ความแปรปรวนของ random intercept จาก GLMM (สเกล log odds)3.434

ตัวอย่างคำนวณ: จาก odds ratio ของ GLMM ที่ประมาณได้ ไปเป็น odds ratio ของ GEE

ข้อมูลจำลอง ต่อไปใช้สูตรเดียวกันกับแบบจำลองสองแบบที่ประมาณจากการทดลองจำลอง logistic GLMM ประมาณ odds ratio ระดับบุคคลได้ 1.85 และความแปรปรวนของ random intercept ได้ 3.43 ส่วน GEE ประมาณ odds ratio ระดับประชากรได้ 1.48 สูตรทำนาย odds ratio ของ GEE จากตัวเลขสองค่าของ GLMM

  1. ขั้นที่ 1: ย้ายไปบนสเกลลอการิทึม

    \[ \beta_{SS} = \ln(1.85) = 0.615 \]

    log odds ratio ระดับบุคคลเท่ากับ 0.615 คือสัมประสิทธิ์ของการรักษาในผลลัพธ์ของ R ด้านล่าง

  2. ขั้นที่ 2: คำนวณตัวหาร

    \[ \sqrt{1 + 0.346 \times 3.43} = \sqrt{2.187} = 1.479 \]

    เมื่อ c² = 0.346 และความแปรปรวนของ random intercept ที่ประมาณได้เท่ากับ 3.43 ตัวหารเท่ากับ 1.479

  3. ขั้นที่ 3: หาร แล้วกลับไปเป็น odds ratio

    \[ \exp(0.615 / 1.479) = 1.52 \]

    odds ratio ระดับประชากรที่ทำนายได้เท่ากับ 1.52

  4. ขั้นที่ 4: เทียบกับ GEE

    \[ 0.615 / 0.393 = 1.56 \]

    GEE ประมาณได้ 1.48 ซึ่งเป็น log odds ratio เท่ากับ 0.393 คือสัมประสิทธิ์ของการรักษาในผลลัพธ์ของ Stata ด้านล่าง log odds ratio ของ GLMM เป็น 1.56 เท่าของ GEE ใกล้เคียงกับตัวหาร 1.479

ผลลัพธ์: จาก GLMM ที่ประมาณได้ สูตรทำนาย odds ratio ระดับประชากรได้ 1.52 และ GEE ประมาณได้ 1.48 จากข้อมูลจำลองชุดเดียวกัน สูตรจึงเชื่อมแบบจำลองที่ประมาณทั้งสองได้ใกล้เคียง แต่สูตรอาศัยการประมาณ จึงใช้บอกขนาดของช่องว่าง ไม่ใช่แปลง odds ratio ค่าหนึ่งเป็นอีกค่าหนึ่งได้อย่างแม่นตรง

ข้อมูลจำลอง เลื่อนความแปรปรวนของ random intercept จาก 0 ไป 9 โดย odds ratio ระดับบุคคลคงที่ที่ค่าประมาณจาก GLMM คือ 1.85 ขณะที่ odds ratio ระดับประชากร ทั้งค่าประมาณจากสูตรและค่าที่แน่นอน ลดลงเข้าหา 1 ที่ความแปรปรวนที่ประมาณได้ 3.43 สูตรให้ 1.52 เส้นค่าที่แน่นอนอยู่ต่ำกว่าเล็กน้อย และจุดแสดงค่าประมาณจาก GEE คือ 1.48 การที่ค่าที่แน่นอนอยู่ใกล้ค่าประมาณจาก GEE เป็นลักษณะของการทดลองจำลองนี้ ไม่ใช่กฎทั่วไป

การอ่านค่าประมาณทั้งสอง

GEE ประมาณ working correlation แบบ exchangeable ระหว่างการตอบสนองสองครั้งของผู้ป่วยคนเดียวกันได้ 0.36 (0.3589 ในการรัน R ซึ่งพิมพ์ไว้ในชื่อ alpha) การตอบสนองของผู้ป่วยคนเดียวกันจึงสัมพันธ์กันอย่างชัดเจน

GLMM ประมาณความแปรปรวนของ random intercept ได้ 3.43 ความแปรปรวนนี้ยิ่งมาก odds ratio ทั้งสองค่ายิ่งห่างกัน ดังที่กราฟโต้ตอบด้านบนแสดง

ในโค้ดด้านล่าง xtgee และ geeglm() ประมาณ GEE ส่วน melogit และ glmer() ประมาณ GLMM สคริปต์แต่ละภาษารันทั้งสองแบบจำลอง ช่องผลลัพธ์ของ Stata แสดงการประมาณ GEE และช่องผลลัพธ์ของ R แสดงการประมาณ GLMM สำหรับผลลัพธ์เรียงอันดับ meologit ใน Stata และ clmm() ในแพ็กเกจ ordinal ของ R ประมาณแบบจำลองระดับบุคคล

Stata: สคริปต์วิเคราะห์การทดลองและผลลัพธ์ของ GEE

โค้ด Stata w4_sim.do
* Simulated longitudinal trial: a symptom score (lower is better) at weeks 0, 4, 8, 12 and a clinician-rated
* responder (0/1) at weeks 4, 8, 12, in 300 participants randomised 1:1 to treatment or control.
* Simulated data: not evidence about any real drug or patient.
* The simulated trial file (not published) is read from a folder two levels above this script.
version 18
clear all
set more off
set linesize 120

* Record whether each command used below is available (all are built into Stata).
foreach c in mixed contrast lincom testparm xtgee melogit anova {
    capture which `c'
    if _rc == 0 {
        display "VERIFY `c' available"
    }
    else {
        display "VERIFY `c' missing"
    }
}

* Load the simulated trial file (long format: one row per participant-visit).
import delimited using ../../datasets/W4/W4.csv, clear asdouble
label define armlab 0 "control" 1 "treatment"
label values arm armlab

* Sample sizes.
quietly count
display "CANON w4.n_obs " r(N)
egen byte first = tag(id)
quietly count if first
display "CANON w4.n " r(N)
quietly count if first & arm == 0
display "CANON w4.n.ctrl " r(N)
quietly count if first & arm == 1
display "CANON w4.n.trt " r(N)
quietly count if !missing(responder)
display "CANON w4.n_obs_resp " r(N)

* ---------------------------------------------------------------- 2 x T means table
* Observed mean score in each arm at each visit (the cells a saturated mean model reproduces).
table arm week, statistic(mean score) statistic(sd score) nformat(%7.2f)
forvalues a = 0/1 {
    local g = cond(`a' == 1, "trt", "ctrl")
    forvalues v = 0/3 {
        quietly summarize score if arm == `a' & visit == `v'
        display "CANON w4.mean.`g'.wk" 4*`v' " " strtrim(string(r(mean), "%12.4f"))
    }
}

* ---------------------------------------------------------------- mixed model, arm x visit
* Categorical visit, arm-by-visit interaction, random intercept and random slope on week (REML).
mixed score i.arm##i.visit || id: week, covariance(unstructured) reml
* beta1 = 1.arm: the arm difference at week 0 (zero in truth, because arms are randomised).
lincom 1.arm
display "CANON w4.lmm.b1 " strtrim(string(r(estimate), "%12.4f"))
display "CANON w4.lmm.b1.se " strtrim(string(r(se), "%12.4f"))
display "CANON w4.lmm.b1.lo " strtrim(string(r(lb), "%12.4f"))
display "CANON w4.lmm.b1.hi " strtrim(string(r(ub), "%12.4f"))
* beta3 at each visit = 1.arm#v.visit: the difference in differences from week 0.
display "CANON w4.lmm.b3.wk4 " strtrim(string(_b[1.arm#1.visit], "%12.4f"))
display "CANON w4.lmm.b3.wk8 " strtrim(string(_b[1.arm#2.visit], "%12.4f"))
lincom 1.arm#3.visit
display "CANON w4.lmm.b3.wk12 " strtrim(string(r(estimate), "%12.4f"))
display "CANON w4.lmm.b3.wk12.se " strtrim(string(r(se), "%12.4f"))
display "CANON w4.lmm.b3.wk12.lo " strtrim(string(r(lb), "%12.4f"))
display "CANON w4.lmm.b3.wk12.hi " strtrim(string(r(ub), "%12.4f"))
* Arm contrast at each visit (beta1 + beta3 at that visit): what a reader usually wants.
contrast r.arm@visit, effects
forvalues v = 0/3 {
    if `v' == 0 {
        quietly lincom 1.arm
    }
    else {
        quietly lincom 1.arm + 1.arm#`v'.visit
    }
    display "CANON w4.lmm.diff.wk" 4*`v' " " strtrim(string(r(estimate), "%12.4f"))
    display "CANON w4.lmm.diff.wk" 4*`v' ".se " strtrim(string(r(se), "%12.4f"))
    display "CANON w4.lmm.diff.wk" 4*`v' ".lo " strtrim(string(r(lb), "%12.4f"))
    display "CANON w4.lmm.diff.wk" 4*`v' ".hi " strtrim(string(r(ub), "%12.4f"))
}
* Joint Wald test that the arm difference is the same at every visit (3 interaction terms).
testparm 1.arm#i.visit
display "CANON w4.lmm.int.chi2 " strtrim(string(r(chi2), "%12.4f"))
display "CANON w4.lmm.int.df " r(df)
display "CANON w4.lmm.int.p " strtrim(string(r(p), "%12.4f"))
* Variance components on the SD scale (slope SD is per week).
display "CANON w4.lmm.sd_int " strtrim(string(exp(_b[lns1_1_2:_cons]), "%12.4f"))
display "CANON w4.lmm.sd_slope " strtrim(string(exp(_b[lns1_1_1:_cons]), "%12.4f"))
display "CANON w4.lmm.corr " strtrim(string(tanh(_b[atr1_1_1_2:_cons]), "%12.4f"))
display "CANON w4.lmm.sd_resid " strtrim(string(exp(_b[lnsig_e:_cons]), "%12.4f"))

* ---------------------------------------------------------------- constrained baseline (cLDA)
* Same random-effects structure, but one common baseline mean for both arms.
generate byte trt_wk4 = arm * (visit == 1)
generate byte trt_wk8 = arm * (visit == 2)
generate byte trt_wk12 = arm * (visit == 3)
mixed score i.visit trt_wk4 trt_wk8 trt_wk12 || id: week, covariance(unstructured) reml
lincom trt_wk12
display "CANON w4.clda.diff.wk12 " strtrim(string(r(estimate), "%12.4f"))
display "CANON w4.clda.diff.wk12.se " strtrim(string(r(se), "%12.4f"))
display "CANON w4.clda.diff.wk12.lo " strtrim(string(r(lb), "%12.4f"))
display "CANON w4.clda.diff.wk12.hi " strtrim(string(r(ub), "%12.4f"))

* ---------------------------------------------------------------- week-12 ANCOVA and change score
* Baseline score copied to every row of the participant.
bysort id (visit): generate double score0 = score[1]
generate double change = score - score0
* ANCOVA: week-12 score on arm, adjusted for the baseline score (OLS).
regress score i.arm score0 if visit == 3
lincom 1.arm
display "CANON w4.ancova.wk12 " strtrim(string(r(estimate), "%12.4f"))
display "CANON w4.ancova.wk12.se " strtrim(string(r(se), "%12.4f"))
display "CANON w4.ancova.wk12.lo " strtrim(string(r(lb), "%12.4f"))
display "CANON w4.ancova.wk12.hi " strtrim(string(r(ub), "%12.4f"))
display "CANON w4.ancova.baseline_coef " strtrim(string(_b[score0], "%12.4f"))
* Change score: (week-12 minus week-0) on arm, no baseline adjustment (OLS).
regress change i.arm if visit == 3
lincom 1.arm
display "CANON w4.change.wk12 " strtrim(string(r(estimate), "%12.4f"))
display "CANON w4.change.wk12.se " strtrim(string(r(se), "%12.4f"))
display "CANON w4.change.wk12.lo " strtrim(string(r(lb), "%12.4f"))
display "CANON w4.change.wk12.hi " strtrim(string(r(ub), "%12.4f"))

* ---------------------------------------------------------------- repeated-measures ANOVA
* Classical RM-ANOVA (sphericity assumed), with the Greenhouse-Geisser correction.
anova score arm / id|arm visit arm#visit, repeated(visit)
* Term 4 is arm#visit; e(gg1) and e(hf1) are the epsilons for the repeated variable.
display "CANON w4.rmanova.f_int " strtrim(string(e(F_4), "%12.4f"))
display "CANON w4.rmanova.df1 " e(df_4)
display "CANON w4.rmanova.df2 " e(df_r)
display "CANON w4.rmanova.p_int " strtrim(string(Ftail(e(df_4), e(df_r), e(F_4)), "%12.4f"))
display "CANON w4.rmanova.gg_eps " strtrim(string(e(gg1), "%12.4f"))
display "CANON w4.rmanova.hf_eps " strtrim(string(e(hf1), "%12.4f"))
display "CANON w4.rmanova.p_int_gg " strtrim(string(Ftail(e(gg1)*e(df_4), e(gg1)*e(df_r), e(F_4)), "%12.4f"))

* ---------------------------------------------------------------- binary responder
* Observed responder proportion by arm and visit (weeks 4, 8, 12).
table arm week if visit >= 1, statistic(mean responder) nformat(%6.3f)
forvalues a = 0/1 {
    local g = cond(`a' == 1, "trt", "ctrl")
    forvalues v = 1/3 {
        quietly summarize responder if arm == `a' & visit == `v'
        display "CANON w4.resp.prop.`g'.wk" 4*`v' " " strtrim(string(r(mean), "%12.4f"))
    }
}
xtset id visit
* GEE: population-averaged OR, exchangeable working correlation, robust (sandwich) SE.
xtgee responder i.arm i.visit if visit >= 1, family(binomial) link(logit) corr(exchangeable) vce(robust)
matrix R = e(R)
scalar gee_b = _b[1.arm]
scalar gee_se = _se[1.arm]
display "CANON w4.gee.logor " strtrim(string(gee_b, "%12.4f"))
display "CANON w4.gee.logor.se " strtrim(string(gee_se, "%12.4f"))
display "CANON w4.gee.or " strtrim(string(exp(gee_b), "%12.4f"))
display "CANON w4.gee.or.lo " strtrim(string(exp(gee_b - invnormal(0.975)*gee_se), "%12.4f"))
display "CANON w4.gee.or.hi " strtrim(string(exp(gee_b + invnormal(0.975)*gee_se), "%12.4f"))
display "CANON w4.gee.alpha " strtrim(string(R[1,2], "%12.4f"))
* GLMM: subject-specific OR from a random-intercept logistic model (12-point adaptive quadrature).
melogit responder i.arm i.visit if visit >= 1 || id:, intpoints(12)
scalar glmm_b = _b[1.arm]
scalar glmm_se = _se[1.arm]
scalar var_u = _b[/var(_cons[id])]
display "CANON w4.glmm.logor " strtrim(string(glmm_b, "%12.4f"))
display "CANON w4.glmm.logor.se " strtrim(string(glmm_se, "%12.4f"))
display "CANON w4.glmm.or " strtrim(string(exp(glmm_b), "%12.4f"))
display "CANON w4.glmm.or.lo " strtrim(string(exp(glmm_b - invnormal(0.975)*glmm_se), "%12.4f"))
display "CANON w4.glmm.or.hi " strtrim(string(exp(glmm_b + invnormal(0.975)*glmm_se), "%12.4f"))
display "CANON w4.glmm.var_u " strtrim(string(var_u, "%12.4f"))
* Attenuation: beta_PA is approximately beta_SS / sqrt(1 + 0.346 sigma^2).
scalar atten = sqrt(1 + 0.346*var_u)
display "CANON w4.atten.factor " strtrim(string(atten, "%12.4f"))
display "CANON w4.atten.pred_or_pa " strtrim(string(exp(glmm_b/atten), "%12.4f"))
display "CANON w4.atten.obs_ratio " strtrim(string(glmm_b/gee_b, "%12.4f"))

* ---------------------------------------------------------------- simulated truth
* The values the trial was simulated with, and the targets they imply for the estimates above.
* Score: mean 50 - 0.5 x week in control and 50 - 1 x week in treatment; random intercept SD 6,
* random slope SD 0.5 per week, intercept-slope correlation -0.2, residual SD 4.
scalar t_mu0 = 50
scalar t_slope_ctrl = -0.5
scalar t_slope_trt = -1
scalar t_sd_int = 6
scalar t_sd_slope = 0.5
scalar t_corr = -0.2
scalar t_sd_resid = 4
foreach g in ctrl trt {
    forvalues v = 0/3 {
        display "CANON w4.truth.score.mean.`g'.wk" 4*`v' " " strtrim(string(t_mu0 + t_slope_`g'*4*`v', "%12.4f"))
    }
}
* true arm difference at each visit; beta1 (week 0) and beta3 (difference in differences from week 0)
forvalues v = 0/3 {
    display "CANON w4.truth.score.diff.wk" 4*`v' " " strtrim(string((t_slope_trt - t_slope_ctrl)*4*`v' + 0, "%12.4f"))
}
display "CANON w4.truth.score.b1 " strtrim(string(0, "%12.4f"))
forvalues v = 1/3 {
    display "CANON w4.truth.score.b3.wk" 4*`v' " " strtrim(string((t_slope_trt - t_slope_ctrl)*4*`v' + 0, "%12.4f"))
}
* ANCOVA, change score and constrained baseline all target the week-12 difference in a randomised trial
display "CANON w4.truth.score.ancova_wk12 " strtrim(string((t_slope_trt - t_slope_ctrl)*12, "%12.4f"))
display "CANON w4.truth.score.change_wk12 " strtrim(string((t_slope_trt - t_slope_ctrl)*12, "%12.4f"))
display "CANON w4.truth.score.clda_wk12 " strtrim(string((t_slope_trt - t_slope_ctrl)*12, "%12.4f"))
display "CANON w4.truth.score.sd_int " strtrim(string(t_sd_int, "%12.4f"))
display "CANON w4.truth.score.sd_slope " strtrim(string(t_sd_slope, "%12.4f"))
display "CANON w4.truth.score.corr " strtrim(string(t_corr, "%12.4f"))
display "CANON w4.truth.score.sd_resid " strtrim(string(t_sd_resid, "%12.4f"))
* implied covariance of the four visits, its SD at each visit and the true Greenhouse-Geisser epsilon,
* tr(SP)^2 / (3 tr(SPSP)) with P the centring matrix
mata:
G = (st_numscalar("t_sd_int")^2, st_numscalar("t_corr")*st_numscalar("t_sd_int")*st_numscalar("t_sd_slope") \ st_numscalar("t_corr")*st_numscalar("t_sd_int")*st_numscalar("t_sd_slope"), st_numscalar("t_sd_slope")^2)
Z = (J(4, 1, 1), (0 \ 4 \ 8 \ 12))
S = Z*G*Z' + st_numscalar("t_sd_resid")^2*I(4)
P = I(4) - J(4, 4, 1/4)
st_matrix("t_sdwk", sqrt(diagonal(S))')
st_numscalar("t_gg", trace(S*P)^2 / (3*trace(S*P*S*P)))
end
forvalues v = 0/3 {
    display "CANON w4.truth.score.sd.wk" 4*`v' " " strtrim(string(t_sdwk[1, `v' + 1], "%12.4f"))
}
display "CANON w4.truth.score.gg_eps " strtrim(string(t_gg, "%12.4f"))
* Responder: logit P(response) = alpha(week) + log(2.5) x treatment + u, u normal with variance 4;
* alpha = -1.5, -1, -0.5 at weeks 4, 8, 12 (control log odds for a participant whose u is 0)
scalar t_or_ss = 2.5
scalar t_var_u = 4
matrix t_alpha = (-1.5, -1, -0.5)
display "CANON w4.truth.responder.or_ss " strtrim(string(t_or_ss, "%12.4f"))
display "CANON w4.truth.responder.logor_ss " strtrim(string(ln(t_or_ss), "%12.4f"))
display "CANON w4.truth.responder.var_u " strtrim(string(t_var_u, "%12.4f"))
display "CANON w4.truth.responder.sd_u " strtrim(string(sqrt(t_var_u), "%12.4f"))
forvalues v = 1/3 {
    display "CANON w4.truth.responder.alpha.wk" 4*`v' " " strtrim(string(t_alpha[1, `v'], "%12.4f"))
}
* the attenuation approximation at the true variance
scalar t_atten = sqrt(1 + 0.346*t_var_u)
display "CANON w4.truth.responder.atten_factor " strtrim(string(t_atten, "%12.4f"))
display "CANON w4.truth.responder.approx_logor_pa " strtrim(string(ln(t_or_ss)/t_atten, "%12.4f"))
display "CANON w4.truth.responder.approx_or_pa " strtrim(string(exp(ln(t_or_ss)/t_atten), "%12.4f"))
* exact population-averaged response probabilities and odds ratio by week: the logistic curve averaged over
* the normal random intercept (trapezoid rule on a fine grid)
mata:
real scalar pa_prob(real scalar a, real scalar v)
{
    real colvector u, f
    real scalar h
    h = 0.0005
    u = range(-40, 40, h)
    f = invlogit(a :+ u) :* normalden(u, 0, sqrt(v))
    return((sum(f) - (f[1] + f[rows(f)])/2)*h)
}
A = st_matrix("t_alpha")
b = ln(st_numscalar("t_or_ss"))
v = st_numscalar("t_var_u")
R = J(3, 3, .)
for (j = 1; j <= 3; j++) {
    R[j, 1] = pa_prob(A[j], v)
    R[j, 2] = pa_prob(A[j] + b, v)
    R[j, 3] = (R[j, 2]/(1 - R[j, 2])) / (R[j, 1]/(1 - R[j, 1]))
}
st_matrix("t_pa", R)
end
forvalues v = 1/3 {
    display "CANON w4.truth.responder.exact_prob.ctrl.wk" 4*`v' " " strtrim(string(t_pa[`v', 1], "%12.4f"))
    display "CANON w4.truth.responder.exact_prob.trt.wk" 4*`v' " " strtrim(string(t_pa[`v', 2], "%12.4f"))
    display "CANON w4.truth.responder.exact_or_pa.wk" 4*`v' " " strtrim(string(t_pa[`v', 3], "%12.4f"))
}
* one summary over the three weeks: the geometric mean of the three exact odds ratios
display "CANON w4.truth.responder.exact_or_pa_mean " strtrim(string(exp((ln(t_pa[1, 3]) + ln(t_pa[2, 3]) + ln(t_pa[3, 3]))/3), "%12.4f"))
ผลลัพธ์จากการรัน w4_sim.log
. * GEE: population-averaged OR, exchangeable working correlation, robust (sandwich) SE.
. xtgee responder i.arm i.visit if visit >= 1, family(binomial) link(logit) corr(exchangeable) vce(robust)

Iteration 1:  Tolerance = .0005793
Iteration 2:  Tolerance = 2.742e-06
Iteration 3:  Tolerance = 5.583e-09

GEE population-averaged model                        Number of obs    =    900
Group variable: id                                   Number of groups =    300
Family: Binomial                                     Obs per group:
Link:   Logit                                                     min =      3
Correlation: exchangeable                                         avg =    3.0
                                                                  max =      3
                                                     Wald chi2(3)     =  21.23
Scale parameter = 1                                  Prob > chi2      = 0.0001

                                     (Std. err. adjusted for clustering on id)
------------------------------------------------------------------------------
             |               Robust
   responder | Coefficient  std. err.      z    P>|z|     [95% conf. interval]
-------------+----------------------------------------------------------------
         arm |
  treatment  |   .3932917   .1782105     2.21   0.027     .0440055    .7425778
             |
       visit |
          2  |   .0980098   .1337247     0.73   0.464    -.1640858    .3601053
          3  |   .5054341     .12858     3.93   0.000      .253422    .7574462
             |
       _cons |  -.6476142   .1508334    -4.29   0.000    -.9432422   -.3519862
------------------------------------------------------------------------------
ข้อมูลจำลอง สคริปต์วิเคราะห์การทดลองจำลองทั้งชุด รวมถึงแบบจำลองของคะแนนอาการที่ตอนที่ 2 ของชุดนี้อ่าน ส่วนช่องผลลัพธ์แสดงเฉพาะการประมาณ GEE ของผลลัพธ์การตอบสนอง สัมประสิทธิ์ของการรักษาและช่วง 95% อยู่บนสเกล log odds และ exponential ของค่าเหล่านี้คือ odds ratio 1.48 (1.04 ถึง 2.10) ในตารางข้างต้น ชุดข้อมูลผู้ป่วย 300 คน (W4.csv) เป็นข้อมูลจำลองและไม่ได้เผยแพร่ จึงให้รันคำสั่งเดียวกันกับข้อมูลการวัดซ้ำของคุณเอง แล้วเทียบโครงสร้างของผลลัพธ์ ไม่ใช่ตัวเลขเหล่านี้ ในสคริปต์ บรรทัดที่พิมพ์ VERIFY บันทึกว่าแต่ละคำสั่งติดตั้งอยู่ และบรรทัดที่พิมพ์ CANON พิมพ์ผลบรรทัดละหนึ่งค่า ปัดเป็นทศนิยม 4 ตำแหน่ง

R: การวิเคราะห์เดียวกันและผลลัพธ์ของ GLMM

โค้ด R w4_sim_r.R
# Simulated longitudinal trial: a symptom score (lower is better) at weeks 0, 4, 8, 12 and a clinician-rated
# responder (0/1) at weeks 4, 8, 12, in 300 participants randomised 1:1 to treatment or control.
# Simulated data: not evidence about any real drug or patient.
# The simulated trial file (not published) is read from a folder two levels above this script.

# Packages: lme4 (mixed models), geepack (GEE); install into the user library when missing.
for (p in c("lme4", "geepack")) {
  if (!requireNamespace(p, quietly = TRUE)) {
    try(install.packages(p, repos = "https://cloud.r-project.org"))
  }
  cat(sprintf("VERIFY %s %s\n", p, if (requireNamespace(p, quietly = TRUE)) "available" else "missing"))
}
suppressPackageStartupMessages({
  library(lme4)
  library(geepack)
})

# One CANON line per quantity: 4 decimals for estimates, integers for counts.
canon <- function(key, x, int = FALSE) {
  cat("CANON w4.", key, " ", if (int) format(x) else formatC(x, format = "f", digits = 4), "\n", sep = "")
}
z975 <- qnorm(0.975)

# Load the simulated trial file (long format: one row per participant-visit).
w4 <- read.csv(file.path("..", "..", "datasets", "W4", "W4.csv"))
w4 <- w4[order(w4$id, w4$visit), ]
w4$visitf <- factor(w4$visit)

# Sample sizes.
first <- !duplicated(w4$id)
canon("n_obs", nrow(w4), int = TRUE)
canon("n", sum(first), int = TRUE)
canon("n.ctrl", sum(first & w4$arm == 0), int = TRUE)
canon("n.trt", sum(first & w4$arm == 1), int = TRUE)
canon("n_obs_resp", sum(!is.na(w4$responder)), int = TRUE)

# ---------------------------------------------------------------- 2 x T means table
# Observed mean score in each arm at each visit (the cells a saturated mean model reproduces).
means <- tapply(w4$score, list(arm = w4$arm, week = w4$week), mean)
print(round(means, 2))
for (a in 0:1) for (wk in c(0, 4, 8, 12)) {
  canon(sprintf("mean.%s.wk%d", if (a == 1) "trt" else "ctrl", wk), means[as.character(a), as.character(wk)])
}

# ---------------------------------------------------------------- mixed model, arm x visit
# Categorical visit, arm-by-visit interaction, random intercept and random slope on week (REML).
lmm <- lmer(score ~ arm * visitf + (week | id), data = w4, REML = TRUE)
print(summary(lmm), correlation = FALSE)
b <- fixef(lmm)
V <- as.matrix(vcov(lmm))
# Wald estimate, SE and 95% CI (normal) of a linear combination L'b.
lc <- function(L) {
  est <- sum(L * b)
  se <- sqrt(drop(t(L) %*% V %*% L))
  c(est = est, se = se, lo = est - z975 * se, hi = est + z975 * se)
}
L_of <- function(names) {
  L <- setNames(numeric(length(b)), names(b))
  L[names] <- 1
  L
}
# beta1 = arm: the arm difference at week 0 (zero in truth, because arms are randomised).
r <- lc(L_of("arm"))
canon("lmm.b1", r["est"]); canon("lmm.b1.se", r["se"]); canon("lmm.b1.lo", r["lo"]); canon("lmm.b1.hi", r["hi"])
# beta3 at each visit = arm:visit: the difference in differences from week 0.
canon("lmm.b3.wk4", b["arm:visitf1"])
canon("lmm.b3.wk8", b["arm:visitf2"])
r <- lc(L_of("arm:visitf3"))
canon("lmm.b3.wk12", r["est"]); canon("lmm.b3.wk12.se", r["se"])
canon("lmm.b3.wk12.lo", r["lo"]); canon("lmm.b3.wk12.hi", r["hi"])
# Arm contrast at each visit (beta1 + beta3 at that visit): what a reader usually wants.
for (v in 0:3) {
  r <- lc(L_of(if (v == 0) "arm" else c("arm", paste0("arm:visitf", v))))
  wk <- 4 * v
  canon(sprintf("lmm.diff.wk%d", wk), r["est"])
  canon(sprintf("lmm.diff.wk%d.se", wk), r["se"])
  canon(sprintf("lmm.diff.wk%d.lo", wk), r["lo"])
  canon(sprintf("lmm.diff.wk%d.hi", wk), r["hi"])
}
# Joint Wald test that the arm difference is the same at every visit (3 interaction terms).
idx <- paste0("arm:visitf", 1:3)
chi2 <- drop(t(b[idx]) %*% solve(V[idx, idx]) %*% b[idx])
canon("lmm.int.chi2", chi2)
canon("lmm.int.df", length(idx), int = TRUE)
canon("lmm.int.p", pchisq(chi2, length(idx), lower.tail = FALSE))
# Variance components on the SD scale (slope SD is per week).
vc <- VarCorr(lmm)$id
canon("lmm.sd_int", attr(vc, "stddev")[["(Intercept)"]])
canon("lmm.sd_slope", attr(vc, "stddev")[["week"]])
canon("lmm.corr", attr(vc, "correlation")[1, 2])
canon("lmm.sd_resid", sigma(lmm))

# ---------------------------------------------------------------- constrained baseline (cLDA)
# Same random-effects structure, but one common baseline mean for both arms.
w4$trt_wk4 <- w4$arm * (w4$visit == 1)
w4$trt_wk8 <- w4$arm * (w4$visit == 2)
w4$trt_wk12 <- w4$arm * (w4$visit == 3)
clda <- lmer(score ~ visitf + trt_wk4 + trt_wk8 + trt_wk12 + (week | id), data = w4, REML = TRUE)
est <- fixef(clda)[["trt_wk12"]]
se <- sqrt(as.matrix(vcov(clda))["trt_wk12", "trt_wk12"])
canon("clda.diff.wk12", est); canon("clda.diff.wk12.se", se)
canon("clda.diff.wk12.lo", est - z975 * se); canon("clda.diff.wk12.hi", est + z975 * se)

# ---------------------------------------------------------------- week-12 ANCOVA and change score
# One row per participant: baseline and week-12 scores side by side.
wide <- data.frame(id = w4$id[w4$visit == 0], arm = w4$arm[w4$visit == 0],
                   score0 = w4$score[w4$visit == 0], score12 = w4$score[w4$visit == 3])
# ANCOVA: week-12 score on arm, adjusted for the baseline score (OLS).
anc <- lm(score12 ~ arm + score0, data = wide)
print(summary(anc))
ci <- confint(anc)["arm", ]
canon("ancova.wk12", coef(anc)[["arm"]])
canon("ancova.wk12.se", coef(summary(anc))["arm", "Std. Error"])
canon("ancova.wk12.lo", ci[[1]]); canon("ancova.wk12.hi", ci[[2]])
canon("ancova.baseline_coef", coef(anc)[["score0"]])
# Change score: (week-12 minus week-0) on arm, no baseline adjustment (OLS).
chg <- lm(I(score12 - score0) ~ arm, data = wide)
ci <- confint(chg)["arm", ]
canon("change.wk12", coef(chg)[["arm"]])
canon("change.wk12.se", coef(summary(chg))["arm", "Std. Error"])
canon("change.wk12.lo", ci[[1]]); canon("change.wk12.hi", ci[[2]])

# ---------------------------------------------------------------- repeated-measures ANOVA
# Classical RM-ANOVA (sphericity assumed): arm between participants, visit within.
rma <- aov(score ~ factor(arm) * visitf + Error(factor(id) / visitf), data = w4)
tab <- summary(rma)[["Error: factor(id):visitf"]][[1]]
print(tab)
row <- grep("factor(arm):visitf", rownames(tab), fixed = TRUE)
f_int <- tab[row, "F value"]; df1 <- tab[row, "Df"]; df2 <- tab[nrow(tab), "Df"]
canon("rmanova.f_int", f_int)
canon("rmanova.df1", df1, int = TRUE)
canon("rmanova.df2", df2, int = TRUE)
canon("rmanova.p_int", pf(f_int, df1, df2, lower.tail = FALSE))
# Greenhouse-Geisser and Huynh-Feldt epsilons from the within-arm pooled covariance of the 4 visits.
Y <- matrix(w4$score, ncol = 4, byrow = TRUE)
grp <- w4$arm[w4$visit == 0]
S <- ((sum(grp == 0) - 1) * cov(Y[grp == 0, ]) + (sum(grp == 1) - 1) * cov(Y[grp == 1, ])) / (nrow(Y) - 2)
C <- qr.Q(qr(cbind(1, poly(1:4, 3))))[, -1]
M <- t(C) %*% S %*% C
gg <- sum(diag(M))^2 / (3 * sum(diag(M %*% M)))
hf <- (nrow(Y) * 3 * gg - 2) / (3 * (nrow(Y) - 2 - 3 * gg))
canon("rmanova.gg_eps", gg)
canon("rmanova.hf_eps", min(hf, 1))
canon("rmanova.p_int_gg", pf(f_int, gg * df1, gg * df2, lower.tail = FALSE))

# ---------------------------------------------------------------- binary responder
d1 <- w4[w4$visit >= 1, ]
d1$visitf <- factor(d1$visit)
# Observed responder proportion by arm and visit (weeks 4, 8, 12).
props <- tapply(d1$responder, list(arm = d1$arm, week = d1$week), mean)
print(round(props, 3))
for (a in 0:1) for (wk in c(4, 8, 12)) {
  canon(sprintf("resp.prop.%s.wk%d", if (a == 1) "trt" else "ctrl", wk), props[as.character(a), as.character(wk)])
}
# GEE: population-averaged OR, exchangeable working correlation, robust (sandwich) SE.
gee <- geeglm(responder ~ arm + visitf, id = id, data = d1, family = binomial, corstr = "exchangeable")
print(summary(gee))
gee_b <- coef(gee)[["arm"]]
gee_se <- summary(gee)$coefficients["arm", "Std.err"]
canon("gee.logor", gee_b); canon("gee.logor.se", gee_se)
canon("gee.or", exp(gee_b))
canon("gee.or.lo", exp(gee_b - z975 * gee_se)); canon("gee.or.hi", exp(gee_b + z975 * gee_se))
canon("gee.alpha", gee$geese$alpha[[1]])
# GLMM: subject-specific OR from a random-intercept logistic model (12-point adaptive quadrature).
glmm <- glmer(responder ~ arm + visitf + (1 | id), data = d1, family = binomial, nAGQ = 12)
print(summary(glmm), correlation = FALSE)
glmm_b <- fixef(glmm)[["arm"]]
glmm_se <- sqrt(as.matrix(vcov(glmm))["arm", "arm"])
var_u <- VarCorr(glmm)$id[1, 1]
canon("glmm.logor", glmm_b); canon("glmm.logor.se", glmm_se)
canon("glmm.or", exp(glmm_b))
canon("glmm.or.lo", exp(glmm_b - z975 * glmm_se)); canon("glmm.or.hi", exp(glmm_b + z975 * glmm_se))
canon("glmm.var_u", var_u)
# Attenuation: beta_PA is approximately beta_SS / sqrt(1 + 0.346 sigma^2).
atten <- sqrt(1 + 0.346 * var_u)
canon("atten.factor", atten)
canon("atten.pred_or_pa", exp(glmm_b / atten))
canon("atten.obs_ratio", glmm_b / gee_b)

# ---------------------------------------------------------------- simulated truth
# The values the trial was simulated with, read from the simulation's settings file (not published), and
# the targets they imply for the estimates above.
tj <- jsonlite::read_json(file.path("..", "..", "datasets", "W4", "truth.json"))
sc <- tj$score
for (g in c("ctrl", "trt")) for (wk in c(0, 4, 8, 12)) {
  canon(sprintf("truth.score.mean.%s.wk%d", g, wk), sc$mu0 + sc[[paste0("slope_", g, "_per_week")]] * wk)
}
# true arm difference at each visit; beta1 (week 0) and beta3 (difference in differences from week 0)
for (wk in c(0, 4, 8, 12)) {
  canon(sprintf("truth.score.diff.wk%d", wk), (sc$slope_trt_per_week - sc$slope_ctrl_per_week) * wk + 0)
}
canon("truth.score.b1", sc$beta1_baseline_difference)
for (wk in c(4, 8, 12)) canon(sprintf("truth.score.b3.wk%d", wk), sc$beta3_did[[paste0("wk", wk)]])
# ANCOVA, change score and constrained baseline all target the week-12 difference in a randomised trial
canon("truth.score.ancova_wk12", sc$ancova_wk12_effect)
canon("truth.score.change_wk12", sc$change_score_wk12_effect)
canon("truth.score.clda_wk12", sc$constrained_baseline_wk12_effect)
canon("truth.score.sd_int", sc$sd_int); canon("truth.score.sd_slope", sc$sd_slope)
canon("truth.score.corr", sc$corr_int_slope); canon("truth.score.sd_resid", sc$sd_resid)
# implied covariance of the four visits, its SD at each visit and the true Greenhouse-Geisser epsilon,
# tr(SP)^2 / (3 tr(SPSP)) with P the centring matrix
cv <- sc$corr_int_slope * sc$sd_int * sc$sd_slope
G <- matrix(c(sc$sd_int^2, cv, cv, sc$sd_slope^2), 2)
Z <- cbind(1, c(0, 4, 8, 12))
S_true <- Z %*% G %*% t(Z) + diag(sc$sd_resid^2, 4)
P <- diag(4) - matrix(1 / 4, 4, 4)
sd_wk <- sqrt(diag(S_true))
for (k in 1:4) canon(sprintf("truth.score.sd.wk%d", 4 * (k - 1)), sd_wk[k])
canon("truth.score.gg_eps", sum(diag(S_true %*% P))^2 / (3 * sum(diag(S_true %*% P %*% S_true %*% P))))
# Responder: logit P(response) = alpha(week) + log(OR_SS) x treatment + u, u normal with variance var_u
rs <- tj$responder
alpha <- unlist(rs$alpha_ctrl_wk4_wk8_wk12)
canon("truth.responder.or_ss", rs$or_ss)
canon("truth.responder.logor_ss", log(rs$or_ss))
canon("truth.responder.var_u", rs$var_u)
canon("truth.responder.sd_u", sqrt(rs$var_u))
for (k in 1:3) canon(sprintf("truth.responder.alpha.wk%d", 4 * k), alpha[k])
# the attenuation approximation at the true variance
atten_t <- sqrt(1 + 0.346 * rs$var_u)
canon("truth.responder.atten_factor", atten_t)
canon("truth.responder.approx_logor_pa", log(rs$or_ss) / atten_t)
canon("truth.responder.approx_or_pa", exp(log(rs$or_ss) / atten_t))
# exact population-averaged response probabilities and odds ratio by week: the logistic curve averaged over
# the normal random intercept (numerical integration)
pa_prob <- function(a, v) integrate(function(u) plogis(a + u) * dnorm(u, 0, sqrt(v)), -Inf, Inf,
                                    rel.tol = 1e-10)$value
pa_or <- numeric(3)
for (k in 1:3) {
  p0 <- pa_prob(alpha[k], rs$var_u); p1 <- pa_prob(alpha[k] + log(rs$or_ss), rs$var_u)
  pa_or[k] <- (p1 / (1 - p1)) / (p0 / (1 - p0))
  canon(sprintf("truth.responder.exact_prob.ctrl.wk%d", 4 * k), p0)
  canon(sprintf("truth.responder.exact_prob.trt.wk%d", 4 * k), p1)
  canon(sprintf("truth.responder.exact_or_pa.wk%d", 4 * k), pa_or[k])
}
# one summary over the three weeks: the geometric mean of the three exact odds ratios
canon("truth.responder.exact_or_pa_mean", exp(mean(log(pa_or))))
ผลลัพธ์จากการรัน w4_sim_r.log
> glmm <- glmer(responder ~ arm + visitf + (1 | id),
+     data = d1, family = binomial, nAGQ = 12)

> print(summary(glmm), correlation = FALSE)
Generalized linear mixed model fit by maximum likelihood (Adaptive
  Gauss-Hermite Quadrature, nAGQ = 12) [glmerMod]
 Family: binomial  ( logit )
Formula: responder ~ arm + visitf + (1 | id)
   Data: d1

      AIC       BIC    logLik -2*log(L)  df.resid
   1126.3    1150.3    -558.1    1116.3       895

Scaled residuals:
   Min     1Q Median     3Q    Max
-1.609 -0.539 -0.341  0.582  1.669

Random effects:
 Groups Name        Variance Std.Dev.
 id     (Intercept) 3.43     1.85
Number of obs: 900, groups:  id, 300

Fixed effects:
            Estimate Std. Error z value Pr(>|z|)
(Intercept)   -1.016      0.240   -4.24  2.2e-05 ***
arm            0.615      0.281    2.19  0.02859 *
visitf2        0.154      0.210    0.73  0.46401
visitf3        0.795      0.213    3.74  0.00019 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
ข้อมูลจำลอง แบบจำลองเดียวกันใน R (geepack และ lme4) ส่วนช่องผลลัพธ์แสดงเฉพาะการประมาณ GLMM ของผลลัพธ์การตอบสนอง สัมประสิทธิ์ของการรักษา 0.615 คือ log odds ratio ระดับบุคคล และ exponential ของค่านี้คือ odds ratio 1.85 ใต้หัวข้อ Random effects คอลัมน์ Variance (3.43) คือความแปรปรวนของ random intercept และคอลัมน์ Std.Dev. คือรากที่สองของค่านั้น ซึ่งไม่ใช่ odds ratio แม้ทั้งสองจะปัดได้ค่าเดียวกันในที่นี้ เช่นเดียวกับสคริปต์ Stata บรรทัด VERIFY บันทึกว่าแต่ละแพ็กเกจติดตั้งอยู่ และบรรทัด CANON พิมพ์ผลบรรทัดละหนึ่งค่า

การขาดการวัด: แต่ละแบบจำลองสมมติอะไร

ผู้ป่วยบางคนพลาดการวัดบางครั้งหรือออกจากการศึกษา และตระกูลของแบบจำลองจัดการเรื่องนี้ต่างกัน ข้อมูลเป็น ข้อมูลขาดหายแบบสุ่มสมบูรณ์ (missing completely at random, MCAR) เมื่อการขาดหายไม่เกี่ยวกับค่าใดที่วัดหรือไม่ได้วัด และเป็นข้อมูลขาดหายแบบสุ่ม (MAR) เมื่อขึ้นกับข้อมูลที่สังเกตแล้วเท่านั้น เช่นคะแนนก่อนหน้า [5] บทความแยกเรื่องกลไกของข้อมูลที่หายไป (บทความภาษาอังกฤษ) อธิบายนิยามเหล่านี้อย่างลึกซึ้ง

mixed model ที่อิงความควรจะเป็น คือ LMM และ GLMM ยังใช้ได้ภายใต้ MAR เมื่อระบุแบบจำลองถูกต้อง RM-ANOVA ตัดผู้ป่วยที่ขาดการวัดครั้งใดก็ตามออกทั้งคน ซึ่งสิ้นเปลืองข้อมูล และป้องกันความลำเอียงได้เฉพาะภายใต้ MCAR

GEE ที่ไม่ถ่วงน้ำหนักและใช้ sandwich standard error ตามปกติต้องการว่าการขาดหายไม่ขึ้นกับผลลัพธ์ คือ MCAR หรือการขาดหายที่ขึ้นกับตัวแปรร่วมที่อยู่ในแบบจำลองเท่านั้น (บางครั้งเรียกว่า covariate-dependent MCAR) เมื่อการออกจากการศึกษาตามหลังการตอบสนองก่อนหน้า สามารถถ่วงน้ำหนัก GEE ด้วยส่วนกลับของความน่าจะเป็นที่จะอยู่ในการศึกษาต่อ (IPW-GEE) ซึ่งต้องมีแบบจำลองการออกจากการศึกษาที่ถูกต้อง

การเติมค่าพหุคูณ (multiple imputation) ภายใต้ MAR ก่อนประมาณ GEE เป็นอีกทางหนึ่ง โดยเติมค่าการวัดที่ขาดหายแต่ละครั้งหลายรอบด้วยค่าที่เป็นไปได้ ประมาณ GEE กับชุดข้อมูลที่เติมครบแล้วแต่ละชุด แล้วรวมผลเข้าด้วยกัน วิธีนี้ต้องมีแบบจำลองการเติมค่าที่ถูกต้อง [5]

เมื่อมีผู้ป่วยน้อยหรือมีศูนย์น้อย

ค่าคลาดเคลื่อนมาตรฐานแบบ sandwich เป็นผลสำหรับตัวอย่างขนาดใหญ่ เมื่อมีผู้ป่วยน้อย หรือมีศูนย์น้อยในกรณีที่ผู้ป่วยถูกจัดกลุ่มตามศูนย์ และศูนย์เป็นหน่วยที่ค่าที่วัดภายในสัมพันธ์กัน ค่านี้มักเล็กเกินไป การแก้ค่า sandwich สำหรับตัวอย่างขนาดเล็ก ซึ่งขยายค่าให้เข้าใกล้ขนาดที่ถูกต้อง จึงอาจเหมาะกว่า

ให้ตัดสินนัยสำคัญเทียบกับการแจกแจง t หรือ F แทนการแจกแจงปกติ วิธีนี้เผื่อความไม่แน่นอนที่เพิ่มขึ้นเมื่อมีกลุ่ม (cluster) น้อย คือผู้ป่วยหรือศูนย์ซึ่งค่าที่วัดภายในแต่ละหน่วยสัมพันธ์กัน

คำถามของการทดลองต้องการ odds ratio ค่าไหน

การทดลองที่ให้ข้อมูลแก่นโยบายหรือแนวทางเวชปฏิบัติมักถามคำถามระดับประชากร คือ odds ของการตอบสนองเปรียบเทียบกันอย่างไรระหว่างประชากรที่ได้รับการรักษากับประชากรที่ไม่ได้รับ GEE ที่ใช้ logit link ตอบคำถามนี้ได้โดยตรง [6] เมื่อคำถามคือการเปลี่ยนแปลงของสัดส่วนผู้ที่ตอบสนอง ผลต่างความเสี่ยงหรืออัตราส่วนความเสี่ยงแบบ marginal อาจตอบได้ตรงกว่า odds ratio ใดๆ โดยประมาณด้วย GEE ที่ใช้ identity link หรือ log link หรือด้วยการทำมาตรฐาน (standardisation) คือการเฉลี่ยความน่าจะเป็นของการตอบสนองที่ทำนายของผู้ป่วยทุกคนภายใต้การรักษาและภายใต้การควบคุม

คำถามระดับบุคคลถามว่า odds ของการตอบสนองเปรียบเทียบกันอย่างไรระหว่างผู้ป่วยที่ได้รับการรักษากับผู้ป่วยที่ไม่ได้รับ โดยทั้งสองมีโอกาสตอบสนองพื้นฐานเท่ากัน คำถามนี้อาจใกล้เคียงกับที่แพทย์ถามเกี่ยวกับผู้ป่วยที่อยู่ตรงหน้า แม้จะไม่เคยทราบ random intercept ของผู้ป่วยรายนั้น GLMM ตอบคำถามนี้ และเป้าหมายของมันอยู่ห่างจาก 1 มากขึ้น เมื่อใดก็ตามที่ผู้ป่วยมีโอกาสตอบสนองพื้นฐานต่างกัน

ให้ระบุ estimand ในโปรโตคอลและแผนการวิเคราะห์ทางสถิติก่อนเปิดเผยการจัดกลุ่ม และรายงานแบบจำลองที่ตรงกับมัน การรายงาน odds ratio ทั้งสองค่าก็สมเหตุสมผล เมื่อแต่ละค่ามีป้ายบอกคำถามของตน การรายงานค่าที่ห่างจาก 1 มากกว่าไม่สมเหตุสมผล

ข้อผิดพลาดที่พบบ่อย

  • "GEE กับ GLMM ให้คำตอบเดียวกัน"

    จริงสำหรับคะแนน แต่ไม่จริงสำหรับผู้ที่ตอบสนอง

    วิธีแก้: สำหรับผลลัพธ์ต่อเนื่องที่ใช้ identity link สัมประสิทธิ์ของการรักษาตรงกัน สำหรับผลลัพธ์ทวิภาคที่ใช้ logit link ทั้งสองประมาณ odds ratio ต่างกัน คือระดับประชากรและระดับบุคคล และทั้งสองอาจถูกต้อง

  • "สูตรการลดทอนแปลง odds ratio ค่าหนึ่งเป็นอีกค่าหนึ่งได้อย่างแม่นยำ"

    สูตรอาศัยการประมาณเส้นโค้งลอจิสติกด้วยเส้นโค้งปกติ จึงให้ค่าใกล้เคียงแต่ไม่แม่นตรง ในตัวอย่างคำนวณด้วยมือ สูตรให้ 1.81 ขณะที่ค่าที่แน่นอนราว 1.75

    วิธีแก้: ใช้สูตรเพื่อประเมินขนาดของช่องว่าง และประมาณแบบจำลองที่ตรงกับ estimand ที่ต้องการ

  • "robust standard error ช่วยแก้แบบจำลองที่ระบุผิด"

    sandwich ปกป้องค่าคลาดเคลื่อนมาตรฐานจาก working correlation ที่ผิด แล้วคนมักขยายความข้อนี้ไปเป็นการปกป้องจากแบบจำลองที่ผิด

    วิธีแก้: robust standard error ช่วยแก้ค่าคลาดเคลื่อนมาตรฐานเมื่อข้อสมมติเรื่องความแปรปรวนผิด แต่ไม่ได้แก้แบบจำลองค่าเฉลี่ยที่ผิด และสัมประสิทธิ์ที่ลำเอียงก็ยังคงลำเอียงอยู่

  • "GEE ที่ไม่ถ่วงน้ำหนักปลอดภัยเมื่อผู้ป่วยออกจากการศึกษา"

    หากผู้ที่ไม่ตอบสนองออกจากการศึกษามากกว่า ผู้ป่วยที่ยังเหลืออยู่ในภายหลังจะมีโอกาสเป็นผู้ที่ตอบสนองมากกว่า และ GEE ที่ไม่ถ่วงน้ำหนักรับความลำเอียงนั้นไปด้วย

    วิธีแก้: คงไว้ซึ่ง estimand สำหรับคำถามระดับประชากร ให้ถ่วงน้ำหนัก GEE ด้วยส่วนกลับของความน่าจะเป็นที่จะอยู่ในการศึกษาต่อ หรือใช้การเติมค่าพหุคูณ (multiple imputation) ภายใต้ MAR แล้วจึงประมาณ GEE สำหรับคำถามระดับบุคคล mixed model ที่อิงความควรจะเป็นใช้ได้ภายใต้ MAR

  • "RM-ANOVA หรือการถดถอยลอจิสติกธรรมดาใช้ได้กับผลลัพธ์ทวิภาคหรือเรียงอันดับที่วัดซ้ำ"

    RM-ANOVA คาดหวังผลลัพธ์ต่อเนื่อง ส่วนการถดถอยลอจิสติกธรรมดาไม่คำนึงถึงความสัมพันธ์ และรองรับเพียงสองหมวด

    วิธีแก้: เลือกตามชนิดของผลลัพธ์ คือ GEE หรือ GLMM สำหรับผลลัพธ์ทวิภาค และ ordinal GEE หรือ ordinal mixed model สำหรับหมวดหมู่ที่เรียงลำดับ

สิ่งที่ควรทำในการวิเคราะห์ของคุณเอง

อภิธานศัพท์

repeated measures (การวัดซ้ำ)
การวัดผลลัพธ์เดียวกันในผู้ป่วยคนเดียวกันมากกว่าหนึ่งครั้ง
RM-ANOVA (การวิเคราะห์ความแปรปรวนแบบวัดซ้ำ)
การวิเคราะห์ความแปรปรวนแบบวัดซ้ำ ใช้ F test ที่ต้องการข้อมูลครบและสมดุล และต้องมี sphericity
sphericity (ความเท่ากันของความแปรปรวนของผลต่างระหว่างครั้งที่วัด)
ความแปรปรวนของผลต่างระหว่างสองครั้งที่วัดใดๆ เท่ากันทุกคู่
GEE (สมการประมาณค่าวางนัยทั่วไป)
Generalized estimating equations วิธีระดับประชากรสำหรับข้อมูลที่สัมพันธ์กัน
working correlation (ความสัมพันธ์สมมติ)
รูปแบบความสัมพันธ์ที่ GEE สมมติระหว่างค่าที่วัดของผู้ป่วยคนเดียวกัน
GLMM (แบบจำลองผสมเชิงเส้นวางนัยทั่วไป)
Generalized linear mixed model คือ generalized linear model ที่มี random effect
population-averaged effect (ผลระดับประชากร)
ผลที่เปรียบเทียบผลลัพธ์เฉลี่ยของประชากรที่ได้รับการรักษาทั้งหมดกับประชากรกลุ่มควบคุมทั้งหมด สำหรับ odds ratio คือ marginal odds ratio
subject-specific effect (ผลระดับบุคคล)
ผลที่เปรียบเทียบผู้ป่วยที่ได้รับการรักษากับผู้ป่วยที่ไม่ได้รับ โดยมี random effect เท่ากัน คือมีโอกาสตอบสนองพื้นฐานเท่ากัน สำหรับ odds ratio คือ conditional odds ratio
random intercept (ค่าตัดแกนสุ่ม)
การเลื่อนระดับพื้นฐานระดับผู้ป่วย ซึ่งใช้ร่วมกันในทุกค่าที่วัดของผู้ป่วยคนนั้น
MCAR (ข้อมูลขาดหายแบบสุ่มสมบูรณ์)
Missing completely at random การขาดหายที่ไม่เกี่ยวกับค่าใดที่วัดหรือไม่ได้วัด
MAR (ข้อมูลขาดหายแบบสุ่ม)
Missing at random การขาดหายที่ขึ้นกับข้อมูลที่สังเกตแล้วเท่านั้น
sandwich standard error (ค่าคลาดเคลื่อนมาตรฐานแบบแซนด์วิช)
ค่าคลาดเคลื่อนมาตรฐานที่ยังใช้ได้เมื่อแบบจำลองความสัมพันธ์ผิด แต่ใช้ไม่ได้เมื่อแบบจำลองค่าเฉลี่ยผิด

เอกสารอ้างอิง

  1. Gueorguieva R, Krystal JH. Move over ANOVA: progress in analyzing repeated-measures data and its reflection in papers published in the Archives of General Psychiatry. Arch Gen Psychiatry. 2004;61(3):310-317. https://doi.org/10.1001/archpsyc.61.3.310
  2. Laird NM, Ware JH. Random-effects models for longitudinal data. Biometrics. 1982;38(4):963-974. https://doi.org/10.2307/2529876
  3. Zeger SL, Liang KY, Albert PS. Models for longitudinal data: a generalized estimating equation approach. Biometrics. 1988;44(4):1049-1060. https://doi.org/10.2307/2531734
  4. Liang KY, Zeger SL. Longitudinal data analysis using generalized linear models. Biometrika. 1986;73:13-22. https://doi.org/10.1093/biomet/73.1.13
  5. Fitzmaurice GM, Laird NM, Ware JH. Applied longitudinal analysis. 2nd ed. Wiley; 2011. https://doi.org/10.1002/9781119513469
  6. Hubbard AE, Ahern J, Fleischer NL, Van der Laan M, Lippman SA, Jewell N, et al. To GEE or not to GEE: comparing population average and mixed models for estimating the associations between neighborhood risk factors and health. Epidemiology. 2010;21(4):467-474. https://doi.org/10.1097/EDE.0b013e3181caeb90

ประเด็นสำคัญ

  • เลือกแบบจำลองตามชนิดของผลลัพธ์ และตามว่าคำถามเปรียบเทียบระหว่างประชากร หรือเปรียบเทียบผู้ป่วยที่ได้รับการรักษากับผู้ป่วยที่ไม่ได้รับซึ่งมี random effect เท่ากัน คือมีโอกาสตอบสนองพื้นฐานเท่ากัน
  • สำหรับผลลัพธ์ต่อเนื่องที่ใช้ identity link LMM กับ GEE มุ่งไปที่สัมประสิทธิ์ของการรักษาตัวเดียวกัน
  • สำหรับผลลัพธ์ทวิภาคที่ใช้ logit link GEE กับ GLMM มุ่งไปที่ odds ratio ต่างกัน ในที่นี้คือ 1.48 และ 1.85
  • สูตรการลดทอนประมาณช่องว่างได้ แต่ไม่ใช่การแปลงค่าที่แน่นอน
  • mixed model ที่อิงความควรจะเป็นใช้ได้ภายใต้ MAR ส่วน GEE ที่ไม่ถ่วงน้ำหนักต้องการ MCAR หรือการขาดหายที่ขึ้นกับตัวแปรร่วมในแบบจำลองเท่านั้น

อ่านต่อในวิกิ: [[mixed-model-series-guide-th]] [[mixed-model-conditional-vs-marginal-covariance-th]]

0
ถึงนักอ่านชาวไทยและต่างชาติทำความเข้าใจบริบททางการแพทย์ของผมอ่านต่อ →ถึงนักอ่านชาวไทยและต่างชาติทำความเข้าใจเนื้อหาของผมที่นอกเหนือจากการแพทย์อ่านต่อ →

ความคิดเห็น

ยังไม่มีความคิดเห็น มาเป็นคนแรกกันเลย

เข้าสู่ระบบเพื่อแสดงความคิดเห็น