Skip to content

episodes

Domain-neutral machinery for building episodes and retrieving what belongs to them. A drug episode behaves the same whether the drug is a cytotoxic agent or an antibiotic, so everything here takes concept filters and grouping keys as parameters rather than assuming a clinical specialty. Domain-specific episode classes — for example OncologyEpisode — compose these pieces with their own concept sets and live in analytics.

derivation

How episodes are constructed and related to one another — building episode queries and resolving parent/child hierarchy, written against the raw Episode/Episode_Event tables rather than any materialised view.

Not yet populated. The equivalent built against materialised-view subclasses lives in omop-constructs.

handling

What is inside an episode once it exists.

Linked drug exposures. DrugEpisodeMixin adds retrieval and grouped summaries to any episode view:

from omop_alchemy.toolkit.episodes.handling import DrugEpisodeMixin

class MyEpisode(DrugEpisodeMixin, EpisodeView):
    _drug_concept_ids = my_concept_ids

episode.drug_exposures                # resolved Drug_Exposure rows
episode.drug_exposure_summaries_by()  # grouped by drug concept by default

Construct a summary for an already selected set of rows through the summary type itself:

from omop_alchemy.toolkit.episodes.handling import DrugExposureSummary

summary = DrugExposureSummary.from_exposures(exposures, group_key="regimen-a")

Dose quantities are frequently not comparable across agents, because source units and quantities arrive unnormalised. DoseEvaluability carries that judgement alongside the number, so a summary that cannot be interpreted as a dose says so rather than presenting a misleading total.

Explicit links versus admitted-by-window. Facts linked through Episode_Event are always honoured. episode_attachment_window computes the bounded, date-based fallback window used when a caller opts in to admitting same-person facts that weren't explicitly linked.

Resolution diagnostics. Episode_EventView.resolved_event already resolves an Episode_Event link best-effort, returning None on failure. ResolvedEpisodeEvent extends it to explain why — a miscoded field concept, a target class not yet registered, or a genuinely dangling reference:

from omop_alchemy.toolkit.episodes.handling import ResolvedEpisodeEvent

ee = session.get(ResolvedEpisodeEvent, (episode_id, event_id, field_concept_id))
ee.resolved_event               # the resolved row, or None
ee.event_resolution_diagnostics # [] if resolved cleanly, otherwise why not

Mix ResolvedEpisodeEventMixin into an episode view to reach diagnostics through ordinary episode.episode_events traversal instead of a direct query — this is how OncologyEpisode gets diagnostics on oncology-aware event resolution for free.

Retrieve and summarise the clinical facts an episode contains.

Once an episode exists, the recurring question is what belongs to it. Some facts are linked explicitly through Episode_Event; others fall inside the episode's dates but were never linked. This module resolves both, and is explicit about which rule admitted each row.

Explicit Episode_Event links are always honoured. A bounded date-based fallback can be enabled per caller for sources that do not populate episode links reliably, and episode_attachment_window defines the window used. Open-ended episodes are handled without letting the window run unbounded.

DrugEpisodeMixin adds linked-drug retrieval to any episode view. Subclasses supply the concept filter and grouping key::

class MyEpisode(DrugEpisodeMixin, EpisodeView):
    _drug_concept_ids = my_concept_ids

episode.drug_exposures                  # resolved rows
episode.drug_exposure_summaries_by()    # grouped summaries

Summaries group exposures by any key the caller chooses — drug concept, ingredient, or regimen member — via DrugExposureSummary and summarize_drug_exposures_by.

Dose quantities are frequently not comparable across agents, because source units and quantities arrive unnormalised. DoseEvaluability carries that judgement alongside the number, so a summary that cannot be interpreted as a dose says so rather than presenting a misleading total.

An explicit Episode_Event link can still fail to resolve — a miscoded field concept, a target class not yet registered, a dangling reference. ResolvedEpisodeEvent reports which of those applied instead of silently returning None; mix ResolvedEpisodeEventMixin into an episode view to reach it through ordinary episode.episode_events traversal.

DoseEvaluability dataclass

DoseEvaluability(
    evaluable: bool, reason: Optional[str] = None
)

Whether a dose-like summary can be interpreted as a dose quantity.

Drug exposure rows often carry source units and quantities without enough normalization to compare across agents or regimens. Domain-specific dosing modules should use this as a small shared vocabulary instead of silently treating missing or mixed units as comparable.

DrugEpisodeMixin

Episode mixin for generic linked-drug retrieval and summary.

Domain-specific subclasses can supply concept filters, grouping keys, and dose validity rules. This base layer only knows how to find and summarise drug exposure rows.

DrugExposureSummary dataclass

DrugExposureSummary(
    group_key: object,
    n_exposures: int,
    first_start_date: Optional[date],
    last_start_date: Optional[date],
    total_quantity: Optional[float],
    dose_unit_source_values: frozenset[str],
    drug_concept_ids: frozenset[int],
)

Small generic summary of drug exposures grouped by a caller-chosen key.

The grouping key can be a drug concept, ingredient concept, regimen member, or any downstream classifier. This type deliberately does not encode SACT or any other clinical programme-specific policy.

from_exposures classmethod

from_exposures(
    exposures: Sequence[Drug_Exposure], *, group_key: object
) -> Self

Summarize drug exposure rows for one grouping key.

EpisodeEventResolutionDiagnostic dataclass

EpisodeEventResolutionDiagnostic(
    kind: ResolutionDiagnosticKind,
    episode_id: int,
    event_id: int,
    episode_event_field_concept_id: int,
    message: str,
)

Advisory detail for an episode_event row whose target cannot be resolved.

Resolution remains best-effort: unresolved rows still return None from resolved_event. These diagnostics give maintenance and QA code enough information to distinguish miscoded field concepts from harmless ORM coverage gaps and genuine dangling event references.

ResolvedEpisodeEvent

Bases: Episode_EventView

Episode-event view that explains why a link did not resolve.

Episode_EventView.resolved_event already resolves best-effort and returns None on failure. event_resolution_diagnostics names which of three reasons applied: the field concept is not a recognised ModifierFieldConcepts value, it is recognised but no registered ORM class declares it, or the target row itself is missing.

Query this class directly for diagnostics on a known episode_event, or mix ResolvedEpisodeEventMixin into an episode view to reach it through ordinary episode.episode_events traversal.

ResolvedEpisodeEventMixin

Relationship override so episode views can traverse to resolution diagnostics.

Mix into an EpisodeContext-derived view to have episode_events load ResolvedEpisodeEvent rows instead of the bare Episode_EventView, making episode.episode_events[0].event_resolution_diagnostics reachable through ordinary traversal rather than a direct query.

episode_attachment_window

episode_attachment_window(
    episode,
    *,
    days_prior: int = DEFAULT_EPISODE_WINDOW_DAYS_PRIOR,
    open_end_fallback_days: int = DEFAULT_EPISODE_OPEN_END_FALLBACK_DAYS,
) -> tuple[date, date]

Date window used for episode-attributable facts admitted by date rather than by an explicit Episode_Event link.

Time-varying facts such as body measurements use a bounded episode-relative window. An explicit episode_end_date is always honoured, however long the episode ran. Only open-ended episodes fall back to a finite post-start window, so a missing end date cannot absorb a person's entire future history.

resolve_drug_exposure_series

resolve_drug_exposure_series(
    episode,
    concept_ids: Optional[Sequence[int]] = None,
    *,
    include_window: bool = False,
) -> list[Drug_Exposure]

Resolve drug exposures linked to an episode.

Explicit Episode_Event links are always honoured. A bounded episode-date fallback can be enabled by callers that have a meaningful concept set and want to recover unlinked rows. The default stays explicit-only to avoid accidentally attributing unrelated same-person medications to a drug episode.