Skip to notebook content 🏠 Home ← Previous West Nile Virus Surveillance Next →

West Nile Virus & Vector-Borne Disease Surveillance in Colorado¶

A One Health ecological forecasting and public health surveillance notebook

This notebook is part of a broader platform for environmental vector-borne disease risk forecasting and ecological surveillance in Colorado and the western United States. It integrates environmental, climatic, ecological, entomological, and epidemiological data to estimate risk for diseases including West Nile Virus, Lyme disease, Colorado tick fever, Rocky Mountain spotted fever, tularemia, plague, hantavirus, mosquito-borne encephalitis, and emerging climate-shifted vector diseases.

Key Features:

  • CDC ArboNET integration (human cases, mosquito pool surveillance)
  • Environmental and climate anomaly data (NOAA, NASA, USGS)
  • Vector habitat suitability modeling (mosquito, tick, rodent, bird migration)
  • Probabilistic, uncertainty-aware risk estimation
  • Rolling historical backtesting and operational validation
  • One Health framing: human, animal, vector, climate, land use

This notebook replaces dengue-centric assumptions with a Colorado-focused, One Health ecological surveillance approach.


Colorado Vector-Borne Disease Situation Brief¶

AEDES | Advanced Early Disease Prediction and Exploration Service

This report is not only West Nile Virus. It combines:

  • CDC finalized annual WNV history (through 2024)
  • CDC provisional current-season weekly signals (2026 YTD WNV + Lyme)
  • NASA POWER daily climate conditions (temperature and precipitation)
  • iNaturalist citizen-science mosquito observations

Project Navigation

  • Home
  • Notebook Hub
  • Surveillance Dashboard

Quick Links to Report Sections¶

  • Current Situation (Today)
  • Summary of the Analysis
  • Annual Case Trends (Finalized, 2010-2024)
  • Recent Climate Conditions (90-Day Window)
  • iNaturalist Vector Observations
  • Early Warning Summary
  • Run History (daily/weekly snapshots)

Summary of the Analysis¶

This page leads with what matters now (current season signals and near-term risk), then provides historical context.

What to know about data recency:

  • Finalized annual CDC WNV counts currently end at 2024. This is expected due to reporting and verification lag.
  • Current-year activity is available as provisional weekly data (used in this notebook for 2026 YTD indicators).
  • Climate and iNaturalist data are current feeds and are used to interpret short-term vector pressure.

How to read this report:

  • Top sections answer: Is current risk low, moderate, or high?
  • Middle sections explain why (temperature, seasonality, vector observations).
  • Historical section is included for baseline comparison, not as the main message.
"""
Imports: Unified Surveillance Module

This cell imports all components from the aedesproject_uif.surveillance module,
which provides:
- Multi-disease/vector registry and ecology data
- Standardized data loading from CDC, NOAA, NASA, USGS, iNaturalist
- Ecological feature engineering (thermal, GDD, humidity, phenology)
- Probabilistic risk scoring with uncertainty quantification
- Multi-layer validation (ecological, entomological, epidemiological)
"""

import sys
from pathlib import Path

# Ensure surveillance module is available
try:
    from aedesproject_uif.surveillance import (
        DiseaseVectorRegistry, DiseaseType, VectorType,
        SurveillanceDataLoader, EcologicalFeatureEngine,
        ProbabilisticRiskScorer, MultiLayerValidator
    )
except ImportError:
    # Fallback: add src/ to path if running in notebooks directory
    sys.path.insert(0, str(Path.cwd().parent / "src"))
    from aedesproject_uif.surveillance import (
        DiseaseVectorRegistry, DiseaseType, VectorType,
        SurveillanceDataLoader, EcologicalFeatureEngine,
        ProbabilisticRiskScorer, MultiLayerValidator
    )

# Standard scientific/viz imports
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import plotly.graph_objects as go
import plotly.express as px
from plotly.subplots import make_subplots
import datetime as dt
import calendar
import json
import csv
from pathlib import Path

print("✓ Surveillance module imported successfully")
print(f"  - Registry: {len(DiseaseVectorRegistry.list_diseases())} diseases, {len(DiseaseVectorRegistry.list_vectors())} vectors")
print(f"  - Data loader, feature engine, risk scorer, validator ready")
✓ Surveillance module imported successfully
  - Registry: 11 diseases, 4 vectors
  - Data loader, feature engine, risk scorer, validator ready
# Setup: Initialize registry, data loader, and snapshot history tracking

TODAY = dt.date.today().isoformat()

# Resolve project root whether notebook runs from repo root or notebooks/.
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"
HISTORY_DIR = DATA_DIR / "history"
HISTORY_DIR.mkdir(parents=True, exist_ok=True)
SNAPSHOT_FILE = HISTORY_DIR / "notebook_snapshots.csv"

# Initialize registry and data loader
registry = DiseaseVectorRegistry()
loader = SurveillanceDataLoader(data_dir=DATA_DIR)

# Feature engineering and scoring for WNV
engine = EcologicalFeatureEngine(VectorType.MOSQUITO)
scorer = ProbabilisticRiskScorer()
validator = MultiLayerValidator()

print(f"✓ Initialized:")
print(f"  - Base data path: {DATA_DIR}")
print(f"  - WNV ecology: {registry.get_vector_ecology(VectorType.MOSQUITO).scientific_names}")
print(f"  - Current year: {dt.date.today().year}")

SNAPSHOT_COLUMNS = [
    "snapshot_date", "notebook_run_ts",
    "wnv_ytd_cases", "lyme_ytd_cases",
    "climate_mean_temp_14d", "climate_days_above_18", "climate_data_days",
    "inat_obs_count", "risk_level",
]

def save_snapshot(record: dict) -> None:
    """Append one row of key metrics to the rolling snapshot CSV."""
    record.setdefault("snapshot_date", TODAY)
    record.setdefault("notebook_run_ts", dt.datetime.now(dt.timezone.utc).isoformat(timespec="seconds"))
    row = {col: record.get(col, "") for col in SNAPSHOT_COLUMNS}
    write_header = not SNAPSHOT_FILE.exists()
    with open(SNAPSHOT_FILE, "a", newline="", encoding="utf-8") as fh:
        writer = csv.DictWriter(fh, fieldnames=SNAPSHOT_COLUMNS)
        if write_header:
            writer.writeheader()
        writer.writerow(row)
    print(f"Snapshot saved → {SNAPSHOT_FILE.relative_to(PROJECT_ROOT)}  ({TODAY})")

def load_snapshot_history() -> pd.DataFrame:
    """Return the full snapshot history as a tidy DataFrame."""
    if not SNAPSHOT_FILE.exists():
        return pd.DataFrame(columns=SNAPSHOT_COLUMNS)
    df = pd.read_csv(SNAPSHOT_FILE, parse_dates=["snapshot_date"])
    df = df.sort_values("notebook_run_ts").drop_duplicates("snapshot_date", keep="last")
    return df.sort_values("snapshot_date").reset_index(drop=True)
✓ Initialized:
  - Base data path: /home/runner/work/aedesproject-uif/aedesproject-uif/data/surveillance
  - WNV ecology: ['Culex tarsalis']
  - Current year: 2026

Current Situation (Today) #

This section is the operational briefing for current conditions and likely near-term risk (next 2-4 weeks).

# Current Conditions: Load data via surveillance module and compute risk

year_now = dt.date.today().year
month_now = dt.date.today().month
month_name = calendar.month_name[month_now]

print(f"Loading surveillance data for {month_name} {year_now}...\n")

# ── Load climate data (90-day window) ──────────────────────────────────────
try:
    climate_df = loader.load_noaa_climate_data("colorado", days_back=90)
    print(f"✓ Climate data: {len(climate_df)} records")
except Exception as e:
    print(f"✗ Climate data unavailable: {e}")
    climate_df = pd.DataFrame()

# ── Load WNV cases YTD ─────────────────────────────────────────────────────
try:
    wnv_cases_df = loader.load_cdc_arbonet_cases("wnv", "colorado", year_start=year_now, year_end=year_now)
    wnv_ytd_cases = len(wnv_cases_df) if len(wnv_cases_df) > 0 else 0
    print(f"✓ WNV cases YTD: {wnv_ytd_cases}")
except Exception as e:
    print(f"✗ WNV cases unavailable: {e}")
    wnv_ytd_cases = 0
    wnv_cases_df = pd.DataFrame()

# ── Load Lyme cases YTD ────────────────────────────────────────────────────
try:
    lyme_cases_df = loader.load_cdc_arbonet_cases("lyme_disease", "colorado", year_start=year_now, year_end=year_now)
    lyme_ytd_cases = len(lyme_cases_df) if len(lyme_cases_df) > 0 else 0
    print(f"✓ Lyme cases YTD: {lyme_ytd_cases}")
except Exception as e:
    print(f"✗ Lyme cases unavailable: {e}")
    lyme_ytd_cases = 0
    lyme_cases_df = pd.DataFrame()

# ── Load iNaturalist observations ──────────────────────────────────────────
try:
    inat_df = loader.load_inaturalist_vector_observations("mosquito", "colorado")
    inat_count = len(inat_df) if len(inat_df) > 0 else 0
    print(f"✓ iNaturalist mosquito observations: {inat_count}")
except Exception as e:
    print(f"✗ iNaturalist data unavailable: {e}")
    inat_df = pd.DataFrame()
    inat_count = 0

# ── Ecological features ────────────────────────────────────────────────────
print("\nComputing ecological features...")
recent_mean_temp_14d = None
days_above_18_14d = None
habitat_series = None

if len(climate_df) > 0 and 'temp_c' in climate_df.columns:
    temps = climate_df['temp_c']
    recent14 = temps.tail(14)
    if len(recent14):
        recent_mean_temp_14d = float(recent14.mean())
        days_above_18_14d = int((recent14 > 18).sum())
        print(f"  - 14-day mean temperature: {recent_mean_temp_14d:.1f}°C")
        print(f"  - Days above 18°C: {days_above_18_14d}")

    try:
        # Build a DatetimeIndex-indexed DataFrame for the module
        if 'date' in climate_df.columns:
            dt_index = pd.DatetimeIndex(pd.to_datetime(climate_df['date'], errors='coerce'))
            clim_indexed = climate_df.copy()
            clim_indexed.index = dt_index
        else:
            clim_indexed = climate_df.copy()

        # Add humidity column (neutral fallback if unavailable)
        if 'humidity_percent' not in clim_indexed.columns:
            clim_indexed['humidity_percent'] = 60.0

        habitat_series = engine.compute_combined_habitat_suitability(
            clim_indexed[['temp_c', 'humidity_percent']],
            weights={"thermal": 0.6, "humidity": 0.3, "season": 0.1}
        )
        thermal_suit = engine.compute_thermal_suitability(temps, window_days=14)
        print(f"  - Thermal suitability (last 7d mean): {thermal_suit.tail(7).mean():.2f}")
        print(f"  - Habitat suitability (last 7d mean): {habitat_series.tail(7).mean():.2f}")
    except Exception as e:
        print(f"  - Feature computation note: {e}")

# ── Probabilistic risk scoring ─────────────────────────────────────────────
print("\nScoring integrated risk...")
risk_label = "UNKNOWN"
risk_probability = 0.3
low_ci = 0.15
high_ci = 0.45

try:
    idx = climate_df.index if len(climate_df) > 0 else pd.RangeIndex(1)
    seasonal_weight = 0.9 if month_now in (7, 8) else (0.6 if month_now in (6, 9) else 0.3)

    vec_prob = habitat_series.fillna(seasonal_weight) if habitat_series is not None \
               else pd.Series(seasonal_weight, index=idx)

    temp_factor = float(np.clip(((recent_mean_temp_14d or 15) - 10) / 20, 0.05, 0.95))
    trans_prob   = pd.Series(temp_factor, index=idx)
    expo_prob    = pd.Series(0.3, index=idx)
    outbreak_prob = pd.Series(float(min(wnv_ytd_cases / 5.0, 1.0)), index=idx)

    risk_s, lower_s, upper_s = scorer.compute_integrated_risk_score(
        vec_prob, trans_prob, expo_prob, outbreak_prob
    )

    risk_probability = float(risk_s.iloc[-1])
    low_ci  = float(lower_s.iloc[-1])
    high_ci = float(upper_s.iloc[-1])
    risk_label = str(scorer.categorize_risk(risk_s).iloc[-1])
    print(f"  - Integrated risk: {risk_label} ({risk_probability:.1%})")
    print(f"    95% CI: {low_ci:.1%} — {high_ci:.1%}")
except Exception as e:
    print(f"  - Risk scoring note: {e}")

# ── Current briefing ───────────────────────────────────────────────────────
print("\n" + "="*70)
print(f"CURRENT SITUATION ({month_name} {year_now})")
print("="*70)
print(f"Mosquito-borne disease risk: {risk_label}  ({risk_probability:.1%})")
print(f"95% CI: {low_ci:.1%} – {high_ci:.1%}")
print()
if risk_label == "LOW":
    print("Interpretation: Immediate statewide risk appears low at this time.")
elif risk_label == "MODERATE":
    print("Interpretation: Conditions support some transmission risk.")
    print("What to expect: Localized activity may increase over 2–4 weeks.")
else:
    print("Interpretation: Conditions are favorable for elevated transmission risk.")
print()
print("Current evidence:")
print(f"  • WNV cases YTD : {wnv_ytd_cases}")
print(f"  • Lyme cases YTD: {lyme_ytd_cases}")
print(f"  • Mean temp 14d : {recent_mean_temp_14d:.1f}°C" if recent_mean_temp_14d else "  • Mean temp 14d : unavailable")
print(f"  • Days > 18°C   : {days_above_18_14d}" if days_above_18_14d is not None else "  • Days > 18°C   : unavailable")
print(f"  • iNat obs      : {inat_count}")
Loading surveillance data for October 2026...

✓ Climate data: 88 records
✓ WNV cases YTD: 0
✓ Lyme cases YTD: 0
✓ iNaturalist mosquito observations: 0

Computing ecological features...
  - 14-day mean temperature: 16.1°C
  - Days above 18°C: 2
  - Thermal suitability (last 7d mean): 0.33
  - Habitat suitability (last 7d mean): 0.36

Scoring integrated risk...
  - Integrated risk: LOW (24.2%)
    95% CI: 10.3% — 38.0%

======================================================================
CURRENT SITUATION (October 2026)
======================================================================
Mosquito-borne disease risk: LOW  (24.2%)
95% CI: 10.3% – 38.0%

Interpretation: Immediate statewide risk appears low at this time.

Current evidence:
  • WNV cases YTD : 0
  • Lyme cases YTD: 0
  • Mean temp 14d : 16.1°C
  • Days > 18°C   : 2
  • iNat obs      : 0

📊 Historical Case Trends Archive (Finalized, 2010–2024) # (click to expand)

ℹ️ CDC Data Lag: CDC's finalized annual case counts are published approximately 12–18 months after the end of each calendar year. The most recent finalized data is 2024. Current-season (2026) provisional data appears in the Current Situation section above.

Back to Home | Back to Top

# Load historical WNV data via surveillance module (2010-2024 finalized)

print("Loading historical WNV cases (2010-2024)...\n")

try:
    # Try to load from data loader (covers 2010-2024)
    df_wnv = loader.load_cdc_arbonet_cases("wnv", "colorado", year_start=2010, year_end=2024)
    
    if len(df_wnv) > 0:
        # Group by year for annual summary
        df_wnv['year'] = pd.to_datetime(df_wnv['date'], errors='coerce').dt.year
        annual = df_wnv.groupby('year').size().reset_index(name='cases')
        df_wnv = annual
        source_label = "CDC ArboNET (via surveillance module)"
    else:
        raise ValueError("No data returned from loader")
        
except Exception as e:
    print(f"Note: Using built-in reference data. ({e})")
    source_label = "CDC NNDSS (built-in reference)"
    
    # Built-in historical reference (CDC published annual neuroinvasive/severe cases)
    df_wnv = pd.DataFrame({
        'year': [2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024],
        'cases': [51, 20, 130, 14, 43, 72, 14, 5, 15, 10, 10, 8, 16, 6, 12],
    })

print(f"Source: {source_label}")
print(f"Records: {len(df_wnv)} years")
print(f"Total cases (all years): {df_wnv['cases'].sum()}")
print(f"Peak year: {df_wnv.loc[df_wnv['cases'].idxmax(), 'year']:.0f} ({df_wnv['cases'].max():.0f} cases)")
print()

display(df_wnv.tail(10))
Loading historical WNV cases (2010-2024)...

Source: CDC ArboNET (via surveillance module)
Records: 15 years
Total cases (all years): 15
Peak year: 2010 (1 cases)

year cases
5 2015 1
6 2016 1
7 2017 1
8 2018 1
9 2019 1
10 2020 1
11 2021 1
12 2022 1
13 2023 1
14 2024 1
if df_wnv.empty:
    print("No historical WNV data available — skipping chart")
else:
    year_col = 'year'
    case_col = 'cases'
    
    # Normalize column names from either loader or built-in data
    if 'neuroinvasive' in df_wnv.columns and 'cases' not in df_wnv.columns:
        df_wnv = df_wnv.rename(columns={'neuroinvasive': 'cases'})
    if 'deaths' not in df_wnv.columns:
        df_wnv['deaths'] = 0
    
    peak_year = df_wnv.loc[df_wnv[case_col].idxmax(), year_col]
    peak_count = df_wnv[case_col].max()

    fig, axes = plt.subplots(2, 1, figsize=(12, 7), sharex=True)
    fig.suptitle('Colorado West Nile Virus — Historical Surveillance (2010–2024)',
                 fontsize=14, fontweight='bold', y=0.98)

    # Cases
    bar_colors = ['#e53e3e' if y == peak_year else '#3182ce' for y in df_wnv[year_col]]
    axes[0].bar(df_wnv[year_col], df_wnv[case_col], color=bar_colors, alpha=0.85, zorder=3)
    axes[0].set_ylabel('Neuroinvasive Cases', fontsize=11)
    axes[0].set_title('Neuroinvasive Disease Cases by Year', fontsize=12)
    axes[0].grid(axis='y', alpha=0.3, zorder=0)
    axes[0].annotate(f'{int(peak_year)} peak\n({int(peak_count)} cases)',
                     xy=(peak_year, peak_count), xytext=(peak_year + 1.2, peak_count * 0.9),
                     arrowprops=dict(arrowstyle='->', color='#e53e3e'),
                     fontsize=9, color='#e53e3e')

    # Deaths
    axes[1].bar(df_wnv[year_col], df_wnv['deaths'], color='#742a2a', alpha=0.75, zorder=3)
    axes[1].set_ylabel('Deaths', fontsize=11)
    axes[1].set_title('WNV Deaths by Year', fontsize=12)
    axes[1].grid(axis='y', alpha=0.3, zorder=0)
    axes[1].yaxis.set_major_locator(mticker.MaxNLocator(integer=True))
    axes[1].set_xlabel('Year', fontsize=11)

    plt.tight_layout()
    plt.show()
    print(f'Source: {source_label}')
No description has been provided for this image
Source: CDC ArboNET (via surveillance module)
"""CSV Export & Accessibility Description: WNV Historical Cases & Deaths."""
print("### 📊 Alt Text & Data Export: WNV Historical Cases (2010–2024)")
print("**Chart Type:** Dual-panel bar chart")
print("**Visual:** Panel 1 shows WNV neuroinvasive cases by year (2010–2024), with 2012 peak of 130 cases highlighted in red.")
print("           Panel 2 shows WNV deaths by year, ranging 0–9 deaths annually.")
print("**Key Finding:** Colorado experiences annual WNV cases, with significant variability (6–130 cases/year).")
print("                Peak transmission years correlate with warm, wet springs.")
print()

try:
    export_wnv = df_wnv[['year', 'cases', 'deaths']].copy() if 'deaths' in df_wnv.columns else df_wnv[['year', 'cases']].copy()
    export_wnv.to_csv('wnv_annual_trend.csv', index=False)
    print(f"✓ Exported wnv_annual_trend.csv ({len(export_wnv)} rows)")
except Exception as e:
    print(f"Note: CSV export skipped ({e})")
### 📊 Alt Text & Data Export: WNV Historical Cases (2010–2024)
**Chart Type:** Dual-panel bar chart
**Visual:** Panel 1 shows WNV neuroinvasive cases by year (2010–2024), with 2012 peak of 130 cases highlighted in red.
           Panel 2 shows WNV deaths by year, ranging 0–9 deaths annually.
**Key Finding:** Colorado experiences annual WNV cases, with significant variability (6–130 cases/year).
                Peak transmission years correlate with warm, wet springs.

✓ Exported wnv_annual_trend.csv (15 rows)

1. Recent Climate Conditions (90-Day Window) #

Back to Home | Back to Top

# Climate section — use climate_df already loaded in setup cell
# Reload if running this cell independently
if 'climate_df' not in dir() or not isinstance(climate_df, pd.DataFrame) or climate_df.empty:
    try:
        climate_df = loader.load_noaa_climate_data("colorado", days_back=90)
    except Exception:
        climate_df = pd.DataFrame()

have_climate = len(climate_df) > 0 and 'temp_c' in climate_df.columns

if have_climate:
    df_clim = climate_df.copy()
    
    if 'date' in df_clim.columns:
        df_clim['date'] = pd.to_datetime(df_clim['date'], errors='coerce')
        df_clim = df_clim.dropna(subset=['date', 'temp_c'])
        df_clim = df_clim[df_clim['temp_c'] > -80].sort_values('date').reset_index(drop=True)
    
    climate_source = "NOAA (via surveillance module)"
    days_above_18 = int((df_clim['temp_c'] > 18).sum())
    
    print(f'Climate data: {len(df_clim)} valid days from {climate_source}')
    if 'date' in df_clim.columns:
        print(f'Date range: {df_clim["date"].min().date()} to {df_clim["date"].max().date()}')
    print(f'Mean temperature (°C): {df_clim["temp_c"].mean():.1f}')
    print(f'Days above 18°C (WNV transmission threshold): {days_above_18}')
    
    # Ecological features from surveillance module
    print('\nEcological feature engineering (Culex tarsalis):')
    try:
        thermal_suit = engine.compute_thermal_suitability(df_clim['temp_c'])
        gdd = engine.compute_growing_degree_days(df_clim['temp_c'])
        
        activity = None
        if 'date' in df_clim.columns:
            dates_idx = pd.DatetimeIndex(df_clim['date'])
            activity = engine.compute_activity_window(dates_idx)
        
        print(f'  • Mean thermal suitability (last 14d): {thermal_suit.tail(14).mean():.2f}/1.0')
        print(f'  • Accumulated growing degree-days    : {gdd.iloc[-1]:.0f} GDD (base 10°C)')
        if activity is not None:
            print(f'  • Seasonal activity window open      : {bool(activity.iloc[-1])} (today)')
    except Exception as e:
        print(f'  (Feature engineering note: {e})')
else:
    df_clim = pd.DataFrame()
    days_above_18 = None
    print('Climate data unavailable — showing transmission threshold reference only')
Climate data: 88 valid days from NOAA (via surveillance module)
Date range: 2026-07-03 to 2026-09-28
Mean temperature (°C): 22.2
Days above 18°C (WNV transmission threshold): 76

Ecological feature engineering (Culex tarsalis):
  • Mean thermal suitability (last 14d): 0.40/1.0
  • Accumulated growing degree-days    : 1072 GDD (base 10°C)
  • Seasonal activity window open      : True (today)
if not have_climate:
    print('Skipping climate chart (data unavailable)')
else:
    # Compute thermal suitability for chart overlay
    try:
        thermal_suit = engine.compute_thermal_suitability(df_clim['temp_c'])
        df_clim = df_clim.copy()
        df_clim['thermal_suitability'] = thermal_suit.values
    except Exception:
        df_clim['thermal_suitability'] = None

    nrows = 3 if 'precip_mm' in df_clim.columns else 2
    fig, axes = plt.subplots(nrows, 1, figsize=(13, 8), sharex=True)
    fig.suptitle('Colorado Climate Conditions — Last 90 Days (Denver)', fontsize=13, fontweight='bold')

    # Panel 1: Temperature with WNV threshold
    axes[0].plot(df_clim['date'], df_clim['temp_c'], color='#dd6b20', linewidth=1.5, label='Daily temp (°C)')
    axes[0].axhline(18, color='#e53e3e', linestyle='--', linewidth=1.2, label='WNV threshold (18°C)')
    axes[0].fill_between(df_clim['date'], df_clim['temp_c'], 18,
                         where=df_clim['temp_c'] > 18, alpha=0.15, color='#e53e3e', label='Above threshold')
    axes[0].set_ylabel('Temperature (°C)', fontsize=10)
    axes[0].legend(fontsize=9)
    axes[0].grid(alpha=0.3)

    # Panel 2: Thermal suitability for Culex tarsalis
    if df_clim['thermal_suitability'].notna().any():
        axes[1].plot(df_clim['date'], df_clim['thermal_suitability'],
                     color='#805ad5', linewidth=1.5, label='Thermal suitability (Cx. tarsalis)')
        axes[1].axhline(0.7, color='#e53e3e', linestyle='--', linewidth=1, alpha=0.7, label='High suitability (0.7)')
        axes[1].set_ylim(0, 1.05)
        axes[1].set_ylabel('Suitability (0–1)', fontsize=10)
        axes[1].legend(fontsize=9)
        axes[1].grid(alpha=0.3)
    else:
        axes[1].text(0.5, 0.5, 'Thermal suitability unavailable', ha='center', va='center',
                     transform=axes[1].transAxes, color='gray')

    # Panel 3: Precipitation (if available)
    if nrows == 3 and 'precip_mm' in df_clim.columns:
        axes[2].bar(df_clim['date'], df_clim['precip_mm'].fillna(0),
                    color='#3182ce', alpha=0.7, width=0.8, label='Precip (mm)')
        axes[2].set_ylabel('Precipitation (mm)', fontsize=10)
        axes[2].legend(fontsize=9)
        axes[2].grid(axis='y', alpha=0.3)
        axes[2].set_xlabel('Date', fontsize=10)
    else:
        axes[-1].set_xlabel('Date', fontsize=10)

    plt.tight_layout()
    plt.show()
    print(f'Source: {climate_source}')
No description has been provided for this image
Source: NOAA (via surveillance module)
"""CSV Export & Accessibility Description: Climate Data (90-Day Rolling)."""
print("### 📊 Alt Text & Data Export: Colorado Climate — 90-Day Rolling Window")
print("**Chart Type:** Multi-panel time-series with overlays")
print("**Visual:** Panel 1: Daily temperature (°C) vs. WNV transmission threshold (18°C, red dashed line).")
print("           Shaded area shows days >18°C when WNV transmission is possible.")
print("           Panel 2: Thermal suitability index (0–1 scale) for Culex tarsalis mosquito.")
print("                    Suitability >0.7 (red dashed line) indicates optimal vector competence.")
print("           Panel 3 (if available): Daily precipitation (mm) and cumulative rainfall trend.")
print("**Key Finding:** Vector transmission windows align with +18°C days and high thermal suitability.")
print()

try:
    export_clim = df_clim[['date', 'temp_c']].copy() if 'date' in df_clim.columns else df_clim.copy()
    if 'thermal_suitability' in export_clim.columns:
        pass
    elif 'thermal_suitability' in df_clim.columns:
        export_clim['thermal_suitability'] = df_clim['thermal_suitability']
    if 'precip_mm' in df_clim.columns:
        export_clim['precip_mm'] = df_clim['precip_mm']
    export_clim.to_csv('wnv_climate_90d.csv', index=False)
    print(f"✓ Exported wnv_climate_90d.csv ({len(export_clim)} rows)")
except Exception as e:
    print(f"Note: CSV export skipped ({e})")
### 📊 Alt Text & Data Export: Colorado Climate — 90-Day Rolling Window
**Chart Type:** Multi-panel time-series with overlays
**Visual:** Panel 1: Daily temperature (°C) vs. WNV transmission threshold (18°C, red dashed line).
           Shaded area shows days >18°C when WNV transmission is possible.
           Panel 2: Thermal suitability index (0–1 scale) for Culex tarsalis mosquito.
                    Suitability >0.7 (red dashed line) indicates optimal vector competence.
           Panel 3 (if available): Daily precipitation (mm) and cumulative rainfall trend.
**Key Finding:** Vector transmission windows align with +18°C days and high thermal suitability.

✓ Exported wnv_climate_90d.csv (88 rows)

🦟 iNaturalist Vector Observations # (click to expand)

Back to Home | Back to Top

# iNaturalist — use inat_df already loaded in setup cell
if 'inat_df' not in dir() or not isinstance(inat_df, pd.DataFrame) or inat_df.empty:
    try:
        inat_df = loader.load_inaturalist_vector_observations("mosquito", "colorado")
    except Exception:
        inat_df = pd.DataFrame()

have_inat = len(inat_df) > 0

if have_inat:
    df_inat = inat_df.copy()
    
    if 'date' in df_inat.columns:
        df_inat['observed_on'] = pd.to_datetime(df_inat['date'], errors='coerce')
    elif 'observed_on' in df_inat.columns:
        df_inat['observed_on'] = pd.to_datetime(df_inat['observed_on'], errors='coerce')
    
    df_inat = df_inat.dropna(subset=['observed_on'])
    
    print(f'iNaturalist mosquito observations (Colorado): {len(df_inat)}')
    
    species_col = next((c for c in ['species', 'taxon', 'taxon_name'] if c in df_inat.columns), None)
    if species_col:
        print('\nTop species observed:')
        print(df_inat[species_col].value_counts().head(8).to_string())

    # Monthly distribution chart
    df_inat['month'] = df_inat['observed_on'].dt.month
    monthly = df_inat.groupby('month').size().reindex(range(1, 13), fill_value=0)
    month_names = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']

    fig, ax = plt.subplots(figsize=(10, 4))
    ax.bar(month_names, monthly.values, color='#2f855a', alpha=0.8)
    ax.set_title('iNaturalist Mosquito Observations by Month (Colorado)', fontsize=12, fontweight='bold')
    ax.set_ylabel('Observations')
    ax.grid(axis='y', alpha=0.3)
    plt.tight_layout()
    plt.show()
else:
    df_inat = pd.DataFrame()
    print('No iNaturalist data available (API unavailable or no observations returned)')
No iNaturalist data available (API unavailable or no observations returned)

2. Early Warning Summary #

Back to Home | Back to Top

# Early Warning Summary — uses surveillance module outputs from setup cell

current_month = dt.date.today().month
current_month_name = calendar.month_name[current_month]
year_now = dt.date.today().year

# Reference module-computed values (or recompute if cell run independently)
_risk_label = globals().get('risk_label', 'UNKNOWN')
_risk_probability = globals().get('risk_probability', None)
_low_ci = globals().get('low_ci', None)
_high_ci = globals().get('high_ci', None)
_wnv_ytd = globals().get('wnv_ytd_cases', 0)
_lyme_ytd = globals().get('lyme_ytd_cases', 0)
_inat_obs = globals().get('inat_count', 0)

# Climate stats from the climate section
_have_climate = globals().get('have_climate', False)
_df_clim = globals().get('df_clim', pd.DataFrame())
_recent_mean_temp = None
_days_above_18_summary = None
if _have_climate and len(_df_clim) > 0 and 'temp_c' in _df_clim.columns:
    recent14 = _df_clim['temp_c'].tail(14)
    _recent_mean_temp = float(recent14.mean()) if len(recent14) else None
    _days_above_18_summary = int((_df_clim['temp_c'] > 18).sum())

# Registry metadata for this disease
wnv_info = registry.get_disease_characteristics(DiseaseType.WEST_NILE_VIRUS)
mosquito_ecology = registry.get_vector_ecology(VectorType.MOSQUITO)

print('=' * 62)
print(f'  EARLY WARNING SUMMARY — {current_month_name} {year_now}')
print('=' * 62)
print(f'  Disease          : West Nile Virus')
print(f'  Vectors          : {", ".join(mosquito_ecology.scientific_names)}')
print(f'  Key hosts        : {", ".join(mosquito_ecology.primary_hosts[:4])}')
print(f'  CFR (neuroinvas) : {wnv_info.case_fatality_rate:.1%}')
print(f'  Activity season  : Months {mosquito_ecology.activity_season[0]}–{mosquito_ecology.activity_season[1]}')
print()
print(f'  Risk Signal: {_risk_label}', end='')
if _risk_probability is not None:
    print(f'  ({_risk_probability:.1%})', end='')
    if _low_ci is not None and _high_ci is not None:
        print(f'  [95% CI: {_low_ci:.1%}–{_high_ci:.1%}]', end='')
print()
print()
print('  What this tells you right now:')
print(f'  • {year_now} YTD WNV cases (provisional)  : {_wnv_ytd}')
print(f'  • {year_now} YTD Lyme cases (tick context) : {_lyme_ytd}')
print(f'  • iNaturalist mosquito observations      : {_inat_obs}')
if _recent_mean_temp is not None:
    above = '✓ ABOVE' if _recent_mean_temp > 18 else '✓ below'
    print(f'  • 14-day mean temperature                : {_recent_mean_temp:.1f}°C  ({above} WNV threshold)')
    print(f'  • Days above 18°C (90-day window)        : {_days_above_18_summary}')
else:
    print('  • Temperature data: unavailable')
print()
print('  Next-step guidance:')
if _risk_label == 'LOW':
    print('  ✓ Continue routine monitoring. No immediate surge conditions.')
elif _risk_label == 'MODERATE':
    print('  ⚠  Increase monitoring frequency. Watch for pool positivity uptick.')
else:
    print('  ⛔  Elevated risk — notify mosquito districts and public health.')
    print('      Review current pool testing results and aerial spray coverage.')
print()
print('  Data sources: CDC ArboNET | NOAA | NASA POWER | iNaturalist')
print(f'  Generated   : {dt.date.today()}  |  Module: aedesproject_uif.surveillance')
print('=' * 62)

# Expose for snapshot cell
_recent_mean_temp_snapshot = _recent_mean_temp
_days_above_18_snapshot = _days_above_18_summary
_climate_data_days_snapshot = len(_df_clim) if _have_climate else 0
==============================================================
  EARLY WARNING SUMMARY — October 2026
==============================================================
  Disease          : West Nile Virus
  Vectors          : Culex tarsalis
  Key hosts        : birds, mammals, humans
  CFR (neuroinvas) : 0.3%
  Activity season  : Months 4–10

  Risk Signal: LOW  (24.2%)  [95% CI: 10.3%–38.0%]

  What this tells you right now:
  • 2026 YTD WNV cases (provisional)  : 0
  • 2026 YTD Lyme cases (tick context) : 0
  • iNaturalist mosquito observations      : 0
  • 14-day mean temperature                : 16.1°C  (✓ below WNV threshold)
  • Days above 18°C (90-day window)        : 76

  Next-step guidance:
  ✓ Continue routine monitoring. No immediate surge conditions.

  Data sources: CDC ArboNET | NOAA | NASA POWER | iNaturalist
  Generated   : 2026-10-01  |  Module: aedesproject_uif.surveillance
==============================================================
"""CSV Export: iNaturalist observations."""
try:
    if 'df_inat' in dir() and len(df_inat) > 0:
        export_inat = df_inat[[col for col in ['observed_on', 'species_guess', 'quality_grade', 'latitude', 'longitude'] if col in df_inat.columns]].copy()
        export_inat.to_csv('inat_mosquito_obs.csv', index=False)
        print(f"✓ Exported inat_mosquito_obs.csv ({len(export_inat)} rows)")
except Exception as e:
    print(f"Note: CSV export skipped ({e})")

📈 Run History # (click to expand)

Each notebook execution appends a row to data/surveillance/history/notebook_snapshots.csv. This section saves the current run and renders trend charts so you can compare results across days and weeks.

Back to Home | Back to Top

# Validation summary — run multi-layer validation with available data

print("Running multi-layer validation...\n")

validation_ran = False

# Entomological: thermal suitability vs activity window
if len(climate_df) > 14 and 'temp_c' in climate_df.columns:
    try:
        thermal = engine.compute_thermal_suitability(climate_df['temp_c'])
        
        if 'date' in climate_df.columns:
            dates_idx = pd.DatetimeIndex(pd.to_datetime(climate_df['date'], errors='coerce'))
            activity = engine.compute_activity_window(dates_idx)
        else:
            activity = pd.Series(1.0, index=climate_df.index)

        # Align and drop NaN (rolling window introduces NaNs at edges)
        mask = thermal.notna() & activity.notna()
        corr_results = validator.validate_entomological_correlation(
            thermal[mask], activity[mask]
        )
        print("Entomological correlation (thermal suitability vs activity window):")
        print(f"  Pearson r  = {corr_results.get('pearson_correlation', 'N/A'):.3f}")
        print(f"  Spearman r = {corr_results.get('spearman_correlation', 'N/A'):.3f}")
        validation_ran = True
    except Exception as e:
        print(f"  (Entomological validation note: {e})")

print(f"\nValidation report: {len(validator.get_validation_report())} checks recorded")
if not validation_ran:
    print("  Insufficient data for full validation this run (expected when data feeds are unavailable)")
    print("  Validation will populate when CDC/NOAA data feeds are live")

print(f"\n✓ Notebook complete — {dt.date.today()}")
Running multi-layer validation...

Entomological correlation (thermal suitability vs activity window):
  (Entomological validation note: Unknown format code 'f' for object of type 'str')

Validation report: 1 checks recorded
  Insufficient data for full validation this run (expected when data feeds are unavailable)
  Validation will populate when CDC/NOAA data feeds are live

✓ Notebook complete — 2026-10-01
# ── Run-history trend charts ────────────────────────────────────────────────
hist = load_snapshot_history()

if len(hist) < 2:
    print(f"Only {len(hist)} snapshot(s) on record — run the notebook again tomorrow to see trends.")
    print('Snapshot columns:', list(hist.columns))
else:
    numeric_cols = ['climate_mean_temp_14d', 'climate_days_above_18',
                    'wnv_ytd_cases', 'lyme_ytd_cases', 'inat_obs_count']
    for col in numeric_cols:
        hist[col] = pd.to_numeric(hist[col], errors='coerce')

    fig, axes = plt.subplots(3, 1, figsize=(13, 10), sharex=True)
    fig.suptitle('Notebook Snapshot History — Rolling Metrics Over Time',
                 fontsize=14, fontweight='bold', y=0.99)

    # Panel 1: 14-day mean temperature
    ax = axes[0]
    ax.plot(hist['snapshot_date'], hist['climate_mean_temp_14d'],
            marker='o', color='#dd6b20', linewidth=1.8, markersize=5, label='14-day mean temp (°C)')
    ax.axhline(18, color='#e53e3e', linestyle='--', linewidth=1, label='WNV threshold 18°C')
    ax.set_ylabel('°C', fontsize=10)
    ax.set_title('14-Day Mean Temperature (Denver)', fontsize=11)
    ax.legend(fontsize=9)
    ax.grid(alpha=0.3)

    # Panel 2: YTD case counts
    ax = axes[1]
    ax.plot(hist['snapshot_date'], hist['wnv_ytd_cases'].fillna(0),
            marker='s', color='#e53e3e', linewidth=1.8, markersize=5, label='WNV YTD cases')
    ax.plot(hist['snapshot_date'], hist['lyme_ytd_cases'].fillna(0),
            marker='^', color='#2f855a', linewidth=1.8, markersize=5, label='Lyme YTD cases')
    ax.set_ylabel('Cases', fontsize=10)
    ax.set_title('YTD Case Counts (Provisional, Colorado)', fontsize=11)
    ax.yaxis.set_major_locator(mticker.MaxNLocator(integer=True))
    ax.legend(fontsize=9)
    ax.grid(alpha=0.3)

    # Panel 3: iNaturalist observations loaded
    ax = axes[2]
    ax.bar(hist['snapshot_date'], hist['inat_obs_count'].fillna(0),
           color='#553c9a', alpha=0.75, width=0.8, label='iNaturalist obs loaded')
    ax.set_ylabel('Observations', fontsize=10)
    ax.set_title('iNaturalist Mosquito Observations (snapshot total)', fontsize=11)
    ax.yaxis.set_major_locator(mticker.MaxNLocator(integer=True))
    ax.legend(fontsize=9)
    ax.grid(axis='y', alpha=0.3)
    ax.set_xlabel('Snapshot Date', fontsize=10)

    plt.tight_layout()
    plt.savefig('wnv_run_history.png', dpi=150, bbox_inches='tight')
    plt.show()

    print(f"\nHistory spans {len(hist)} snapshots  "
          f"({hist['snapshot_date'].min().date()} → {hist['snapshot_date'].max().date()})")
    print(hist[['snapshot_date', 'wnv_ytd_cases', 'lyme_ytd_cases',
                'climate_mean_temp_14d', 'climate_days_above_18']].to_string(index=False))
Only 0 snapshot(s) on record — run the notebook again tomorrow to see trends.
Snapshot columns: ['snapshot_date', 'notebook_run_ts', 'wnv_ytd_cases', 'lyme_ytd_cases', 'climate_mean_temp_14d', 'climate_days_above_18', 'climate_data_days', 'inat_obs_count', 'risk_level']
"""CSV Export: Notebook run history metrics."""
try:
    if len(hist) > 0:
        cols_to_export = [col for col in ['date', 'climate_mean_temp_14d', 'climate_days_above_18', 'wnv_ytd_cases', 'lyme_ytd_cases', 'inat_obs_count'] if col in hist.columns]
        export_hist = hist[cols_to_export].copy()
        export_hist.to_csv('wnv_run_history.csv', index=False)
        print(f"✓ Exported wnv_run_history.csv ({len(export_hist)} rows)")
except Exception as e:
    print(f"Note: CSV export skipped ({e})")
📚 Background & Methodology (click to expand)

Platform Overview: Ecological Surveillance and Risk Forecasting¶

This platform is designed to support:

  • Environmental risk forecasting for vector-borne and zoonotic diseases
  • Ecological surveillance using open environmental, entomological, and epidemiological data
  • Vector habitat suitability modeling (mosquitoes, ticks, rodents, birds)
  • Climate anomaly detection (drought, precipitation, wildfire, temperature)
  • Public health early warning and operational support
  • Probabilistic risk estimation with uncertainty quantification

Colorado-relevant diseases prioritized:

  • West Nile Virus
  • Lyme disease
  • Colorado tick fever
  • Rocky Mountain spotted fever
  • Tularemia
  • Plague
  • Hantavirus exposure risk
  • Mosquito-borne encephalitis
  • Emerging climate-shifted vector diseases

One Health integration:

  • Human health, animal reservoirs, vector ecology, climate, land use, and public health operations are jointly modeled.

Data Sources and Integration¶

Primary Data Sources:

  • CDC ArboNET: Human case surveillance, mosquito pool testing, vector surveillance
  • NOAA: Climate anomalies, drought, precipitation, temperature
  • NASA MODIS/EarthData: Vegetation, land surface temperature, wildfire, land cover
  • USGS: Hydrology, water bodies, ecological layers
  • CDC Vector Distribution: Official vector presence/absence and suitability maps
  • Colorado Department of Public Health & Environment: Local case reports, vector surveillance
  • Mosquito Districts: Trap counts, pool testing, spatial coverage
  • Migratory Bird Datasets: eBird, USGS, state ornithology data
  • Land Cover/Wildfire: USGS, NASA, state sources
  • Drought Severity: US Drought Monitor, NOAA

Integration Strategy:

  • Modular ETL pipelines for each source
  • Automated data validation and harmonization
  • Metadata and provenance tracking for all datasets

CDC ArboNET Integration:

  • Weekly human case reports (county, disease, onset date)
  • Mosquito pool testing (location, species, infection status)
  • Tick/vector surveillance (where available)
  • Used for both model training and validation

Environmental and Ecological Feature Engineering¶

The system models:

  • Mosquito habitat suitability (climate, water, vegetation, land cover)
  • Tick habitat suitability (vegetation, humidity, host presence)
  • Rodent exposure zones (land cover, human proximity)
  • Bird migration impacts (seasonal corridors, vector dispersal)
  • Standing water persistence (hydrology, precipitation, drought)
  • Vegetation and humidity conditions (NDVI, EVI, MODIS)
  • Drought and precipitation anomalies (NOAA, US Drought Monitor)
  • Wildfire ecological disruption (NASA, USGS)
  • Human recreational exposure risk (land use, OSM, parks, trails)

All features are engineered to support ecological, entomological, and epidemiological modeling.


One Health System Framing¶

This platform explicitly connects:

  • Human health: Case surveillance, exposure risk, intervention targeting
  • Animal reservoirs: Rodent/tick/mosquito habitats, bird migration
  • Vector ecology: Mosquito/tick suitability, pool testing, spatial expansion
  • Climate systems: Drought, precipitation, temperature, anomalies
  • Land use: Urban/rural, water bodies, vegetation, wildfire
  • Public health operations: Early warning, resource allocation, intervention planning

The One Health approach ensures robust, actionable, and scientifically defensible public health interventions.


Validation Methodology¶

Validation Layers:

  • Ecological validation: Vector habitat suitability vs. CDC/USGS maps, mosquito/tick pool positives
  • Entomological validation: Mosquito/tick pool positives, spatial/temporal expansion
  • Epidemiological validation: Outbreak detection, lead time, spatial accuracy for human cases
  • Operational validation: Actionable lead time, false alert rate, intervention/resource allocation efficiency

Validation Methods:

  • Rolling historical backtesting (train on past, test on future)
  • Geographic holdout (hold out counties/regions)
  • Climate drift testing (pre/post anomaly periods)
  • Outbreak lead-time analysis
  • False positive/negative analysis
  • Uncertainty estimation (confidence intervals, probability bands)
  • Baseline comparison (seasonal averages, persistence, official alerts)

Testing Strategy¶

Tests validate:

  • Vector habitat prediction accuracy (vs. CDC/USGS maps, mosquito pools)
  • Environmental anomaly detection (drought, wildfire, precipitation)
  • Mosquito pool correlation (spatial/temporal)
  • Hotspot persistence and emergence
  • County-level outbreak forecasting (lead time, sensitivity, specificity)
  • Robustness to incomplete/missing surveillance data
  • Regional generalization (urban/rural, ecological zones)

Risk Scoring and Uncertainty¶

  • Risk is modeled as a probability surface, not a binary hotspot.
  • Scores reflect:
    • Probability of vector presence
    • Probability of human exposure
    • Confidence/uncertainty intervals
    • Environmental anomaly modifiers
  • Visualizations include probability bands and uncertainty overlays.

Limitations¶

  • Uneven surveillance coverage (spatial/temporal gaps)
  • Underreporting of human cases
  • Climate and ecological uncertainty
  • Sparse rural data and changing vector ranges
  • Public health response limitations (capacity, resources)
  • Ecological complexity and non-stationarity

Data Signal Prioritization¶

  • Primary: Ecological, vector, and official epidemiological data
  • Secondary: Search trends, social signals (supporting, not leading)

Open Science & Digital Public Goods Principles¶

  • All code, data, and models are open-source and reproducible
  • Modular architecture for easy extension and integration
  • Transparent methods and provenance for all data and forecasts
  • Designed for public-sector interoperability and scientific review