From 024e3165dcc7dc8c14841fcaf7431e79a6a4bf74 Mon Sep 17 00:00:00 2001 From: fosterwei Date: Tue, 29 Sep 2026 21:36:49 -0700 Subject: [PATCH] Do not read an allocation ratio as a sample size The screening-count fix shipped with the retatrutide page reordered the sample-size patterns, and that reordering broke five rows while fixing four. Moving the noun pattern below the bare verb pattern let "randomized 1:1" win: kisspeptin went from 62 to 1, survodutide from 387 to 1, thymosin-alpha-1 from 194 to 1, semax from 43 to 2 and tesamorelin from 152 to 3. None of those reached a record, because the drafter never regenerates a record that has guide sections, but the next re-draft would have written them. The noun pattern goes back above the verb pattern, keeping the ordering that was right before, with assignment phrasing still ahead of both so CONSORT abstracts read correctly. A number immediately followed by a colon is refused outright. Both context checks now look at what follows the captured number rather than the whole match. Anchoring them to the match end rejected the very case the change existed to fix, because ASSIGNED ends after "randomly assigned" and the next character in "assigned: 134 to retatrutide 4 mg" is a colon. Two more screening counts corrected on retatrutide: the body-composition substudy stored 534 where 534 were screened and 281 enrolled, and the cardiovascular trial stored 367 where 367 were screened and 146 randomised. A full re-check of all 223 stored sample sizes says this class is now closed. Of 64 rows where the reader disagrees with what is stored, 55 disagree with the pre-existing reader too, which means they were corrected by hand and must not be overwritten; the audit cannot tell a drafter bug from a human correction, so it is not a list of fixes to apply. The 9 rows this change moved were each checked against their abstract: 4 were genuine screening counts, all on retatrutide, and 5 were the regressions above. 36 drafter tests, up from 29. Co-Authored-By: Claude Opus 5 (1M context) --- data/compounds/retatrutide.json | 12 ++++++++---- scripts/draft_claims.py | 14 ++++++++++++-- scripts/test_drafter.py | 23 +++++++++++++++++++++++ 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/data/compounds/retatrutide.json b/data/compounds/retatrutide.json index 1a2221a..4459a60 100644 --- a/data/compounds/retatrutide.json +++ b/data/compounds/retatrutide.json @@ -489,7 +489,7 @@ } }, { - "value": "Randomized controlled trial, humans, n=534, 2025: Percent reduction from baseline in total fat mass was 4·9% (SE 1·4%) with retatrutide 0·5 mg, 15·2% (3·2%) with retatrutide 4 mg (pooled), 26·1% (2·5%) with retatrutide 8 mg (pooled), 23·2% (3·0%) with retatrutide 12 mg, 2·6% (1·6%) with dulaglutide, and 4·5% (1·2%) with placebo.", + "value": "Randomized controlled trial, humans, n=281, 2025: Percent reduction from baseline in total fat mass was 4·9% (SE 1·4%) with retatrutide 0·5 mg, 15·2% (3·2%) with retatrutide 4 mg (pooled), 26·1% (2·5%) with retatrutide 8 mg (pooled), 23·2% (3·0%) with retatrutide 12 mg, 2·6% (1·6%) with dulaglutide, and 4·5% (1·2%) with placebo.", "evidence_label": "human-clinical-trial", "source_ids": [ "pmid-40609566" @@ -500,7 +500,7 @@ "study": "Coskun 2025", "year": "2025", "design": "Randomized controlled trial", - "n": 534, + "n": 281, "species": "Humans", "dose": "0·5 mg; 4 mg", "route": "subcutaneous", @@ -649,7 +649,7 @@ } }, { - "value": "Human study, humans, n=367, 2026: Background and hypothesis Retatrutide is an agonist of the glucose-dependent insulinotropic polypeptide, glucagon-like peptide-1 and glucagon receptors that reduced weight and hemoglobin A1c (HbA1c) in individuals with obesity and type 2 diabetes (T2D).", + "value": "Human study, humans, n=146, 2026: Background and hypothesis Retatrutide is an agonist of the glucose-dependent insulinotropic polypeptide, glucagon-like peptide-1 and glucagon receptors that reduced weight and hemoglobin A1c (HbA1c) in individuals with obesity and type 2 diabetes (T2D).", "evidence_label": "observational-human", "source_ids": [ "pmid-41160422" @@ -660,7 +660,7 @@ "study": "Heerspink 2026", "year": "2026", "design": "Human study", - "n": 367, + "n": 146, "species": "Humans", "dose": "12 mg", "route": "not stated in abstract", @@ -1204,6 +1204,10 @@ { "date": "2026-09-29", "change": "Reviewed by Adam Mirando, PharmD, on 2026-09-29." + }, + { + "date": "2026-09-29", + "change": "Corrected two more sample sizes that had stored the screening count: the body-composition substudy from 534 to 281 enrolled, and the cardiovascular trial from 367 screened to 146 randomised." } ], "guide": [ diff --git a/scripts/draft_claims.py b/scripts/draft_claims.py index 3a158bd..4b488eb 100644 --- a/scripts/draft_claims.py +++ b/scripts/draft_claims.py @@ -90,10 +90,14 @@ r"\b" + NUM + r"\s+(?:\([^)]{0,40}\)\s*)?(?:\w+\s+){0,3}?" r"(?:were|was)\s+(?:randomly\s+assigned|randomi[sz]ed|allocated|enrolled)\b", re.IGNORECASE) SCREENED_AFTER = re.compile(r"^[^.]{0,40}?\b(?:were|was)\s+screened\b", re.IGNORECASE) +# "randomized 1:1", "randomised 1:1:1:1:1" is an allocation ratio, not a count. +# The bare verb pattern reads it as a sample of 1, which is why it sits below +# the noun pattern and refuses a number followed by a colon. +RATIO_AFTER = re.compile(r"^\s*:") N_RE = [ ASSIGNED, - re.compile(r"\b(?:enrolled|randomi[sz]ed|included|analy[sz]ed|recruited)\s+" + NUM + r"\b", re.IGNORECASE), re.compile(r"\b" + NUM + r"\s+(?:patients|participants|subjects|volunteers|adults|individuals|people|men|women|children|infants|rats|mice|rabbits|dogs|pigs|animals)\b", re.IGNORECASE), + re.compile(r"\b(?:enrolled|randomi[sz]ed|included|analy[sz]ed|recruited)\s+" + NUM + r"\b", re.IGNORECASE), re.compile(r"\b[nN]\s?=\s?" + NUM + r"\b"), ] DESIGN_FROM_TITLE = [ @@ -227,9 +231,15 @@ def sample_size(abstract: str) -> int | None: n = int(re.sub(r"[ ,\u00a0\u202f]", "", m.group(1))) if not 1 <= n <= 2_000_000: continue + # Both look at what follows the number itself, not the whole match: + # ASSIGNED ends after "randomly assigned", and the colon in + # "assigned: 134 to retatrutide 4 mg" is not an allocation ratio. + after = abstract[m.end(1):] # "930 participants were screened and 537 were randomly assigned": # the screening count is not the trial's size. - if rx is not ASSIGNED and SCREENED_AFTER.match(abstract[m.end():]): + if rx is not ASSIGNED and SCREENED_AFTER.match(after): + continue + if RATIO_AFTER.match(after): continue return n return None diff --git a/scripts/test_drafter.py b/scripts/test_drafter.py index a8bedda..b7e446f 100644 --- a/scripts/test_drafter.py +++ b/scripts/test_drafter.py @@ -54,6 +54,29 @@ def test_screened_then_randomised_short_form(self): def test_plain_enrolment_still_wins(self): self.assertEqual(dc.sample_size("We enrolled 338 adults, 51.8% of whom were men."), 338) + def test_allocation_ratio_is_not_a_count(self): + # "randomized 1:1" put n=1 on kisspeptin, survodutide, semax, tesamorelin + # and thymosin-alpha-1 rows when the noun pattern lost precedence. + self.assertEqual(dc.sample_size( + "Of these, 194 patients have been randomized 1:1 to either Group A or Group B."), 194) + self.assertEqual(dc.sample_size( + "Totally 387 people (aged 18-75 years) were randomized 1:1:1:1:1 to once-weekly " + "subcutaneous survodutide or placebo."), 387) + + def test_colon_after_assignment_is_not_a_ratio(self): + # The colon in "assigned: 134 to retatrutide 4 mg" introduces the arms, + # so it must not disqualify the number that precedes the verb. + self.assertEqual(dc.sample_size( + "930 participants were screened and 537 (296 [55%] female) were randomly assigned: " + "134 to retatrutide 4 mg, 133 to retatrutide 9 mg"), 537) + + def test_screened_participants_phrasing(self): + self.assertEqual(dc.sample_size( + "Of 367 participants screened, 146 were randomized to study interventions."), 146) + self.assertEqual(dc.sample_size( + "534 participants were screened for inclusion into the main study; 253 were excluded " + "and 281 participants were enrolled and randomly assigned to the main study."), 281) + def test_enrolment_beats_subgroup_n(self): text = "The renal impairment study included 33 participants (normal function, n = 14; mild impairment, n = 7)." self.assertEqual(dc.sample_size(text), 33)