Skip to notebook content 🏠 Home ← Previous Multi Disease Dashboard Next →

Colorado Multi-Disease Surveillance Dashboard¶

One Health ecological forecasting — all vectors, all diseases, one view

This notebook provides a unified dashboard across all vector-borne and zoonotic diseases tracked in the Colorado surveillance platform. It uses the aedesproject_uif.surveillance module to compute probabilistic risk scores for each disease and render a consolidated summary with comparative visuals.

Diseases covered: West Nile Virus, Lyme, RMSF, Colorado Tick Fever, Anaplasmosis, Babesiosis, Tick-borne Relapsing Fever, Tularemia, Plague, Hantavirus

Vectors: Culex mosquitoes, Ixodes ticks, Dermacentor ticks, rodents


# Imports — unified surveillance module + viz stack
import sys
from pathlib import Path

try:
    from aedesproject_uif.surveillance import (
        DiseaseVectorRegistry, DiseaseType, VectorType,
        SurveillanceDataLoader, EcologicalFeatureEngine,
        ProbabilisticRiskScorer, MultiLayerValidator
    )
except ImportError:
    sys.path.insert(0, str(Path.cwd().parent / "src"))
    from aedesproject_uif.surveillance import (
        DiseaseVectorRegistry, DiseaseType, VectorType,
        SurveillanceDataLoader, EcologicalFeatureEngine,
        ProbabilisticRiskScorer, MultiLayerValidator
    )

import datetime
import warnings
import numpy as np
import pandas as pd
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import matplotlib.colors as mcolors

warnings.filterwarnings("ignore")

TODAY      = datetime.date.today()
MONTH_NOW  = TODAY.month
YEAR_NOW   = TODAY.year

PROJECT_ROOT = Path.cwd()
if not (PROJECT_ROOT / "data").exists() and (PROJECT_ROOT.parent / "data").exists():
    PROJECT_ROOT = PROJECT_ROOT.parent
DATA_DIR = PROJECT_ROOT / "data" / "surveillance"

registry  = DiseaseVectorRegistry()
loader    = SurveillanceDataLoader(data_dir=DATA_DIR)
scorer    = ProbabilisticRiskScorer()
validator = MultiLayerValidator()

print(f"✓ Multi-disease dashboard — {TODAY}")
print(f"  Registered diseases : {len(DiseaseVectorRegistry.list_diseases())}")
print(f"  Registered vectors  : {len(DiseaseVectorRegistry.list_vectors())}")
print(f"  Data directory      : {DATA_DIR}")
✓ Multi-disease dashboard — 2026-10-01
  Registered diseases : 11
  Registered vectors  : 4
  Data directory      : /home/runner/work/aedesproject-uif/aedesproject-uif/data/surveillance
# Load shared climate data (used for all vector assessments)
print("Loading shared climate data...")

try:
    climate_df = loader.load_noaa_climate_data("colorado", days_back=90)
    print(f"  ✓ Climate: {len(climate_df)} records")
    if "date" in climate_df.columns:
        climate_df["date"] = pd.to_datetime(climate_df["date"], errors="coerce")
        climate_df = (
            climate_df.dropna(subset=["date", "temp_c"])
            .query("temp_c > -80")
            .sort_values("date")
            .reset_index(drop=True)
        )
        climate_df = climate_df.set_index("date")
    have_climate = len(climate_df) > 0
except Exception as e:
    print(f"  ✗ Climate unavailable: {e}")
    climate_df = pd.DataFrame()
    have_climate = False

# Per-vector feature engines
mosquito_engine = EcologicalFeatureEngine(VectorType.MOSQUITO)
tick_engine     = EcologicalFeatureEngine(VectorType.TICK)

# Compute habitat suitability for each vector type
mosquito_habitat = pd.Series([0.3])  # fallback
tick_habitat     = pd.Series([0.3])

if have_climate and "temp_c" in climate_df.columns:
    clim_full = climate_df.copy()
    if "humidity_percent" not in clim_full.columns:
        clim_full["humidity_percent"] = 55.0

    try:
        mosquito_habitat = mosquito_engine.compute_combined_habitat_suitability(
            clim_full[["temp_c", "humidity_percent"]])
        tick_habitat = tick_engine.compute_combined_habitat_suitability(
            clim_full[["temp_c", "humidity_percent"]])
        print(f"  ✓ Mosquito habitat suitability (7d mean): {mosquito_habitat.dropna().tail(7).mean():.2f}")
        print(f"  ✓ Tick habitat suitability (7d mean)    : {tick_habitat.dropna().tail(7).mean():.2f}")
    except Exception as e:
        print(f"  Feature engineering note: {e}")
Loading shared climate data...
  ✓ Climate: 88 records
  ✓ Mosquito habitat suitability (7d mean): 0.36
  ✓ Tick habitat suitability (7d mean)    : 0.51
# Compute probabilistic risk scores for all registered diseases
print("Computing risk scores for all diseases...\n")

# Disease → (vector_type, case_ytd_key)
DISEASE_CONFIG = [
    (DiseaseType.WEST_NILE_VIRUS,              VectorType.MOSQUITO, "wnv"),
    (DiseaseType.LYME_DISEASE,                 VectorType.TICK,     "lyme"),
    (DiseaseType.ROCKY_MOUNTAIN_SPOTTED_FEVER, VectorType.TICK,     "rmsf"),
    (DiseaseType.COLORADO_TICK_FEVER,          VectorType.TICK,     "ctf"),
    (DiseaseType.ANAPLASMOSIS,                 VectorType.TICK,     "anaplasmosis"),
    (DiseaseType.BABESIOSIS,                   VectorType.TICK,     "babesiosis"),
    (DiseaseType.TICK_BORNE_RELAPSING_FEVER,   VectorType.TICK,     "tbrf"),
    (DiseaseType.TULAREMIA,                    VectorType.TICK,     "tularemia"),
    (DiseaseType.PLAGUE,                       VectorType.RODENT,   "plague"),
    (DiseaseType.HANTAVIRUS,                   VectorType.RODENT,   "hantavirus"),
]

# Seasonal activity lookup by month for each vector
def seasonal_weight(vector_type: VectorType, month: int) -> float:
    eco = registry.get_vector_ecology(vector_type)
    s, e = eco.activity_season
    if s <= e:
        in_season = s <= month <= e
    else:
        in_season = month >= s or month <= e
    # Peak vs shoulder
    mid = (s + e) // 2
    dist = abs(month - mid)
    if not in_season:
        return 0.1
    return max(0.3, 1.0 - dist * 0.12)

# Vector habitat lookup
HABITAT = {
    VectorType.MOSQUITO: mosquito_habitat,
    VectorType.TICK:     tick_habitat,
    VectorType.RODENT:   pd.Series([0.5]),   # no ecological model yet
    VectorType.BIRD:     pd.Series([0.4]),
}

rows = []
for disease_type, vector_type, case_key in DISEASE_CONFIG:
    info = registry.get_disease_characteristics(disease_type)
    eco  = registry.get_vector_ecology(vector_type)

    # Load YTD cases (quiet fail)
    try:
        cases_df = loader.load_cdc_arbonet_cases(
            case_key, "colorado", year_start=YEAR_NOW, year_end=YEAR_NOW)
        ytd_cases = len(cases_df)
    except Exception:
        ytd_cases = 0

    # Build single-element Series for scoring
    habitat = HABITAT[vector_type]
    sw      = seasonal_weight(vector_type, MONTH_NOW)
    idx     = pd.RangeIndex(1)

    vec_p   = pd.Series(float(np.clip(habitat.dropna().tail(7).mean() * sw, 0.05, 0.95)), index=idx)
    trans_p = pd.Series(float(np.clip(sw * 0.5, 0.05, 0.95)), index=idx)
    expo_p  = pd.Series(0.30, index=idx)
    out_p   = pd.Series(float(min(ytd_cases / 5.0, 1.0)), index=idx)

    risk_s, lo_s, hi_s = scorer.compute_integrated_risk_score(vec_p, trans_p, expo_p, out_p)
    risk_val  = float(risk_s.iloc[0])
    low_ci    = float(lo_s.iloc[0])
    high_ci   = float(hi_s.iloc[0])
    risk_cat  = str(scorer.categorize_risk(risk_s).iloc[0])

    rows.append({
        "disease":        disease_type.name.replace("_", " ").title(),
        "disease_type":   disease_type,
        "vector":         vector_type.name.title(),
        "ytd_cases":      ytd_cases,
        "risk_pct":       round(risk_val * 100, 1),
        "low_ci_pct":     round(low_ci * 100, 1),
        "high_ci_pct":    round(high_ci * 100, 1),
        "risk_category":  risk_cat,
        "cfr":            info.case_fatality_rate,
        "colorado_endemic": info.colorado_endemic,
    })

risk_df = pd.DataFrame(rows).sort_values("risk_pct", ascending=False).reset_index(drop=True)

print(f"Risk scores computed for {len(risk_df)} diseases")
print()
print(risk_df[["disease", "vector", "ytd_cases", "risk_pct", "risk_category", "cfr"]].to_string(index=False))
Computing risk scores for all diseases...

Risk scores computed for 10 diseases

                     disease   vector  ytd_cases  risk_pct risk_category   cfr
                Lyme Disease     Tick          0      25.1           LOW 0.000
Rocky Mountain Spotted Fever     Tick          0      25.1           LOW 0.010
  Tick Borne Relapsing Fever     Tick          0      25.1           LOW 0.005
         Colorado Tick Fever     Tick          0      25.1           LOW 0.001
                Anaplasmosis     Tick          0      25.1           LOW 0.005
                  Babesiosis     Tick          0      25.1           LOW 0.005
                   Tularemia     Tick          0      25.1           LOW 0.005
             West Nile Virus Mosquito          0      23.2           LOW 0.003
                      Plague   Rodent          0      22.0           LOW 0.100
                  Hantavirus   Rodent          0      22.0           LOW 0.380
if 'risk_df' not in dir() or risk_df is None or (hasattr(risk_df, '__len__') and len(risk_df) == 0):
    print('DEBUG: risk_df not computed (check cell 4) — skipping section')
else:
    # Risk comparison chart — all diseases, sorted by risk probability

    RISK_COLORS = {"LOW": "#48bb78", "MODERATE": "#ed8936", "HIGH": "#e53e3e", "nan": "#a0aec0"}

    fig, axes = plt.subplots(1, 2, figsize=(15, 6), gridspec_kw={"width_ratios": [2, 1]})
    fig.suptitle(
        f"Colorado Vector-Borne Disease Risk Dashboard — {TODAY.strftime('%B %Y')}",
        fontsize=14, fontweight="bold"
    )

    # ── Panel 1: Horizontal bar chart — risk probability with CI ─────────────────
    ax = axes[0]
    diseases = risk_df["disease"].tolist()
    y_pos    = range(len(diseases))
    colors   = [RISK_COLORS.get(r, "#a0aec0") for r in risk_df["risk_category"]]

    bars = ax.barh(y_pos, risk_df["risk_pct"], color=colors, alpha=0.85, height=0.6)

    # CI error bars
    xerr_lo = risk_df["risk_pct"] - risk_df["low_ci_pct"]
    xerr_hi = risk_df["high_ci_pct"] - risk_df["risk_pct"]
    ax.errorbar(
        risk_df["risk_pct"], y_pos,
        xerr=[xerr_lo, xerr_hi],
        fmt="none", color="#2d3748", linewidth=1.2, capsize=4
    )

    # Annotations: YTD cases
    for i, row in risk_df.iterrows():
        ax.text(
            row["risk_pct"] + 1.5, list(y_pos)[i],
            f"{row['ytd_cases']} cases YTD" if row["ytd_cases"] else "",
            va="center", fontsize=8, color="#4a5568"
        )

    ax.set_yticks(list(y_pos))
    ax.set_yticklabels(diseases, fontsize=9)
    ax.set_xlabel("Integrated Risk Probability (%)", fontsize=10)
    ax.set_title("Risk Probability with 95% Confidence Intervals", fontsize=11)
    ax.axvline(30, color="#48bb78", linestyle="--", linewidth=0.8, alpha=0.6, label="LOW threshold (30%)")
    ax.axvline(70, color="#e53e3e", linestyle="--", linewidth=0.8, alpha=0.6, label="HIGH threshold (70%)")
    ax.set_xlim(0, 105)
    ax.grid(axis="x", alpha=0.25)
    ax.legend(fontsize=8, loc="lower right")

    # Legend patches
    legend_patches = [
        mpatches.Patch(color=RISK_COLORS["LOW"],      label="LOW"),
        mpatches.Patch(color=RISK_COLORS["MODERATE"], label="MODERATE"),
        mpatches.Patch(color=RISK_COLORS["HIGH"],     label="HIGH"),
    ]
    ax.legend(handles=legend_patches, fontsize=9, loc="lower right")

    # ── Panel 2: Risk × CFR bubble chart ─────────────────────────────────────────
    ax2 = axes[1]
    cfr_pct   = risk_df["cfr"] * 100
    risk_vals = risk_df["risk_pct"]
    sizes     = np.clip(risk_df["ytd_cases"] * 40 + 60, 60, 400)
    pt_colors = [RISK_COLORS.get(r, "#a0aec0") for r in risk_df["risk_category"]]

    scatter = ax2.scatter(risk_vals, cfr_pct, s=sizes, c=pt_colors, alpha=0.8, edgecolors="#2d3748", linewidths=0.5)

    for _, row in risk_df.iterrows():
        ax2.annotate(
            row["disease"].split()[0],   # first word only to keep labels compact
            (row["risk_pct"], row["cfr"] * 100),
            fontsize=7, ha="left", va="bottom",
            xytext=(3, 3), textcoords="offset points"
        )

    ax2.set_xlabel("Integrated Risk Probability (%)", fontsize=10)
    ax2.set_ylabel("Case Fatality Rate (%)", fontsize=10)
    ax2.set_title("Risk vs. Severity\n(bubble size = YTD cases)", fontsize=11)
    ax2.grid(alpha=0.3)
    ax2.set_xscale("linear")

    plt.tight_layout()
        # Export data table alongside chart
    risk_df.to_csv('multi_disease_risk_dashboard.csv')
    print(f'  → multi_disease_risk_dashboard.csv ({len(risk_df)} rows)')
    plt.savefig("multi_disease_risk_dashboard.png", dpi=150, bbox_inches="tight")
    plt.show()
    print("Dashboard saved: multi_disease_risk_dashboard.png")
  → multi_disease_risk_dashboard.csv (10 rows)
Dashboard saved: multi_disease_risk_dashboard.png
# Habitat suitability timeline — mosquito vs tick (90-day window)

if not have_climate:
    print("Skipping habitat suitability chart (climate data unavailable)")
else:
    clim = climate_df.copy()
    if "humidity_percent" not in clim.columns:
        clim["humidity_percent"] = 55.0

    try:
        mosq_suit = mosquito_engine.compute_combined_habitat_suitability(
            clim[["temp_c", "humidity_percent"]])
        tick_suit = tick_engine.compute_combined_habitat_suitability(
            clim[["temp_c", "humidity_percent"]])
        mosq_gdd  = mosquito_engine.compute_growing_degree_days(clim["temp_c"])
        tick_gdd  = tick_engine.compute_growing_degree_days(clim["temp_c"], base_temp=4.0)

        fig, axes = plt.subplots(3, 1, figsize=(14, 9), sharex=True)
        fig.suptitle(
            "Vector Habitat Suitability & Phenology — Colorado (Last 90 Days)",
            fontsize=13, fontweight="bold"
        )

        # Panel 1: Temperature with thresholds
        axes[0].plot(clim.index, clim["temp_c"], color="#dd6b20", lw=1.4, label="Daily temp (°C)")
        axes[0].axhline(10, color="#805ad5", ls=":", lw=1, alpha=0.8, label="Tick threshold (10°C)")
        axes[0].axhline(18, color="#e53e3e", ls="--", lw=1, alpha=0.8, label="WNV threshold (18°C)")
        axes[0].fill_between(clim.index, clim["temp_c"], 18,
                             where=clim["temp_c"] > 18, alpha=0.12, color="#e53e3e")
        axes[0].set_ylabel("°C", fontsize=10)
        axes[0].legend(fontsize=8)
        axes[0].grid(alpha=0.25)

        # Panel 2: Habitat suitability by vector type
        axes[1].plot(clim.index, mosq_suit.values, color="#3182ce", lw=1.6,
                     label="Mosquito (Cx. tarsalis)")
        axes[1].plot(clim.index, tick_suit.values, color="#744210", lw=1.6,
                     label="Tick (I. scapularis)")
        axes[1].axhline(0.7, color="#e53e3e", ls="--", lw=0.9, alpha=0.6, label="High suitability")
        axes[1].set_ylabel("Habitat Suitability (0–1)", fontsize=10)
        axes[1].set_ylim(0, 1.05)
        axes[1].legend(fontsize=8)
        axes[1].grid(alpha=0.25)

        # Panel 3: Growing degree-days
        ax3 = axes[2]
        ax3.plot(clim.index, mosq_gdd.values, color="#3182ce", lw=1.4, label="Mosquito GDD (base 10°C)")
        ax3_r = ax3.twinx()
        ax3_r.plot(clim.index, tick_gdd.values, color="#744210", lw=1.4, ls="--", label="Tick GDD (base 4°C)")
        ax3.set_ylabel("Mosquito GDD", fontsize=9, color="#3182ce")
        ax3_r.set_ylabel("Tick GDD", fontsize=9, color="#744210")
        ax3.tick_params(axis="y", labelcolor="#3182ce")
        ax3_r.tick_params(axis="y", labelcolor="#744210")
        lines1, labels1 = ax3.get_legend_handles_labels()
        lines2, labels2 = ax3_r.get_legend_handles_labels()
        ax3.legend(lines1 + lines2, labels1 + labels2, fontsize=8)
        ax3.grid(alpha=0.25)
        ax3.set_xlabel("Date", fontsize=10)

        plt.tight_layout()

        # Export data table alongside chart
        try:
            hab_export = clim[["temp_c"]].copy()
            hab_export.index.name = "date"
            hab_export["mosquito_habitat"] = mosq_suit.values
            hab_export["tick_habitat"] = tick_suit.values
            hab_export["mosquito_gdd"] = mosq_gdd.values
            hab_export["tick_gdd"] = tick_gdd.values
            hab_export.to_csv("vector_habitat_suitability.csv")
            print("  → vector_habitat_suitability.csv")
        except Exception:
            pass

        plt.savefig("vector_habitat_suitability.png", dpi=150, bbox_inches="tight")
        plt.show()
        print("Chart saved: vector_habitat_suitability.png")

    except Exception as e:
        print(f"Habitat suitability chart note: {e}")
  → vector_habitat_suitability.csv
Chart saved: vector_habitat_suitability.png
if 'DISEASE_CONFIG' not in dir() or DISEASE_CONFIG is None or (hasattr(DISEASE_CONFIG, '__len__') and len(DISEASE_CONFIG) == 0):
    print('DEBUG: DISEASE_CONFIG not defined (check cell 4) — skipping section')
else:
    # Seasonal risk calendar — all diseases × 12 months
    # Computed from registry phenology + scorer, not manual lookup tables

    months = ["Jan","Feb","Mar","Apr","May","Jun",
              "Jul","Aug","Sep","Oct","Nov","Dec"]

    calendar_rows = []
    for disease_type, vector_type, _ in DISEASE_CONFIG:
        monthly_risks = []
        for m in range(1, 13):
            sw = seasonal_weight(vector_type, m)
            idx = pd.RangeIndex(1)
            vec_p   = pd.Series(sw * 0.8, index=idx)
            trans_p = pd.Series(sw * 0.5, index=idx)
            expo_p  = pd.Series(0.30, index=idx)
            out_p   = pd.Series(0.05, index=idx)    # no case signal for forecast
            risk_s, _, _ = scorer.compute_integrated_risk_score(vec_p, trans_p, expo_p, out_p)
            monthly_risks.append(float(risk_s.iloc[0]) * 100)
        calendar_rows.append(monthly_risks)

    disease_labels = [d.name.replace("_", " ").title() for d, _, _ in DISEASE_CONFIG]
    calendar_matrix = np.array(calendar_rows)

    fig, ax = plt.subplots(figsize=(14, 5))
    im = ax.imshow(calendar_matrix, aspect="auto", cmap="RdYlGn_r",
                   vmin=0, vmax=60, interpolation="nearest")

    ax.set_xticks(range(12))
    ax.set_xticklabels(months, fontsize=10)
    ax.set_yticks(range(len(disease_labels)))
    ax.set_yticklabels(disease_labels, fontsize=9)
    ax.set_title(
        "Colorado Vector-Borne Disease Seasonal Risk Calendar (Module-Computed)",
        fontsize=12, fontweight="bold", pad=12
    )

    # Annotate cells with risk %
    for i in range(len(disease_labels)):
        for j in range(12):
            val = calendar_matrix[i, j]
            ax.text(j, i, f"{val:.0f}%", ha="center", va="center",
                    fontsize=7, color="white" if val > 40 else "black",
                    fontweight="bold" if val > 40 else "normal")

    # Highlight current month
    ax.axvline(MONTH_NOW - 1, color="#2b6cb0", linewidth=2.5)
    ax.text(MONTH_NOW - 1, -0.7, "▲ Now", ha="center", fontsize=9, color="#2b6cb0")

    plt.colorbar(im, ax=ax, label="Risk Probability (%)", shrink=0.7)
    plt.tight_layout()
        # Export data table alongside chart
    cal_df = pd.DataFrame(calendar_rows, columns=months)
    cal_df.insert(0, 'disease', [d.value if hasattr(d,'value') else str(d) for d,_,_ in DISEASE_CONFIG[:len(calendar_rows)]])
    cal_df.to_csv('multi_disease_seasonal_calendar.csv', index=False)
    print(f'  → multi_disease_seasonal_calendar.csv ({len(cal_df)} rows)')
    plt.savefig("multi_disease_seasonal_calendar.png", dpi=150, bbox_inches="tight")
    plt.show()
    print("Seasonal calendar saved: multi_disease_seasonal_calendar.png")
  → multi_disease_seasonal_calendar.csv (10 rows)
Seasonal calendar saved: multi_disease_seasonal_calendar.png
# Multi-layer validation summary
print("Running validation checks...\n")

validation_notes = []

# Entomological validation: mosquito thermal suitability vs activity window
if have_climate and "temp_c" in climate_df.columns:
    try:
        thermal = mosquito_engine.compute_thermal_suitability(climate_df["temp_c"])
        dates_idx = pd.DatetimeIndex(climate_df.index)
        activity  = mosquito_engine.compute_activity_window(dates_idx)
        mask = thermal.notna() & activity.notna()
        result = validator.validate_entomological_correlation(thermal[mask], activity[mask])
        print("Mosquito entomological validation:")
        print(f"  Pearson r  = {result.get('pearson_correlation', 'N/A'):.3f}")
        print(f"  Spearman r = {result.get('spearman_correlation', 'N/A'):.3f}")
        validation_notes.append("Mosquito entomological: OK")
    except Exception as e:
        print(f"  Mosquito validation note: {e}")

    try:
        tick_thermal = tick_engine.compute_thermal_suitability(climate_df["temp_c"])
        dates_idx    = pd.DatetimeIndex(climate_df.index)
        tick_activity = tick_engine.compute_activity_window(dates_idx)
        mask = tick_thermal.notna() & tick_activity.notna()
        result = validator.validate_entomological_correlation(tick_thermal[mask], tick_activity[mask])
        print("\nTick entomological validation:")
        print(f"  Pearson r  = {result.get('pearson_correlation', 'N/A'):.3f}")
        print(f"  Spearman r = {result.get('spearman_correlation', 'N/A'):.3f}")
        validation_notes.append("Tick entomological: OK")
    except Exception as e:
        print(f"  Tick validation note: {e}")
else:
    print("Validation skipped: climate data unavailable")

print(f"\nValidation report: {len(validator.get_validation_report())} checks recorded")
print(f"\n✓ Multi-disease dashboard complete — {TODAY}")
print(f"  {len(risk_df)} diseases scored | {len([r for r in risk_df['risk_category'] if r == 'HIGH'])} HIGH | "
      f"{len([r for r in risk_df['risk_category'] if r == 'MODERATE'])} MODERATE | "
      f"{len([r for r in risk_df['risk_category'] if r == 'LOW'])} LOW")
Running validation checks...

Mosquito entomological validation:
  Mosquito validation note: Unknown format code 'f' for object of type 'str'

Tick entomological validation:
  Tick validation note: Unknown format code 'f' for object of type 'str'

Validation report: 1 checks recorded

✓ Multi-disease dashboard complete — 2026-10-01
  10 diseases scored | 0 HIGH | 0 MODERATE | 10 LOW