Skip to content

Cohorts are sets of samples, as on cBioPortal - #85

Merged
alisman merged 5 commits into
masterfrom
session/samples
Oct 28, 2026
Merged

alisman merged 5 commits into
masterfrom
session/samples

Conversation

@alisman

@alisman alisman commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

What was wrong

A cohort compiled to a set of patients, and every view joined on <patient> IN (SELECT pid FROM cohort). A sample-level clause therefore pulled in every other sample of the patients it matched. The gallery's NSCLC cohort (msk_chord_2024) held 7,876 samples, 67 of them colorectal, breast, pancreatic and so on, and its gene frequencies counted mutations found only in those samples:

NSCLC, mutated before after cbioportal.org
TP53 4,020 4,005 4,005
APC 327 312 312
PIK3CA 457 443 443
KRAS 2,194 2,183 2,183

check:portal never saw it: it compared filtered cohorts only by patient count, which is right by construction, and compared gene tables only under a patient-level filter (SEX = Female) and only through Explore's code path.

What this changes (cBioPortal's semantics)

  • A cohort is a set of samples. Every clause compiles to (pid, sid) rows of the study's samples.
    • Sample-level clauses select the matching samples: sample attributes, OQL alterations, sample lists, profiled, genomicData, proteinPosition, caseList.
    • Patient-level clauses select every sample of the matching patients: patient attributes, event, derived, patient lists, the held-out split.
    • all/any/not are intersection, union and complement over samples.
  • A clinical attribute is matched at the level each study records it. For example, GRADE is sample-level in pancan_pcawg_2020 and patient-level in lihc_tcga, and one clause now matches each study its own way. per is removed from the DSL and refused by name. No backward compatibility, as agreed.
  • One join rule. A cohort's patients are the owners of its samples. inCohortSamples and inCohortPatients (lib/cohort2.ts) replace all 127 hand-written joins.
    • Gene counts, pair counts, co-expression and differential expression count samples, as the portal does.
    • Patient-unit views (survival, Cox, timelines, …) take a patient's genomic state from its in-cohort samples only.
  • Locks freeze the cohort's samples, with each arm's samples as parts. Lock drift is measured in samples.
  • Splits are decided per sample (first match wins), and their conservation is checked in samples.
  • Explore treats the draft as a sample set. The oncoprint builds patient columns from the cohort's samples, resolved once and shipped as an external table.
  • The patient-data boundary is unchanged in kind. Cohort SQL stays a subquery, and aggregate-only or remote peers never send or receive ids. The boundary and sql-kinds suites pass.

Verification

  • check:portal: 724/724 agree with cbioportal.org. Two new sections:
    • Sample-level selections: NSCLC; SAMPLE_TYPE = Primary; Metastasis + SEX = Female; GRADE = IV in difg_glass_2019, where 56 patients have samples of different grades; OQL TP53: MUT. Each is compared on sizes, every row of Explore's gene tables, and the views' gene counts.
    • Mixed levels: pancan_pcawg_2020 + lihc_tcga under GRADE = G3, giving 75 + 125 samples.
    • These new sections failed 75 of 83 comparisons on the old code.
  • check:cohorts: 41/41. The reference evaluator now works over sample ids.
  • check:mutants: 21/21. Two new planted bugs bring the leak back, and both are caught:
    • a sample-level clause that takes whole patients (by check:cohorts);
    • gene counts joined on patients (by check:portal).
  • Unit tests, the Postgres suite, the examples and their method scripts, and lint all pass.
  • View snapshots are re-recorded and pass, including the reverse-order run.
  • Example counts are re-pinned. Mutant cohorts shrink to the portal's numbers; wild-type cohorts grow, because a patient with any non-mutant sample now has samples in them.
  • scripts/gate.sh: everything up to e2e passed: lint, build, unit, Postgres, access, Python conformance, the splitBy cost check, and the warehouse checks. The e2e stage was still running when this PR was opened. Please don't merge until it, or CI, is green.

Performance (docs/PERFORMANCE.md)

Base commit and this branch, run back to back and interleaved, uncached:

base this branch change
all 92 gallery examples, end to end 191.0 s 204.9 s +7.3% (median example 1.07×)
oncoprint bench (three cases) 6–17% faster
  • Not the neutral result expected. Patient-level-clause cohorts now expand through sample_derived, and patient-unit views join on in-cohort samples. Two suspected causes were measured and ruled out; details are in the doc.
  • The first version of the oncoprint was 3× slower. That's fixed by resolving the column samples once.

Left for follow-up

  • Overlap exclusion (step 2, agreed): cBioPortal's group-comparison rule, which excludes overlapping samples from sample views and overlapping patients from patient views. This matters now, because a patient whose primary is TP53-mutant and whose metastasis isn't has samples in both "TP53 mut" and "TP53 wt".
  • Generic-assay facets in Explore still count patients (the owners of in-cohort rows); the portal counts samples. Fixing it needs a unit label in the UI.
  • drivernegative keeps patients as its unit, built from in-cohort samples. cBioPortal's group comparison would count samples.
  • The patient page's tables still list all of a patient's samples, as cBioPortal's patient view does. The ?sampleId= mode (session/patient) will need an optional sample filter on alterationSource, patientSamplesSource and patientEventsSql.
  • bench.mjs imported ./lib/oncoprint.js and could not run. Fixed here.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Q3ezTZdin1WVnUH8kzqcMC

A cohort compiled to a set of patients, and every view joined its tables on
`<patient> IN (SELECT pid FROM cohort)`. A sample-level clause therefore took
every other sample of the patients it matched: the NSCLC cohort of
msk_chord_2024 held 7,876 samples (67 of them colorectal, breast, pancreatic,
…), and its gene frequencies counted mutations from those samples (TP53
4,020 vs cBioPortal's 4,005, APC 327 vs 312).

Semantics (cBioPortal's):
- every clause compiles to rows (pid, sid) of the study's samples. Sample-level
  clauses (sample attributes, OQL alterations, sample lists, profiled,
  genomicData, proteinPosition, caseList) select the matching samples;
  patient-level ones (patient attributes, events, derived, patient lists, the
  held-out split) every sample of the matching patients. all/any/not are
  intersection, union and complement over samples.
- a clinical attribute is matched at the level each study records it, so one
  clause works over a study that records GRADE per sample and one that records
  it per patient. `per` is removed from the DSL and refused by name.
- a cohort's patients are the owners of its samples. Two helpers in
  lib/cohort2.ts are the one join rule: inCohortSamples for sample-keyed
  tables, inCohortPatients for patient-keyed ones; all 127 hand-written joins
  go through them. Gene counts, pair counts, co-expression and differential
  expression count samples, as the portal does; patient-unit views derive a
  patient's genomic state from its in-cohort samples only.
- locks freeze the cohort's samples (a sample-level id set, arms as parts);
  drift is measured in samples. Splits are decided per sample and conserved
  in samples.
- Explore's draft is a sample set: totals, gene tables, facets and gene-state
  counts follow it. The oncoprint's patient columns are built from the
  cohort's samples, shipped once as an external table.

Verified:
- check:portal 724/724 against cbioportal.org, including new sections:
  sample-level selections (NSCLC, SAMPLE_TYPE = Primary, Metastasis + SEX,
  GRADE = IV in difg_glass_2019, OQL TP53: MUT) compared by sizes, Explore's
  gene tables and the views' gene counts, and pancan_pcawg_2020 + lihc_tcga
  under GRADE = G3, sample-level in one and patient-level in the other
  (75 + 125 samples). These failed 75 of 83 before the change.
- check:cohorts 41/41: the reference evaluator is now over sample ids.
- check:mutants 21/21, two new planted bugs bring the leak back (a
  sample-level clause taking whole patients; gene counts joined on patients).
- unit, postgres, view snapshots (re-recorded and reviewed), examples and
  method scripts, lint. Example counts re-pinned: mutant cohorts shrink to the
  portal's numbers (KRAS-mutant NSCLC 2,194 -> 2,183), wild-type ones grow.
- performance recorded in docs/PERFORMANCE.md: the gallery is ~7% slower
  end to end, the oncoprint bench neutral-to-better.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3ezTZdin1WVnUH8kzqcMC
@vercel

vercel Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
cbiolab-genie Ready Ready Preview Oct 28, 2026 1:43am UTC
cohort-lab Ready Ready Preview Oct 28, 2026 1:43am UTC

Request Review

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

The compiler refuses a clinical clause carrying per (the level is each
study's own); the browser suites and scripts/figure-stress.json still wrote
it, so their documents answered 400 and ten e2e checks failed or timed out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3ezTZdin1WVnUH8kzqcMC
…ider step

A lock freezes samples, so a sample new since the lock is one the
definition's drift already reports as entered; newSamples counted the frozen
patients' other samples instead, and reported a just-locked sample cohort as
changed. The lock e2e check reads n and nPatients; the risk-groups slider check
asserts a step of at least one whole percentile rather than a value pinned to
the old scores.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3ezTZdin1WVnUH8kzqcMC
@alisman
alisman merged commit 448ae87 into master Oct 28, 2026
10 checks passed
@alisman
alisman deleted the session/samples branch October 28, 2026 10:17

This branch was successfully deployed

1 active deployment
Preview – cohort-lab — 20f04ac0 Deployed Oct 28, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants