Clinical analytics¶
Analytics packages combine domain-neutral retrieval with governed concept sets and clinical interpretation. This is where a procedure becomes evidence of radiotherapy, a series of measurements becomes a weight trajectory, or percentage weight loss becomes a severity grade.
Oncology¶
| API | Capability |
|---|---|
OncologyEpisode |
Classifies episode purpose and modality; traverses events linked to the episode and its direct children. |
structural_modalities / concept_modalities |
Preserve every evidenced modality so mixed-treatment and SACT classification disagreements remain visible. |
structural_modality / concept_modality |
Select one deterministic modality in radiotherapy, surgery, diagnostic/staging, SACT priority order. |
OncologyProcedure / OncologyDrugExposure |
Add governed is_radiotherapy, is_surgery, is_diagnostic_staging, and is_sact questions to CDM facts. |
RTDoseSummary.from_procedures(...) |
Constructs one radiotherapy summary; summarize_rt_procedures_by(...) groups before construction. |
SACTDoseSummary.from_exposures(...) |
Constructs one SACT summary; summarize_sact_exposures_by(...) groups before construction. |
OncologyEpisodeEvent |
Resolves oncology-aware facts while retaining episode-event diagnostics. |
OncologyEpisode is the main entry point for an episode-centred oncology analysis. Keep the object attached to its SQLAlchemy session while accessing properties that traverse related events or resolve vocabulary-backed concept groups:
from sqlalchemy.orm import Session
from omop_alchemy.toolkit.analytics.oncology import OncologyEpisode
with Session(engine) as session:
episode = session.get(OncologyEpisode, episode_id)
if episode is None:
raise LookupError(f"Unknown episode: {episode_id}")
treatment_episodes = episode.child_treatment_episodes
modalities = episode.structural_modalities
sact = episode.sact_dose_summaries_by_drug_concept
radiotherapy = episode.rt_dose_summaries_by_site
The episode includes events linked directly to it and events linked to its direct children. This supports a regimen whose drug exposures or procedures are recorded against cycle-level child episodes without flattening the episode hierarchy itself.
(e.g. cycle-level drug exposures)"] Direct --> Pool["Evidence pool for
modalities, dose summaries, weight loss"] ChildEvents --> Pool
Modality evidence¶
An episode can contain evidence for more than one treatment modality. structural_modalities and concept_modalities therefore return sets rather than forcing the record into a single label:
- structural evidence treats any linked drug exposure as SACT evidence and uses governed concepts for radiotherapy, surgery, and diagnostic or staging procedures;
- concept evidence requires drug exposures to belong to the governed SACT concept set as well.
Comparing the two sets makes source-structure and terminology disagreements visible:
structural = episode.structural_modalities
governed = episode.concept_modalities
if structural != governed:
review_episode_modality(episode.episode_id, structural, governed)
When a caller needs one value, structural_modality and concept_modality apply a deterministic order: radiotherapy, surgery, diagnostic or staging, then SACT. This is a stable tie-break for mixed evidence, not a statement of clinical importance. Use the plural properties when mixed treatment matters to the analysis.
The single-value properties return the first modality in this order for which the episode has evidence.
Treatment summaries¶
sact_exposures contains linked exposures whose concepts belong to the governed SACT set. sact_dose_summaries_by_drug_concept groups them by drug concept; sact_dose_summary provides an all-SACT summary. The summary keeps source units and carries a DoseEvaluability result. Mixed units or missing quantities remain visible instead of being presented as a valid combined dose.
rt_procedures applies the governed radiotherapy procedure set. Site-grouped and whole-episode summaries expose dates, procedure and modifier concepts, counts, quantities, and dose evaluability. OMOP Procedure Occurrence does not provide a universal radiotherapy dose model, so these summaries preserve the available evidence for a site-specific policy rather than inferring one.
OncologyProcedure and OncologyDrugExposure expose the same governed classifications on individual facts. OncologyEpisodeEvent retains resolution diagnostics when a linked event cannot be loaded.
Condition modifiers and preferred stage¶
The oncology package publishes lazy governed concept specifications for T, N,
M, and group stage, tumour grade, and metastatic disease. Laterality and tumour
size are exposed as governed scalar accessors. These declarations consume
omop-semantics; the narrow metastatic-disease descendant group requires
omop-semantics 0.6.1. Importing the module does not expand a vocabulary or
contact a database.
Stage selection is a query policy over an already filtered canonical modifier
source enriched with modifier_concept_code. By default, pathological codes
(trimmed, case-insensitive codes beginning with p) rank before clinical codes
(beginning with c), with unclassified codes retained as fallback. Time and
canonical modifier identity then break ties deterministically.
from omop_alchemy.toolkit.analytics.oncology import preferred_stage_select
# Earliest pathological, otherwise earliest clinical, otherwise unclassified.
preferred = preferred_stage_select(
stage_modifiers,
concept_code_column="modifier_concept_code",
)
The preference is immutable and query-scoped. Override it explicitly rather than changing process-global state:
from omop_alchemy.toolkit.analytics.oncology import StageSelectionSpec
from omop_alchemy.toolkit.core.modifiers import ModifierSelectionPolicy
clinical_first = preferred_stage_select(
stage_modifiers,
spec=StageSelectionSpec.clinical_first(),
concept_code_column="modifier_concept_code",
)
chronological = preferred_stage_select(
stage_modifiers,
spec=StageSelectionSpec.chronological_only(),
)
latest_pathological = preferred_stage_select(
stage_modifiers,
spec=StageSelectionSpec(
temporal_policy=ModifierSelectionPolicy.latest,
),
concept_code_column="modifier_concept_code",
)
Basis-ranked selection requires an enriched source and an explicit
concept_code_column=. Chronological-only selection does not require the
enrichment. An empty basis priority disables pathological/clinical preference;
a non-empty priority must contain every StageBasis exactly once.
Oncology policies for condition modifiers and preferred stage values.
StageSelectionSpec
dataclass
¶
StageSelectionSpec(
basis_priority: tuple[StageBasis, ...] = (
StageBasis.pathological,
StageBasis.clinical,
StageBasis.unclassified,
),
temporal_policy: ModifierSelectionPolicy = ModifierSelectionPolicy.earliest,
)
Preferred stage basis followed by temporal tie-breaking policy.
The default prefers pathological stage, then clinical stage, then values
whose vocabulary code does not identify a basis. Supply an empty
basis_priority for chronological-only selection.
preferred_stage_select ¶
preferred_stage_select(
source: FromClause | SelectBase,
*,
spec: StageSelectionSpec = DEFAULT_STAGE_SELECTION,
concept_code_column: str | None = None,
) -> sa.Select[Any]
Select one preferred stage modifier for every canonical target.
Basis-ranked selection requires a source enriched with a concept-code column. Chronological-only selection does not require that enrichment.
stage_basis_expression ¶
stage_basis_expression(
concept_code: ColumnElement[Any],
) -> sa.ColumnElement[str]
Classify stage codes using the source vocabulary's p/c convention.
The staging vocabulary does not expose a reliable parent concept that separates pathological from clinical staging. The code prefix is therefore the intentional source-level contract rather than a synthetic hierarchy.
stage_basis_priority_expression ¶
stage_basis_priority_expression(
concept_code: ColumnElement[Any],
spec: StageSelectionSpec = DEFAULT_STAGE_SELECTION,
) -> sa.ColumnElement[int]
Render the configured stage-basis preference as a sortable SQL CASE.
Bases: OncologyCriticalWeightLossMixin, OncologySACTDosingMixin, OncologyRTDosingMixin, OncologyEpisodeEventMixin, EpisodeView
Oncology-aware episode view.
This composes generic episode hierarchy support, oncology-aware
Episode_Event resolution, body-metric adverse-event grading, and
treatment dose-summary interfaces. Modality classification exposes both
structural treatment evidence, such as linked drug exposures, and governed
concept evidence, such as SACT-classified drug concepts, so callers can
audit disagreements.
structural_modalities
cached
property
¶
structural_modalities: frozenset[OncologyModality]
All modalities supported by linked event structure.
Any linked drug exposure is treated as structural SACT evidence, while radiotherapy, surgery, and diagnostic/staging require governed procedure concept membership. Events linked to direct child episodes are included.
structural_modality
cached
property
¶
structural_modality: OncologyModality
Highest-priority structural modality, or UNKNOWN when none apply.
Priority is radiotherapy, surgery, diagnostic/staging, then SACT.
concept_modalities
cached
property
¶
concept_modalities: frozenset[OncologyModality]
All modalities supported by linked procedure and drug concept identity.
This is intentionally distinct from structural_modalities so SACT
disagreements remain visible.
concept_modality
cached
property
¶
concept_modality: OncologyModality
Highest-priority concept modality, or UNKNOWN when none apply.
Priority is radiotherapy, surgery, diagnostic/staging, then SACT.
child_treatment_episodes_by_modality
cached
property
¶
child_treatment_episodes_by_modality: dict[
OncologyModality, list[Self]
]
child_treatment_episodes_by_concept_modality
cached
property
¶
child_treatment_episodes_by_concept_modality: dict[
OncologyModality, list[Self]
]
Radiotherapy procedure summary for one caller-chosen site/group key.
OMOP procedure rows do not provide one universal RT dose model. This summary exposes dates, procedure concepts, modifiers, counts, and evaluability so a site-specific RT policy can decide what is clinically meaningful.
from_procedures
classmethod
¶
from_procedures(
procedures: Sequence[OncologyProcedure],
*,
group_key: object,
) -> Self
Summarize radiotherapy procedure rows for one grouping key.
Bases: DrugExposureSummary
SACT dose summary for one caller-chosen drug grouping.
This is deliberately a summary interface, not a dose-reduction rule. It preserves mixed/missing units as evaluability states for downstream SACT policy to interpret.
from_exposures
classmethod
¶
from_exposures(
exposures: Sequence[Drug_Exposure], *, group_key: object
) -> Self
Summarize SACT exposures and attach dose evaluability policy.
Construction is field-based and therefore accepts the base OMOP exposure
type. Oncology filtering belongs to sact_exposures before this summary
boundary; keeping the inherited input type also preserves substitutability.
Body metrics¶
| API | Capability |
|---|---|
MeasurementReading.from_measurement(...) |
Reduces an OMOP measurement to the fields used by calculations and records its resolution source. |
MeasurementSeriesMixin |
Resolves normalised measurement series for an episode. |
WeightTrajectoryMixin |
Exposes normalised weight and height, BMI, BSA, windowed change, trajectories, and a dict-shaped typed summary. |
WeightChange |
Represents percentage change and whether it was evaluable; unevaluable change has pct_change=None. |
WeightTrajectorySummary |
Types the DataFrame- and JSON-friendly mapping returned by weight_trajectory_summary(). |
WeightTrajectoryMixin turns an episode's weight measurements and the person's height measurements into a normalised longitudinal view. Weight is converted to kilograms, height to centimetres, and measurements with missing or unrecognised units are excluded from calculations.
An episode that includes the mixin can produce a compact, tabular summary:
summary = episode.weight_trajectory_summary()
print(summary["baseline_weight_kg"])
print(summary["latest_weight_kg"])
print(summary["pct_change_from_baseline"])
print(summary["pct_change_from_baseline_evaluable"])
The baseline is the first normalised weight in the resolved episode series and the latest weight is the last. Percentage change is negative for weight loss. A result separates its value from evaluability so that missing evidence is not confused with zero change.
pct_change_over(days) compares the latest reading with the earliest reading inside the requested look-back period. pct_change_trajectory() returns every normalised point relative to baseline. sustained_loss() asks whether the final consecutive readings all meet a configurable loss threshold. These are deliberately distinct questions; choose the one that matches the analysis rather than treating them as interchangeable summaries of weight loss.
Body-metric defaults resolve governed measurement and unit concepts. A deployment that uses local concepts can supply its own BodyMetricRules on the episode class.
A resolved numeric measurement reduced to the fields trajectory math needs.
from_measurement
classmethod
¶
from_measurement(
measurement: Measurement, *, source: ReadingSource
) -> Self
Reduce an OMOP measurement row to trajectory input fields.
Episode mixin exposing normalized weight, height, BMI, and trajectories.
Subclasses may set _body_metric_rules to avoid loading the default
omop-semantics-backed concept IDs.
weight_readings
cached
property
¶
weight_readings: list[MeasurementReading]
Weight readings normalized to kg.
height_readings_cm
cached
property
¶
height_readings_cm: list[MeasurementReading]
Height readings normalized to cm and resolved without an episode date window.
height_m
property
¶
height_m: Optional[float]
Return the first normalized height as metres, when available.
baseline_weight
property
¶
baseline_weight: Optional[MeasurementReading]
Return the earliest normalized weight reading in the series.
latest_weight
property
¶
latest_weight: Optional[MeasurementReading]
Return the latest normalized weight reading in the series.
baseline_bmi
property
¶
baseline_bmi: Optional[float]
Calculate BMI from the baseline weight and first available height.
baseline_bsa_mosteller_m2
property
¶
baseline_bsa_mosteller_m2: Optional[float]
Calculate Mosteller BSA from baseline weight and first height.
pct_change_from_baseline ¶
pct_change_from_baseline(
as_of: Optional[MeasurementReading] = None,
) -> WeightChange
Compare a target reading with baseline, preserving non-evaluability.
pct_change_over ¶
pct_change_over(days: int) -> WeightChange
Compare latest weight with the earliest reading in the time window.
pct_change_trajectory ¶
pct_change_trajectory() -> list[WeightTrajectoryPoint]
Return each valid reading's percentage change from baseline.
sustained_loss ¶
sustained_loss(
threshold_pct: float = 5.0, min_consecutive: int = 2
) -> Optional[bool]
Test whether recent readings sustain the requested loss threshold.
weight_trajectory_summary ¶
weight_trajectory_summary() -> WeightTrajectorySummary
Return the stable summary contract used by analytics consumers.
Adverse events¶
| API | Policy |
|---|---|
ctcae_weight_loss_grade(...) |
Grades percentage weight loss against CTCAE-style bins. |
martin_weight_loss_grade(...) |
Applies the Martin et al. BMI-adjusted matrix. |
critical_weight_loss_grade(...) |
Uses the Martin matrix when BMI is available and otherwise falls back to CTCAE-style bins. |
The adverse-event functions apply grading policy to an already calculated percentage change and, where available, BMI:
from omop_alchemy.toolkit.analytics.adverse_events import (
critical_weight_loss_grade,
)
grade = critical_weight_loss_grade(
pct_change=-8.2,
bmi=21.4,
)
martin_weight_loss_grade() applies the BMI-adjusted Martin et al. matrix. ctcae_weight_loss_grade() applies the CTCAE v5.0 physiological percentage-loss thresholds but does not infer intervention qualifiers such as hospitalisation, tube feeding, or parenteral nutrition. critical_weight_loss_grade() uses the Martin matrix when both percentage change and BMI are available and otherwise falls back to the percentage-only CTCAE-style grade.
All three return None when the inputs needed by that policy are unavailable. They do not retrieve measurements or choose a baseline; those decisions remain in the body-metric layer.
CTCAEWeightLoss ¶
CTCAE-style weight-loss severity from percent weight change only.
This implements physiological percent-loss bins.
CTCAE intervention qualifiers such as hospitalisation, tube feeding, or TPN are not inferred here.
MartinWeightLoss ¶
Martin et al. BMI-adjusted percent-weight-loss grading.
Source: Martin L, Senesse P, Gioulbasanis I, et al. "Diagnostic criteria for the classification of cancer-associated weight loss." J Clin Oncol. 2015;33(1):90-99. The published system crosses five percent-weight-loss categories with five BMI categories to give grades 0-4.
This matrix is kept in adverse events because it is clinical severity policy over body measurements, not body-measurement arithmetic itself.
critical_weight_loss_grade ¶
critical_weight_loss_grade(
pct_change: Optional[float], bmi: Optional[float]
) -> Optional[int]
Critical-weight-loss grade using Martin where BMI is available.
Falls back to CTCAE-style percent-weight-loss grading when percent change is evaluable but BMI is not, preserving coverage without guessing BMI.