ADR 001: Strict Variable Identity, \(O(1)\) Column Lookup, and Polymorphic Expressions¶
- Status: Accepted
- Date: 2026-10-14
Context¶
Previously, Variable carried both column: str and alias: str | None = None, along with a computed @property def name(self) -> str returning self.alias or self.column and a @property def title(self) -> str performing ad-hoc .replace("_", " ").title() string manipulation.
This dual identity caused systemic complexity across Dataset, Aggregator, BaseEstimator, and Plotter:
1. Schema Divergence: Variable.to_expr() projected pl.col(self.column).alias(self.name). Whenever alias was set for human-readable display (e.g., Trait("K_locus", alias="Capsule") and Trait("K_type", alias="Capsule") in KleborateParser, or Trait("target", alias=target_var.name) in Aggregator), var.name diverged from var.column or caused naming collisions.
2. Defensive Lookup Clutter: Because callers could not trust whether a variable was indexed by column, alias, or passed as a Variable vs str, both DataModel and Aggregator implemented resolve_variable() with linear fallback scans over variables.values(), and downstream classes maintained redundant target_col vs target_name and strata_cols vs strata_names properties.
3. Leaked Domain Logic: Aggregator contained isinstance cascades checking (isinstance(v, Trait) and v.compositional) or isinstance(v, Taxonomy), checking Spatial/Temporal resolutions to choose between to_expr(), truncate_expr(), and to_resolution_expr(), and branching on Target | Criterion | Variable.
Decision¶
- Single Physical Identity (
column) + Explicit Display Label (title): - Remove
aliasandnamefromVariablecompletely. Variable.column: stris strictly the physical column name in the PolarsDataFrame.-
Variable.title: stris the human-readable display title used in plots and tables. Users may passtitle: str | None = Noneat initialization; if omitted (Noneor""),__post_init__setstitletoself.columnverbatim (exceptEstimate, which defaultstitleto"Prevalence"). Zero automatic.replace("_", " ").title()string manipulation is performed on column names. -
Strict \(O(1)\) Variable Lookup & Removal of
resolve_variable(): variablesmappings onDataModel(Dataset) andStratifiedResultMixin(Aggregator,BaseEstimator) are strictlydict[str, Variable]keyed byvar.column..var(*cols: str)accepts only string column names and performs direct \(O(1)\) dictionary lookup (self.variables[col]).-
Delete
resolve_variable()across the entire codebase. Downstream constructors (Aggregator,Criterion,Target) requireVariableinstances at initialization and use them directly. -
Polymorphism in
seroepi.variables: - Define
@property def is_compositional(self) -> boolon baseVariable(Falseby default,TrueonTaxonomy,self.compositionalonTrait). - Make
Spatial.to_expr()andTemporal.to_expr()polymorphically apply their configuredresolution(to_resolution_expr()andtruncate_expr()respectively whenresolution != UNKNOWN), always aliasing toself.column. - Add
Target.coerce(spec: Target | Criterion | Variable, *, compositional: bool | None = None, negative_indicator: ... = None) -> Targetto normalize any target specification into a canonicalTargetinstance.
Consequences¶
- Positive: Eliminates
resolve_variable(),target_name,strata_names,adjust_for_name, and allisinstancetype-switching cascades inAggregator. - Positive: Guarantees the invariant
var.column in df.columnsat every stage of the pipeline. - Positive: Prevents silent column collisions in
KleborateParserwhere multiple columns share the same biological display title (e.g.,K_locusandK_typeboth havingtitle="Capsule"while keeping distinctcolumn="K_locus"andcolumn="K_type").