Skip to content

Latest commit

Β 

History

History
701 lines (602 loc) Β· 24.3 KB

File metadata and controls

701 lines (602 loc) Β· 24.3 KB

Data Flow

End-to-end data pipeline documentation covering the complete flow from raw data sources through transformations to API responses.


Table of Contents


Overview

Python AMR processes two distinct types of data:

  1. Reference Data - Static datasets (microorganisms, antimicrobials, breakpoints)
  2. Runtime Data - User inputs and analytical results

High-Level Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   Reference Data Pipeline                  β”‚
β”‚  External β†’ Import β†’ NDJSON β†’ Transforms β†’ Parquet        β”‚
β”‚  Sources                                                   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
                              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Application Layer                       β”‚
β”‚  API/CLI β†’ Core Logic β†’ Reference Store β†’ Results         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
                              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   Persistence Pipeline                     β”‚
β”‚  Results β†’ Queue β†’ Repository β†’ Database β†’ Storage        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Reference Data Pipeline

Pipeline Stages

External Sources (TSV/CSV/XLSX/RDA)
    β”‚
    β”œβ”€β–Ί data-raw/external/
    β”‚       β€’ Loinc.csv (200 MB)
    β”‚       β€’ SNOMED_*.txt (5 MB)
    β”‚       β€’ GBIF Taxon.tsv (1 GB)
    β”‚       β€’ EUCAST breakpoints
    β”‚
    β–Ό
Import Scripts (scripts/import_raw_sources.py)
    β”‚
    β”œβ”€β–Ί Format detection and parsing
    β”œβ”€β–Ί Schema validation
    β”œβ”€β–Ί Deduplication
    β”‚
    β–Ό
Canonical NDJSON (data-raw/sources/)
    β”‚
    β”œβ”€β–Ί microorganisms.ndjson (75k records)
    β”œβ”€β–Ί antimicrobials.ndjson (465 records)
    β”œβ”€β–Ί antivirals.ndjson (85 records)
    β”œβ”€β–Ί clinical_breakpoints.ndjson (50k records)
    β”œβ”€β–Ί interpretive_rules.ndjson (1.3k records)
    β”œβ”€β–Ί intrinsic_resistant.ndjson (10k records)
    β”‚
    β–Ό
Transform Pipeline (scripts/run_data_pipeline.py)
    β”‚
    β”œβ”€β–Ί Load NDJSON with Polars
    β”œβ”€β–Ί Apply dataset-specific transforms
    β”œβ”€β–Ί Add computed fields
    β”œβ”€β–Ί Fix data quality issues
    β”‚
    β–Ό
Parquet Snapshots (data/snapshots/)
    β”‚
    β”œβ”€β–Ί microorganisms.parquet
    β”œβ”€β–Ί antimicrobials.parquet
    β”œβ”€β–Ί antivirals.parquet
    β”œβ”€β–Ί clinical_breakpoints.parquet
    β”œβ”€β–Ί interpretive_rules.parquet
    β”œβ”€β–Ί intrinsic_resistant.parquet
    β”‚
    β–Ό
Snapshot Manifest (data/manifests/snapshot_manifest.json)
    β”‚
    └─► Metadata, checksums, schemas

Detailed Flow

1. External Sources β†’ Import

# scripts/import_raw_sources.py

def import_external_sources():
    """
    Import external formats to canonical NDJSON.

    Supported formats:
    - CSV/TSV (Polars)
    - XLSX (pandas + openpyxl)
    - Feather/IPC (Polars)
    - Parquet (Polars)
    - RDA (via feather intermediate)
    """
    mapping = load_import_mapping()  # data-raw/import_mapping.json

    for dataset_name, source_file in mapping.items():
        # Detect format
        format = detect_format(source_file)

        # Parse with appropriate loader
        df = parse_file(source_file, format)

        # Validate schema
        validate_schema(df, dataset_name)

        # Write NDJSON
        output_path = f"data-raw/sources/{dataset_name}.ndjson"
        df.write_ndjson(output_path)

2. NDJSON β†’ Transforms

# scripts/run_data_pipeline.py

from amr.data.pipeline import DataPipeline
from amr.data.transforms import TRANSFORM_REGISTRY

def run_pipeline():
    """
    Execute transformation pipeline.

    Order defined in data-raw/pipeline.json
    """
    pipeline = DataPipeline(
        raw_dir="data-raw/sources",
        snapshot_dir="data/snapshots"
    )

    # Load execution order
    execution_order = load_pipeline_config()

    for dataset_name in execution_order:
        # Load NDJSON
        df = pipeline.load_raw(dataset_name)

        # Apply transforms
        transform = TRANSFORM_REGISTRY.get(dataset_name)
        if transform:
            df = transform.apply(df)

        # Write snapshot
        pipeline.write_snapshot(dataset_name, df)

    # Build manifest
    pipeline.build_manifest()

3. Transform Examples

Microorganisms Transform:

# src/amr/data/transforms/microorganisms.py

class MicroorganismsTransform:
    """Transform microorganisms dataset."""

    def apply(self, df: pl.DataFrame) -> pl.DataFrame:
        """
        Apply transformations:
        1. Add fullname column (genus + species)
        2. Normalize prevalence scores
        3. Add rank_index for sorting
        4. Validate taxonomy hierarchy
        """
        return (
            df
            .with_columns([
                # Combine genus and species
                (pl.col("genus") + " " + pl.col("species")).alias("fullname"),

                # Normalize prevalence
                (pl.col("prevalence") / pl.col("prevalence").max()).alias("prevalence_norm"),

                # Add rank index
                pl.col("rank").map_dict({
                    "kingdom": 1,
                    "phylum": 2,
                    "class": 3,
                    "order": 4,
                    "family": 5,
                    "genus": 6,
                    "species": 7,
                    "subspecies": 8
                }).alias("rank_index")
            ])
            .filter(pl.col("genus").is_not_null())  # Remove invalid entries
        )

Clinical Breakpoints Transform:

# src/amr/data/transforms/clinical_breakpoints.py

class ClinicalBreakpointsTransform:
    """Transform clinical breakpoints dataset."""

    def apply(self, df: pl.DataFrame) -> pl.DataFrame:
        """
        Apply transformations:
        1. Parse MIC/disk breakpoint ranges
        2. Add host_type (human/animal)
        3. Validate guideline consistency
        4. Add effective_date
        """
        return (
            df
            .with_columns([
                # Parse MIC breakpoints
                pl.col("breakpoint_S").cast(pl.Float64, strict=False),
                pl.col("breakpoint_R").cast(pl.Float64, strict=False),

                # Infer host type
                pl.when(pl.col("host").is_not_null())
                  .then(pl.lit("animal"))
                  .otherwise(pl.lit("human"))
                  .alias("host_type"),

                # Extract year from guideline
                pl.col("guideline")
                  .str.extract(r"(\d{4})", 1)
                  .cast(pl.Int32, strict=False)
                  .alias("guideline_year")
            ])
            .filter(
                # Remove invalid breakpoints
                (pl.col("breakpoint_S").is_not_null()) |
                (pl.col("breakpoint_R").is_not_null())
            )
        )

Runtime Data Flow

API Request β†’ Response Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 1. HTTP Request                                     β”‚
β”‚    POST /v1/sir/interpret                           β”‚
β”‚    {values: [0.5, 2, 16], mo: "B_ESCHR_COLI", ...} β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                   β”‚
                   β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 2. FastAPI Handler                                  β”‚
β”‚    β€’ Pydantic validation                            β”‚
β”‚    β€’ Request parsing                                β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                   β”‚
                   β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 3. Core Logic (amr.core.interpretation)             β”‚
β”‚    β€’ Load breakpoints from reference store          β”‚
β”‚    β€’ Apply interpretation rules                     β”‚
β”‚    β€’ Return SIR results                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                   β”‚
                   β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 4. Persistence Queue (optional)                     β”‚
β”‚    β€’ Enqueue run data                               β”‚
β”‚    β€’ Return optimistic run_id                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                   β”‚
                   β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 5. HTTP Response                                    β”‚
β”‚    {results: ["S", "S", "R"], run_id: 123}         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

SIR Interpretation Flow

Detailed Sequence

User Input: values=[0.5, 2, 16], mo="B_ESCHR_COLI", ab="CIP"
    β”‚
    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 1. Normalize Inputs                     β”‚
β”‚    β€’ mo β†’ "B_ESCHR_COLI" (if needed)   β”‚
β”‚    β€’ ab β†’ "CIP" (if needed)            β”‚
β”‚    β€’ Detect method (MIC/disk)          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚
             β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 2. Load Reference Data                  β”‚
β”‚    ReferenceStore.get_breakpoints()     β”‚
β”‚    └─► Returns Polars DataFrame        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚
             β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 3. Filter Breakpoints                   β”‚
β”‚    df.filter(                           β”‚
β”‚        (pl.col("mo") == mo) &           β”‚
β”‚        (pl.col("ab") == ab) &           β”‚
β”‚        (pl.col("guideline") == guide)   β”‚
β”‚    )                                    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚
             β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 4. Apply Breakpoint Logic               β”‚
β”‚    For each value:                      β”‚
β”‚      if value <= S_breakpoint: "S"      β”‚
β”‚      elif value >= R_breakpoint: "R"    β”‚
β”‚      else: "I"                          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚
             β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 5. Apply Interpretive Rules (optional)  β”‚
β”‚    β€’ EUCAST expert rules                β”‚
β”‚    β€’ Intrinsic resistance               β”‚
β”‚    β€’ Disk/MIC discrepancy resolution    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚
             β–Ό
Output: ["S", "S", "R"]

Code Flow

# src/amr/core/interpretation.py

def as_sir(
    values: list,
    mo: str | None,
    ab: str | None,
    guideline: str = "EUCAST 2025",
    method: str | None = None,
    add_intrinsic_resistance: bool = True,
    interpretive_rules: str | None = None
) -> list[str]:
    """
    Interpret MIC/disk values to SIR categories.

    Data flow:
    1. Input validation
    2. Reference data lookup
    3. Breakpoint application
    4. Rule application
    5. Result return
    """
    # 1. Validate inputs
    if not values:
        raise ValueError("values cannot be empty")

    # 2. Load breakpoints
    from amr.data.reference_store import get_breakpoints
    breakpoints_df = get_breakpoints()

    # 3. Filter to relevant breakpoints
    filtered = breakpoints_df.filter(
        (pl.col("mo") == mo) &
        (pl.col("ab") == ab) &
        (pl.col("guideline") == guideline) &
        (pl.col("method") == (method or "MIC"))
    )

    if filtered.height == 0:
        raise ValueError(f"No breakpoints found for {mo}/{ab}/{guideline}")

    # 4. Extract breakpoint values
    s_breakpoint = filtered["breakpoint_S"][0]
    r_breakpoint = filtered["breakpoint_R"][0]

    # 5. Apply breakpoints
    results = []
    for value in values:
        if value <= s_breakpoint:
            sir = "S"
        elif value >= r_breakpoint:
            sir = "R"
        else:
            sir = "I"
        results.append(sir)

    # 6. Apply interpretive rules
    if interpretive_rules == "EUCAST":
        results = apply_eucast_rules(results, mo, ab)

    if add_intrinsic_resistance:
        results = apply_intrinsic_resistance(results, mo, ab)

    return results

Antibiogram Generation Flow

Input DataFrame:
    patient_id | organism    | AMX | CIP | GEN
    1          | E. coli     | R   | S   | S
    2          | E. coli     | S   | S   | S
    3          | K. pneumo   | R   | R   | S
    β”‚
    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 1. Normalize Organisms                  β”‚
β”‚    organism β†’ mo_code via as_mo()       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚
             β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 2. Group by Pathogen                    β”‚
β”‚    df.group_by("mo_code")               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚
             β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 3. Calculate Resistance %                β”‚
β”‚    For each (pathogen, antimicrobial):  β”‚
β”‚      n_resistant = count(SIR == "R")    β”‚
β”‚      n_total = count(*)                 β”‚
β”‚      pct = 100 * n_resistant / n_total  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚
             β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 4. Filter by Minimum                    β”‚
β”‚    Keep only pathogens with n >= min    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚
             β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 5. Compute Confidence Intervals          β”‚
β”‚    Wilson score interval for each %     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚
             β–Ό
Output DataFrame:
    microorganism    | AMX  | CIP  | GEN
    Escherichia coli | 50.0 | 0.0  | 0.0
    K. pneumoniae    | 100  | 100  | 0.0

Run Persistence Flow

Async Queue Architecture

API Handler
    β”‚
    β”‚ enqueue_persist(run_data)
    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚     AsyncQueue (4096 max)       β”‚
β”‚  β”Œβ”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”     β”‚
β”‚  β”‚ 1 β”‚ 2 β”‚ 3 β”‚ 4 β”‚ 5 β”‚...β”‚     β”‚
β”‚  β””β”€β”€β”€β”΄β”€β”€β”€β”΄β”€β”€β”€β”΄β”€β”€β”€β”΄β”€β”€β”€β”΄β”€β”€β”€β”˜     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚
          β”‚ queue.get()
          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Worker (async coroutine)      β”‚
β”‚   β€’ Retry loop (max 3)          β”‚
β”‚   β€’ Exponential backoff         β”‚
β”‚   β€’ Error handling              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚
          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚      RunRepository              β”‚
β”‚  β€’ save_run_async()             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚
          β”œβ”€β”€β–Ί SQLite/Postgres (metadata)
          β”‚    INSERT INTO runs (...)
          β”‚
          └──► DuckDB (output rows)
               INSERT INTO run_outputs (...)

Data Transformation

# Input (API request)
{
  "values": [0.5, 2, 16],
  "microorganism": "B_ESCHR_COLI",
  "antimicrobial": "CIP",
  "guideline": "EUCAST 2025"
}

# Transformed for persistence
{
  "run_type": "sir",
  "input_data": {
    "values": [0.5, 2, 16],
    "microorganism": "B_ESCHR_COLI",
    "antimicrobial": "CIP",
    "guideline": "EUCAST 2025"
  },
  "output_data": {
    "results": ["S", "S", "R"]
  },
  "row_count": 3,
  "created_at": "2026-02-15T12:00:00Z",
  "metadata": {
    "source": "lab_system",
    "batch_id": "2026-02-15-001"
  }
}

# Stored in database
runs table:
  id | run_type | row_count | created_at | metadata_json

run_outputs table (DuckDB):
  run_id | row_index | input_value | sir_result
  123    | 0         | 0.5         | S
  123    | 1         | 2.0         | S
  123    | 2         | 16.0        | R

Data Refresh Flow

LOINC Refresh Example

External Source
    β”‚
    β”œβ”€β–Ί Download Loinc.csv (200 MB)
    β”‚   from loinc.org
    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ scripts/refresh_loinc.py         β”‚
β”‚  β€’ Parse Loinc.csv              β”‚
β”‚  β€’ Filter DRUG/TOX classes      β”‚
β”‚  β€’ Normalize names              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚
          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Load antimicrobials.ndjson       β”‚
β”‚  β€’ Read existing data           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚
          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Fuzzy Match                      β”‚
β”‚  β€’ Normalize both sides         β”‚
β”‚  β€’ Match by name similarity     β”‚
β”‚  β€’ Add LOINC codes              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚
          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Write antimicrobials.ndjson      β”‚
β”‚  β€’ Preserve existing data       β”‚
β”‚  β€’ Add 'loinc' field            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚
          β–Ό
Run Data Pipeline
    └─► Rebuild snapshots

Error and Dead-Letter Flow

Queue Worker
    β”‚
    β”‚ save_run_async(run_data)
    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Try: Save to database          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚
          β”œβ”€β–Ί Success
          β”‚   └─► Increment counter
          β”‚
          β”œβ”€β–Ί Retriable Error (transient)
          β”‚   └─► Retry with backoff
          β”‚       β”œβ”€β–Ί Retry 1 (50ms delay)
          β”‚       β”œβ”€β–Ί Retry 2 (100ms delay)
          β”‚       └─► Retry 3 (200ms delay)
          β”‚
          └─► Permanent Failure
              └─► Save to Dead Letter Queue
                  β”‚
                  β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  dead_letters table              β”‚
β”‚  β€’ Redact sensitive data        β”‚
β”‚  β€’ Truncate large payloads      β”‚
β”‚  β€’ Store error message          β”‚
β”‚  β€’ Add timestamp                β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Data Transformations

Common Transform Patterns

1. Type Coercion

df.with_columns([
    pl.col("numeric_field").cast(pl.Float64, strict=False),
    pl.col("date_field").str.strptime(pl.Date, "%Y-%m-%d"),
    pl.col("boolean_field").cast(pl.Boolean)
])

2. Null Handling

df.with_columns([
    pl.col("field").fill_null("DEFAULT_VALUE"),
    pl.when(pl.col("field").is_null())
      .then(pl.lit("N/A"))
      .otherwise(pl.col("field"))
      .alias("field_safe")
])

3. String Normalization

df.with_columns([
    pl.col("name")
      .str.to_uppercase()
      .str.strip_chars()
      .str.replace_all(r"\s+", " ")
      .alias("name_normalized")
])

4. Computed Fields

df.with_columns([
    (pl.col("genus") + " " + pl.col("species")).alias("fullname"),
    (pl.col("resistance_count") / pl.col("total_count") * 100).alias("resistance_pct")
])

Related Documentation


Last Updated: 2026-02-15