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