Measurable Abstraction¶
The Measurable layer is the bridge between the generic query engine and the underlying ORM or materialised-view schema.
It tells the engine:
- which ORM class to query for a given
RuleTarget - which columns identify the person, episode, and event date
- which optional columns support concept, numeric, string, or predicate-style filtering
The query engine never hard-codes source-table column names directly. It relies on the measurable contract instead.
Canonical Output Contract¶
Every measurable must be able to emit rows in the canonical measure-member shape:
| Column | Meaning |
|---|---|
person_id |
individual identifier |
episode_id |
clinical episode |
measure_resolver |
alignment key used for higher-level joins |
measure_date |
date on which the measurable event qualifies |
MeasurableBase.table_selectables() and filter_table_dated() produce this shape for subqueries and measures.
MeasurableSpec¶
MeasurableSpec is the declarative mapping attached to each measurable class.
Required attributes:
domainlabelperson_id_attrepisode_id_attrevent_date_attr
Optional value attributes:
value_concept_attrvalue_numeric_attrvalue_string_attrvalue_predicate_attr
Optional control fields:
temporality_mapvalid_targets
The spec stores attribute names, not SQLAlchemy columns. Those names are resolved against the concrete class at bind time.
Binding¶
When a class subclasses MeasurableBase and defines:
__measurable__ = MeasurableSpec(...)
the class is automatically bound to a BoundMeasurableSpec via __init_subclass__.
Binding rules:
- required columns are resolved immediately
- unsupported optional value channels remain
None - no fake SQL constants are injected for missing value channels
That last point is intentional. A measurable that does not support concept filtering should expose value_concept_col = None, and the query layer should decide whether that is acceptable for the rules being executed.
Which Rule Types Need Which Columns¶
The current query engine resolves measurable fields like this:
| Rule style | Required measurable column |
|---|---|
| exact / hierarchy / hierarchy exclusion / presence / absence | value_concept_attr |
| substring | value_string_attr |
| predicate | value_predicate_attr |
| scalar threshold | value_numeric_attr |
scalar threshold with concept_id != 0 |
value_numeric_attr and value_concept_attr |
Scalar-only measurables¶
Derived window measurables can legitimately expose only a numeric column.
Examples in the current codebase:
ReferralToSpecialistWindowMeasurable- window-style derived thresholds generally driven by
ScalarRule
These measurables work when the scalar rule is threshold-only, meaning concept_id = 0.
They do not support concept-constrained scalar filtering. If a scalar rule with concept_id != 0 targets such a measurable, subquery compilation raises a targeted error explaining that value_concept_attr is required for concept filtering.
Episode Requirement¶
This project assumes an episode-linked oncology data model.
Why that matters:
- a person can have multiple disease episodes
- diagnosis, treatment, and modifier rows must align to the correct episode
- composite
ANDlogic joins onmeasure_resolver, which is usually the episode id
Without an episode-level resolver:
ANDlogic would collapse to person-level intersection- cross-episode contamination would become possible
- time-based qualification would be harder to interpret clinically
Current Measurable Families¶
The shipped registry currently maps targets into a small set of measurable families:
- diagnosis measurables: any-diagnosis, primary-diagnosis, staging, metastasis
- event measurables: measurements, procedures, observations
- treatment measurables: surgery, chemotherapy, radiotherapy, intent, derived windows
- person measurables: death-linked person demography rows
Current Target Registry¶
The authoritative supported set is the measurable registry, not the RuleTarget enum by itself.
Some enum members are reserved for future design work and do not currently resolve to a shipped measurable.
Current registry:
| Target | Measurable | Intended use |
|---|---|---|
dx_any |
AnyConditionMeasurable |
any diagnosis row attached to an episode |
dx_primary |
PrimaryDiagnosisEpisodeMeasurable |
diagnosis rows anchored to the primary diagnosis episode |
dx_stage |
StagedConditionMeasurable |
staging modifiers linked to diagnosis episodes |
dx_mets |
MetsConditionMeasurable |
metastatic disease modifiers linked to diagnosis episodes |
tx_surgical |
SurgicalMeasurable |
cancer-related surgical procedures |
tx_current_episode |
AllCurrentTreatmentMeasurable |
treatment-start summary for the current diagnosis episode |
tx_chemotherapy |
ChemoTreatmentMeasurable |
chemotherapy treatment episodes |
tx_radiotherapy |
RTTreatmentMeasurable |
radiotherapy treatment episodes |
intent_sact |
IntentChemoMeasurable |
systemic therapy intent flags |
intent_rt |
IntentRTMeasurable |
radiotherapy intent flags |
tx_to_death_window |
TxDaysBeforeDeath |
derived days from treatment to death |
dx_to_tx_window |
TxDaysToStartTreatment |
derived days from diagnosis to treatment start |
tx_concurrent |
TxConcurrentChemoRT |
concurrent chemo-radiotherapy predicate |
referral_to_specialist_window |
ReferralToSpecialistWindowMeasurable |
derived referral-to-specialist interval |
meas_concept |
MeasurementMeasurable |
diagnosis-linked measurements |
proc_concept |
ProcedureMeasurable |
diagnosis-linked procedures |
obs_concept |
ObserveMeasurable |
diagnosis-linked observations |
ev_visit |
VisitSpecialtyMeasurable |
diagnosis-linked specialty visits |
demog_death |
DeathMeasurable |
death-linked person rows |
Diagnosis Targets¶
For clinical users, the most important distinction is between the two diagnosis concept targets:
dx_anyasks whether a diagnosis row is present on the episode being evaluateddx_primaryasks whether the diagnosis belongs to the patient's primary diagnosis episode anchor
dx_primary is intended for cohort definitions that should be stable at the start of the primary episode of care, rather than changing because later progression or metastatic episodes carry related diagnosis coding.
That design is deliberate. It lets the same reporting engine support both:
- broad episode-level diagnosis logic
- primary-diagnosis anchored cohort logic
without forcing report authors to invent bespoke SQL or special-case post-processing.
String-backed Filters¶
SubstringRule works against the measurable's value_string_attr, but those string fields are not all the same kind of data.
Current examples include:
- diagnosis code strings such as
condition_code - derived labels such as
stage_label - event labels such as
event_label - treatment names such as
surgery_name,regimen_concept, andcourse_concept - provider specialty labels such as
provider_specialty
That is why the measurable contract matters: the rule engine only knows that it is applying a substring filter to the target's declared string field.
See src/oa_cohorts/measurables/measurable_resolver.py for the authoritative target-to-class registry.