Recipe 14.2: Python Implementation Example

Heads up: This is a deliberately simple, illustrative implementation of the pseudocode walkthrough from Recipe 14.2. It shows one way you could translate patient-provider assignment concepts into working Python code using PuLP (an open-source linear programming library). It is not production-ready. Think of it as the sketchpad version: useful for understanding the shape of the solution, not something you'd deploy to your panel management system on Monday morning. Consider it a starting point, not a destination.


Setup

You'll need a few Python packages:

pip install pulp boto3

PuLP is a modeling library for linear and mixed-integer programming. It ships with the CBC solver (open-source, no license required), which handles assignment problems with hundreds of patients and dozens of providers in seconds.

Your environment needs AWS credentials configured (via environment variables, an instance profile, or ~/.aws/credentials). The IAM role or user needs s3:GetObject, s3:PutObject, dynamodb:PutItem, and dynamodb:GetItem.


Config and Constants

Before we get to the optimization logic, here's the configuration that defines the problem. These constants represent a typical primary care practice with multiple providers. In production, you'd pull these from your EHR panel management data rather than hardcoding them.

import json
from dataclasses import dataclass
from decimal import Decimal

# Provider definitions: who's available and what's their capacity?
# Panel size targets come from your medical director. They vary by provider type,
# FTE status, and whether the provider is ramping up (new hire) or winding down (retiring).

PROVIDERS = {
    "DR-CHEN": {
        "name": "Dr. Sarah Chen",
        "specialty": "family_medicine",
        "panel_target": 1800,      # ideal active patient count
        "panel_max": 2000,         # hard cap: beyond this, quality suffers
        "panel_current": 1650,     # how many patients they have today
        "fte": 1.0,               # full-time
        "accepting_new": True,
        "languages": ["en", "zh"],
        "gender": "F",
    },
    "DR-MARTINEZ": {
        "name": "Dr. Carlos Martinez",
        "specialty": "family_medicine",
        "panel_target": 1800,
        "panel_max": 2000,
        "panel_current": 1920,     # over target, approaching max
        "fte": 1.0,
        "accepting_new": False,    # panel is nearly full
        "languages": ["en", "es"],
        "gender": "M",
    },
    "DR-PATEL": {
        "name": "Dr. Priya Patel",
        "specialty": "internal_medicine",
        "panel_target": 1600,      # internists often have smaller panels (sicker patients)
        "panel_max": 1800,
        "panel_current": 1200,     # new provider, ramping up
        "fte": 1.0,
        "accepting_new": True,
        "languages": ["en", "hi", "gu"],
        "gender": "F",
    },
    "NP-JOHNSON": {
        "name": "Taylor Johnson, NP",
        "specialty": "family_medicine",
        "panel_target": 1200,      # NPs typically have smaller panels
        "panel_max": 1400,
        "panel_current": 950,
        "fte": 0.8,               # 4 days per week
        "accepting_new": True,
        "languages": ["en"],
        "gender": "NB",
    },
}

# Patients needing assignment. In production, this comes from a query:
# "patients without a PCP" or "patients whose PCP left the practice."
# Each patient has attributes that influence the match quality.

PATIENTS_TO_ASSIGN = [
    {
        "patient_id": "PAT-001",
        "age": 72,
        "conditions": ["diabetes", "hypertension", "ckd"],
        "complexity": "high",
        "language_preference": "en",
        "gender_preference": None,       # no preference
        "previous_provider": "DR-SMITH", # left the practice
        "visit_frequency": "monthly",    # how often they come in
    },
    {
        "patient_id": "PAT-002",
        "age": 34,
        "conditions": [],
        "complexity": "low",
        "language_preference": "es",
        "gender_preference": "F",
        "previous_provider": None,       # new to practice
        "visit_frequency": "annual",
    },
    {
        "patient_id": "PAT-003",
        "age": 55,
        "conditions": ["diabetes", "depression"],
        "complexity": "medium",
        "language_preference": "zh",
        "gender_preference": None,
        "previous_provider": "DR-SMITH",
        "visit_frequency": "quarterly",
    },
    {
        "patient_id": "PAT-004",
        "age": 28,
        "conditions": ["asthma"],
        "complexity": "low",
        "language_preference": "en",
        "gender_preference": None,
        "previous_provider": None,
        "visit_frequency": "biannual",
    },
    {
        "patient_id": "PAT-005",
        "age": 65,
        "conditions": ["chf", "copd", "diabetes"],
        "complexity": "high",
        "language_preference": "hi",
        "gender_preference": "F",
        "previous_provider": "DR-SMITH",
        "visit_frequency": "biweekly",
    },
    {
        "patient_id": "PAT-006",
        "age": 8,
        "conditions": [],
        "complexity": "low",
        "language_preference": "en",
        "gender_preference": None,
        "previous_provider": None,
        "visit_frequency": "annual",
    },
    {
        "patient_id": "PAT-007",
        "age": 45,
        "conditions": ["hypertension"],
        "complexity": "medium",
        "language_preference": "gu",
        "gender_preference": "F",
        "previous_provider": None,
        "visit_frequency": "quarterly",
    },
]

# Scoring weights: how much does each factor matter in the assignment?
# These are tunable. Your medical director and ops team should agree on these.
# Higher weight = more influence on the assignment decision.

WEIGHTS = {
    "language_match": 30,        # speaking the patient's language is huge
    "gender_preference": 20,     # respecting stated preference
    "complexity_match": 25,      # complex patients to experienced providers
    "panel_balance": 15,         # distribute evenly relative to targets
    "continuity_bonus": 10,      # keep patients with same-team providers if possible
}

# Continuity mapping: when a provider leaves, which remaining providers
# were on the same care team? Patients of DR-SMITH get a small bonus
# for being assigned to someone who already knows their case from huddles.
CONTINUITY_MAP = {
    "DR-SMITH": ["DR-CHEN", "DR-MARTINEZ"],  # Smith's care team partners
}

# Visit frequency weights: high-frequency patients consume more panel capacity.
# A patient who comes monthly uses roughly 12x the appointment slots of an annual patient.
FREQUENCY_WEIGHTS = {
    "biweekly": 26,
    "monthly": 12,
    "quarterly": 4,
    "biannual": 2,
    "annual": 1,
}

Step 1: Compute Preference Scores

The pseudocode calls this compute_preferences(patients, providers). It builds a score matrix where each cell represents how good a particular patient-provider pairing would be. Higher scores mean better matches.

def compute_preference_score(patient: dict, provider_id: str, provider: dict) -> float:
    """
    Compute a match quality score for a single patient-provider pair.

    This is the heart of the assignment problem. The score combines multiple
    factors (language, gender preference, clinical complexity, panel balance)
    into a single number that the optimizer can maximize.

    The score is NOT a probability. It's a weighted sum of match indicators.
    A perfect match might score 100; a terrible match might score 0 or negative.

    Args:
        patient: Patient record with attributes
        provider_id: Provider identifier
        provider: Provider record with capacity and attributes

    Returns:
        Float score. Higher = better match.
    """
    score = 0.0

    # --- Language match ---
    # If the patient has a language preference and the provider speaks it,
    # that's a major quality signal. Concordant language improves outcomes,
    # reduces interpreter costs, and increases patient satisfaction.
    if patient["language_preference"]:
        if patient["language_preference"] in provider["languages"]:
            score += WEIGHTS["language_match"]
        # No penalty for mismatch; just no bonus. Every practice has interpreter services.

    # --- Gender preference ---
    # If the patient stated a gender preference and the provider matches, bonus.
    # If they stated a preference and it doesn't match, penalty (we'd rather not assign here).
    if patient["gender_preference"]:
        if patient["gender_preference"] == provider["gender"]:
            score += WEIGHTS["gender_preference"]
        else:
            score -= WEIGHTS["gender_preference"] * 0.5  # soft penalty, not a hard block

    # --- Complexity match ---
    # High-complexity patients (multiple chronic conditions, frequent visits) should
    # go to experienced physicians rather than new NPs still building their practice.
    # This isn't about capability; it's about panel composition balance.
    if patient["complexity"] == "high":
        if provider["specialty"] == "internal_medicine":
            score += WEIGHTS["complexity_match"]  # internists handle complex patients well
        elif provider["specialty"] == "family_medicine" and provider["fte"] == 1.0:
            score += WEIGHTS["complexity_match"] * 0.7  # full-time FM docs can handle it
        else:
            score += WEIGHTS["complexity_match"] * 0.3  # part-time or NP: less ideal for complex
    elif patient["complexity"] == "low":
        # Low-complexity patients are great for providers ramping up their panels
        remaining_capacity = provider["panel_max"] - provider["panel_current"]
        if remaining_capacity > 400:  # lots of room
            score += WEIGHTS["complexity_match"] * 0.5

    # --- Panel balance ---
    # Prefer providers who are further from their target. This naturally distributes
    # patients toward providers with more capacity.
    remaining_to_target = provider["panel_target"] - provider["panel_current"]
    if remaining_to_target > 0:
        # Provider is below target: good candidate for new patients
        # Scale the bonus by how far below target they are (normalized to 0-1)
        balance_ratio = min(1.0, remaining_to_target / 500)
        score += WEIGHTS["panel_balance"] * balance_ratio
    else:
        # Provider is at or above target: penalize proportionally
        over_ratio = min(1.0, abs(remaining_to_target) / 200)
        score -= WEIGHTS["panel_balance"] * over_ratio

    # --- Continuity bonus ---
    # If the patient's previous provider left and this provider was on the same
    # care team, there's a continuity benefit. They've discussed this patient in
    # huddles, they know the care plan, the transition is smoother.
    if patient["previous_provider"] and patient["previous_provider"] in CONTINUITY_MAP:
        if provider_id in CONTINUITY_MAP[patient["previous_provider"]]:
            score += WEIGHTS["continuity_bonus"]

    return score

def build_preference_matrix(patients: list, providers: dict) -> dict:
    """
    Build the full preference score matrix for all patient-provider pairs.

    Returns a nested dict: scores[patient_id][provider_id] = score
    """
    scores = {}
    for patient in patients:
        pid = patient["patient_id"]
        scores[pid] = {}
        for prov_id, prov_data in providers.items():
            scores[pid][prov_id] = compute_preference_score(patient, prov_id, prov_data)

    return scores

Step 2: Formulate and Solve the Assignment Problem

The pseudocode calls this solve_assignment(preferences, constraints). We use PuLP to formulate a binary integer program where each decision variable represents whether a specific patient is assigned to a specific provider.

import pulp

def solve_assignment(
    patients: list,
    providers: dict,
    preference_scores: dict,
) -> dict:
    """
    Formulate and solve the patient-provider assignment optimization.

    This is a variant of the assignment problem (a classic in operations research).
    Each patient must be assigned to exactly one provider. The objective is to
    maximize total preference score (match quality) subject to capacity constraints.

    Decision variables:
        x[i][j] = 1 if patient i is assigned to provider j, 0 otherwise

    Objective:
        Maximize sum of (preference_score[i][j] * x[i][j]) for all i, j

    Constraints:
        1. Each patient assigned to exactly one provider
        2. No provider exceeds their panel maximum
        3. Providers not accepting new patients get zero new assignments
        4. Weighted capacity: high-frequency patients count more against panel limits

    Args:
        patients: List of patient records
        providers: Dict of provider records
        preference_scores: Nested dict of scores[patient_id][provider_id]

    Returns:
        Dict with assignments, objective value, and solver status.
    """

    patient_ids = [p["patient_id"] for p in patients]
    provider_ids = list(providers.keys())

    # Create the optimization model
    model = pulp.LpProblem("PatientProviderAssignment", pulp.LpMaximize)

    # --- Decision Variables ---
    # x[i][j] = 1 if patient i is assigned to provider j, 0 otherwise.
    # These are binary (0 or 1) because a patient can't be "half assigned" to someone.
    x = {}
    for pid in patient_ids:
        x[pid] = {}
        for prov_id in provider_ids:
            x[pid][prov_id] = pulp.LpVariable(
                f"assign_{pid}_{prov_id}",
                cat="Binary",  # 0 or 1 only
            )

    # --- Objective Function ---
    # Maximize total match quality across all assignments.
    # This is the sum of preference scores for the assignments we actually make.
    model += pulp.lpSum(
        preference_scores[pid][prov_id] * x[pid][prov_id]
        for pid in patient_ids
        for prov_id in provider_ids
    ), "TotalMatchQuality"

    # --- Constraint 1: Each patient assigned to exactly one provider ---
    # No patient left unassigned, no patient assigned to multiple providers.
    for pid in patient_ids:
        model += (
            pulp.lpSum(x[pid][prov_id] for prov_id in provider_ids) == 1,
            f"one_provider_{pid}",
        )

    # --- Constraint 2: Panel capacity limits ---
    # The number of new patients assigned to each provider, weighted by visit
    # frequency, must not push them over their panel maximum.
    # We use frequency weights because a biweekly patient consumes far more
    # appointment capacity than an annual wellness visit patient.
    patient_freq_map = {p["patient_id"]: p["visit_frequency"] for p in patients}

    for prov_id, prov_data in providers.items():
        remaining_capacity = prov_data["panel_max"] - prov_data["panel_current"]

        # Weighted new patient load for this provider
        # Each assigned patient contributes their frequency weight to the load.
        weighted_load = pulp.lpSum(
            FREQUENCY_WEIGHTS.get(patient_freq_map[pid], 1) * x[pid][prov_id]
            for pid in patient_ids
        )

        # The weighted load of new assignments must fit within remaining capacity.
        # Each patient's frequency weight is measured in annual visits.
        # Remaining capacity is in patient slots. We multiply remaining capacity
        # by the average frequency weight to convert both sides to the same unit
        # (weighted visit equivalents).
        avg_freq_weight = sum(FREQUENCY_WEIGHTS.values()) / len(FREQUENCY_WEIGHTS)
        model += (
            weighted_load <= remaining_capacity * avg_freq_weight,
            f"capacity_{prov_id}",
        )

    # --- Constraint 3: Not accepting new patients ---
    # If a provider has closed their panel, they get zero new assignments.
    for prov_id, prov_data in providers.items():
        if not prov_data["accepting_new"]:
            for pid in patient_ids:
                model += (
                    x[pid][prov_id] == 0,
                    f"closed_panel_{prov_id}_{pid}",
                )

    # --- Solve ---
    solver = pulp.PULP_CBC_CMD(msg=0)  # suppress solver output
    model.solve(solver)

    # --- Extract Results ---
    if model.status != pulp.constants.LpStatusOptimal:
        raise RuntimeError(
            f"Solver did not find optimal solution. Status: {pulp.LpStatus[model.status]}"
        )

    # Build the assignment map
    assignments = {}
    for pid in patient_ids:
        for prov_id in provider_ids:
            if pulp.value(x[pid][prov_id]) > 0.5:  # binary, so > 0.5 means assigned
                assignments[pid] = prov_id
                break

    return {
        "assignments": assignments,
        "objective_value": round(pulp.value(model.objective), 2),
        "solver_status": pulp.LpStatus[model.status],
    }

Step 3: Interpret and Validate Results

The pseudocode calls this validate_assignments(solution, patients, providers). After the solver runs, we need to verify the solution makes clinical sense and produce human-readable output for the panel management team.

def interpret_assignments(
    assignments: dict,
    patients: list,
    providers: dict,
    preference_scores: dict,
) -> list:
    """
    Convert raw solver output into human-readable assignment records
    with explanations of why each match was made.

    The panel management team needs to review these before they go live.
    They want to see: who got assigned where, why, and whether any assignments
    look questionable.

    Returns:
        List of assignment records with patient info, provider info, score,
        and a plain-English rationale.
    """
    patient_map = {p["patient_id"]: p for p in patients}
    results = []

    for pid, prov_id in assignments.items():
        patient = patient_map[pid]
        provider = providers[prov_id]
        score = preference_scores[pid][prov_id]

        # Build a rationale explaining the key factors in this assignment
        rationale = []

        if patient["language_preference"] in provider["languages"]:
            rationale.append(
                f"Language match: provider speaks {patient['language_preference']}"
            )

        if patient["gender_preference"] and patient["gender_preference"] == provider["gender"]:
            rationale.append("Gender preference matched")

        remaining = provider["panel_target"] - provider["panel_current"]
        if remaining > 0:
            rationale.append(f"Provider has capacity ({remaining} below target)")

        if patient["previous_provider"] and patient["previous_provider"] in CONTINUITY_MAP:
            if prov_id in CONTINUITY_MAP[patient["previous_provider"]]:
                rationale.append("Care team continuity with previous provider")

        if patient["complexity"] == "high" and provider["specialty"] == "internal_medicine":
            rationale.append("Complex patient matched to internist")

        results.append({
            "patient_id": pid,
            "patient_age": patient["age"],
            "patient_complexity": patient["complexity"],
            "assigned_provider": prov_id,
            "provider_name": provider["name"],
            "match_score": round(score, 1),
            "rationale": rationale if rationale else ["Best available match given constraints"],
        })

    return results

def validate_assignments(assignments: dict, patients: list, providers: dict) -> dict:
    """
    Run validation checks on the proposed assignments.

    Checks for:
    - All patients assigned
    - No provider over capacity
    - No assignments to closed panels
    - Distribution fairness across providers

    Returns:
        Validation report with pass/fail status and any warnings.
    """
    patient_map = {p["patient_id"]: p for p in patients}
    warnings = []
    errors = []

    # Check 1: All patients assigned
    unassigned = [p["patient_id"] for p in patients if p["patient_id"] not in assignments]
    if unassigned:
        errors.append(f"Unassigned patients: {unassigned}")

    # Check 2: No assignments to closed panels
    for pid, prov_id in assignments.items():
        if not providers[prov_id]["accepting_new"]:
            errors.append(f"{pid} assigned to {prov_id} which is not accepting new patients")

    # Check 3: Post-assignment panel sizes
    new_counts = {}
    for prov_id in providers:
        new_counts[prov_id] = 0
    for pid, prov_id in assignments.items():
        new_counts[prov_id] += 1

    for prov_id, count in new_counts.items():
        new_total = providers[prov_id]["panel_current"] + count
        if new_total > providers[prov_id]["panel_max"]:
            errors.append(
                f"{prov_id} would exceed panel max: {new_total} > {providers[prov_id]['panel_max']}"
            )
        elif new_total > providers[prov_id]["panel_target"]:
            warnings.append(
                f"{prov_id} would exceed target: {new_total} > {providers[prov_id]['panel_target']}"
            )

    # Check 4: Distribution fairness
    # Flag if any single provider gets more than 60% of the new patients
    total_assigned = len(assignments)
    for prov_id, count in new_counts.items():
        if total_assigned > 0 and count / total_assigned > 0.6:
            warnings.append(
                f"{prov_id} receiving {count}/{total_assigned} "
                f"({count/total_assigned*100:.0f}%) of new assignments"
            )

    return {
        "valid": len(errors) == 0,
        "errors": errors,
        "warnings": warnings,
        "distribution": new_counts,
    }

Step 4: Store Results

The pseudocode calls this store_assignments(results). It writes the proposed assignments to DynamoDB for the panel management team to review and approve.

import datetime
from datetime import timezone
import boto3
from botocore.config import Config

BOTO3_RETRY_CONFIG = Config(retries={"max_attempts": 3, "mode": "adaptive"})
dynamodb = boto3.resource("dynamodb", config=BOTO3_RETRY_CONFIG)

TABLE_NAME = "patient-provider-assignments"

def store_assignments(
    assignment_records: list,
    validation: dict,
    objective_value: float,
) -> dict:
    """
    Write proposed assignments to DynamoDB for human review.

    Each assignment gets its own record with status='proposed'. The panel
    management team reviews these in a dashboard and either approves (triggering
    the EHR update) or overrides with a manual assignment.

    Args:
        assignment_records: List of interpreted assignment records
        validation: Validation report
        objective_value: Solver's objective function value

    Returns:
        Metadata about the stored batch.
    """
    table = dynamodb.Table(TABLE_NAME)
    timestamp = datetime.datetime.now(timezone.utc).isoformat()
    batch_id = f"BATCH-{datetime.datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')}"

    with table.batch_writer() as batch:
        for record in assignment_records:
            item = {
                "pk": record["patient_id"],
                "sk": f"ASSIGNMENT#{batch_id}",
                "batch_id": batch_id,
                "patient_id": record["patient_id"],
                "assigned_provider": record["assigned_provider"],
                "provider_name": record["provider_name"],
                "match_score": Decimal(str(record["match_score"])),
                "rationale": record["rationale"],
                "patient_complexity": record["patient_complexity"],
                "status": "proposed",  # awaiting human review
                "created_at": timestamp,
                "objective_value": Decimal(str(objective_value)),
                "validation_passed": validation["valid"],
            }
            batch.put_item(Item=item)

    return {
        "batch_id": batch_id,
        "records_written": len(assignment_records),
        "timestamp": timestamp,
    }

Full Pipeline

Here's the full pipeline assembled into a single callable function. This is what your Step Functions workflow or scheduled Lambda would invoke when new patients need assignment.

def run_assignment_pipeline() -> dict:
    """
    Run the complete patient-provider assignment pipeline.

    1. Compute preference scores for all patient-provider pairs
    2. Solve the optimization model
    3. Interpret and validate results
    4. Store proposed assignments for human review

    Returns:
        Full results including assignments, validation, and storage metadata.
    """

    print("=== Patient-Provider Assignment Optimization ===\n")

    # Step 1: Build the preference matrix
    print("Step 1: Computing preference scores...")
    scores = build_preference_matrix(PATIENTS_TO_ASSIGN, PROVIDERS)

    # Show a sample of the score matrix
    print(f"  Score matrix: {len(PATIENTS_TO_ASSIGN)} patients x {len(PROVIDERS)} providers")
    sample_pid = PATIENTS_TO_ASSIGN[0]["patient_id"]
    print(f"  Sample scores for {sample_pid}:")
    for prov_id, score in scores[sample_pid].items():
        print(f"    {prov_id}: {score:.1f}")

    # Step 2: Solve the assignment problem
    print("\nStep 2: Solving assignment optimization...")
    solution = solve_assignment(PATIENTS_TO_ASSIGN, PROVIDERS, scores)
    print(f"  Solver status: {solution['solver_status']}")
    print(f"  Objective value: {solution['objective_value']}")
    print(f"  Assignments: {solution['assignments']}")

    # Step 3: Interpret and validate
    print("\nStep 3: Interpreting and validating assignments...")
    records = interpret_assignments(
        solution["assignments"], PATIENTS_TO_ASSIGN, PROVIDERS, scores
    )
    validation = validate_assignments(solution["assignments"], PATIENTS_TO_ASSIGN, PROVIDERS)

    print(f"  Validation passed: {validation['valid']}")
    if validation["warnings"]:
        for w in validation["warnings"]:
            print(f"  WARNING: {w}")
    if validation["errors"]:
        for e in validation["errors"]:
            print(f"  ERROR: {e}")

    print(f"\n  Distribution across providers:")
    for prov_id, count in validation["distribution"].items():
        print(f"    {prov_id}: {count} new patients")

    print("\n  Assignment details:")
    for rec in records:
        print(f"    {rec['patient_id']} -> {rec['provider_name']} "
              f"(score: {rec['match_score']}, rationale: {'; '.join(rec['rationale'])})")

    # Step 4: Store results
    print("\nStep 4: Storing proposed assignments...")
    try:
        storage = store_assignments(records, validation, solution["objective_value"])
        print(f"  Batch ID: {storage['batch_id']}")
        print(f"  Records written: {storage['records_written']}")
    except Exception as e:
        print(f"  Skipping DynamoDB store (table may not exist): {e}")
        print("  In production, this writes to DynamoDB for the review workflow.")
        storage = {"batch_id": "LOCAL-RUN", "records_written": 0}

    print("\nDone. Assignments ready for panel management team review.")

    return {
        "assignments": records,
        "validation": validation,
        "objective_value": solution["objective_value"],
        "storage": storage,
    }

# Run the pipeline
if __name__ == "__main__":
    result = run_assignment_pipeline()
    print("\n\nFull output:")
    print(json.dumps(result, indent=2, default=str))

The Gap Between This and Production

This example works. Run it and it will produce optimal patient-provider assignments with validation and rationale. But there's meaningful distance between "works in a script" and "runs weekly managing panels for a 50-provider practice." Here's where that gap lives:

Real data integration. This example uses hardcoded patient and provider data. A production system pulls from your EHR's panel management tables (Epic's Empanelment module, Cerner's Panel Management, or your custom attribution logic). That extraction involves complex SQL against scheduling, encounter, and demographics tables. You also need real-time panel counts, not stale snapshots.

Scale considerations. Seven patients and four providers is trivial for CBC. A real batch might be 500 patients (from a departing provider's panel) assigned across 30 providers. The problem is still tractable for CBC at that scale (binary integer programs with ~15,000 variables solve in seconds), but you should test with realistic sizes. If you hit thousands of patients and hundreds of providers, consider decomposition strategies or the HiGHS solver.

Preference model sophistication. The scoring function here uses simple rules. A production system might incorporate: historical visit patterns (does this patient prefer morning appointments? does this provider have morning availability?), geographic proximity (drive time from patient home to provider's clinic location), insurance network matching (is this provider in-network for this patient's plan?), and clinical program enrollment (is the patient in a diabetes management program that requires a specific provider type?).

Incremental vs. batch assignment. This example runs as a batch: "here are 7 patients, assign them all at once." In practice, you need both batch (provider departure, panel rebalancing) and incremental (single new patient calls to schedule, assign them a PCP on the spot). The incremental case is simpler (one patient, pick the best provider) but must respect the same constraints.

Human override workflow. The panel management team will override some assignments. Your system needs to handle: approvals, rejections with reason codes, manual reassignments, and partial approvals (approve 45 of 50, override 5). The DynamoDB records need status transitions: proposed, approved, rejected, overridden. Step Functions orchestrates this with a human approval task.

EHR integration. After approval, the assignment must be written back to the EHR. This typically means an HL7 FHIR CareTeam resource update or a proprietary API call (Epic's SetPrimaryProvider, for example). These integrations are fragile, rate-limited, and require careful error handling. A failed EHR write should not leave your assignment table and the EHR out of sync.

Error handling and retries. The DynamoDB writes have no retry logic for throttling, no conditional writes to prevent duplicate batches, and no dead-letter queue for failed records. Production code wraps every external call in try/except with exponential backoff and alerts on repeated failures.

IAM least-privilege. The IAM role for this pipeline should have exactly: dynamodb:PutItem and dynamodb:BatchWriteItem on the assignments table, dynamodb:GetItem for reading current panel data, and s3:GetObject if you're reading patient/provider data from S3. Not dynamodb:*.

VPC and encryption. Patient demographics and provider panel data are PHI. In production, Lambda or SageMaker Processing jobs run in a VPC with no internet access, using VPC endpoints for DynamoDB and S3. All data at rest is encrypted with KMS customer-managed keys. The assignment rationale text (which mentions patient conditions) is also PHI.

Fairness monitoring. After running assignments for several months, audit the results for unintended bias. Are patients of certain demographics systematically assigned to less experienced providers? Are language-concordant assignments actually happening at the rates you'd expect? Build dashboards that track assignment patterns by patient demographics and flag statistical anomalies.

Testing. There are no tests here. A production pipeline has unit tests for the scoring function (does a language match actually produce the expected bonus?), constraint tests (does a closed panel actually block assignments?), integration tests for the solver (does it find feasible solutions for edge cases like "more patients than total capacity"?), and regression tests that verify known-good assignments still score optimally after code changes.


Part of the Healthcare AI/ML Cookbook. See Recipe 14.2 for the full architectural walkthrough, pseudocode, and honest take on where this gets hard.