Skip to notebook content 🏠 Home ← Previous Current Season Monitoring Next →

2026 Season Monitoring¶

Real-time tracking of Colorado's 2026 vector-borne disease season with weekly case counts, baseline comparisons, and early-warning alert levels.

# Import required libraries and set up paths
import os
import sys
import json
import pandas as pd
import numpy as np
from datetime import date, datetime, timedelta
import plotly.graph_objects as go
import plotly.express as px
from plotly.subplots import make_subplots

# Add src to path for module imports
sys.path.insert(0, os.path.join(os.getcwd(), '..'))

# Define output and data directories
OUTPUT_DIR = os.path.join(os.getcwd(), '..', 'processed', 'Dashboard', 'current_season')
DATA_DIR = os.path.join(os.getcwd(), '..', 'data', 'surveillance')
os.makedirs(OUTPUT_DIR, exist_ok=True)
os.makedirs(DATA_DIR, exist_ok=True)

# Define data paths
season_ytd_path = os.path.join(DATA_DIR, '2026_season_ytd.json')
wnv_hist_path = os.path.join(DATA_DIR, 'wnv_colorado.json')
lyme_hist_path = os.path.join(DATA_DIR, 'lyme_colorado.json')

# Load 2026 season YTD data
try:
    with open(season_ytd_path) as f:
        season_data = json.load(f)
    print(f"✓ Loaded 2026 season YTD data")
except FileNotFoundError:
    print(f"⚠ Season YTD file not found at {season_ytd_path}")
    season_data = {
        "season": "2026",
        "data": [],
        "historical_baseline_2024": {}
    }

# Initialize current_season_df early to avoid NameError in processing sections
if season_data.get("data"):
    current_season_df = pd.DataFrame(season_data['data'])
else:
    current_season_df = pd.DataFrame({
        'week': [],
        'date': [],
        'wnv_cases': [],
        'lyme_cases': [],
        'rmsf_cases': []
    })

# Initialize climate_df early to avoid NameError in processing sections
climate_df = pd.DataFrame({'date': [], 'temp_c': [], 'precip_mm': []})

# Load historical WNV data
try:
    with open(wnv_hist_path) as f:
        wnv_raw = json.load(f)
    wnv_hist = pd.DataFrame(wnv_raw.get("data", []))
except FileNotFoundError:
    wnv_hist = pd.DataFrame(columns=['year', 'neuroinvasive', 'deaths'])

# Load historical Lyme data
try:
    with open(lyme_hist_path) as f:
        lyme_raw = json.load(f)
    lyme_hist = pd.DataFrame(lyme_raw.get("data", []))
except FileNotFoundError:
    lyme_hist = pd.DataFrame(columns=['year', 'confirmed', 'probable'])

# Extract season variables
SEASON_YEAR = int(season_data.get("season", "2026"))
TODAY = date.today()
CURRENT_WEEK = TODAY.isocalendar()[1]

# Convert season data to DataFrame with cumulative calculations
weekly_data = season_data.get("data", [])
weekly_current = pd.DataFrame(weekly_data) if weekly_data else pd.DataFrame(
    columns=['week', 'date', 'wnv_cases', 'lyme_cases', 'rmsf_cases', 'source', 'notes']
)

# Ensure numeric columns and calculate cumulative sums
for col in ['wnv_cases', 'lyme_cases', 'rmsf_cases']:
    if col in weekly_current.columns:
        weekly_current[col] = pd.to_numeric(weekly_current[col], errors='coerce').fillna(0)
    else:
        weekly_current[col] = 0

weekly_current['wnv_cumulative'] = weekly_current['wnv_cases'].cumsum()
weekly_current['lyme_cumulative'] = weekly_current['lyme_cases'].cumsum()
weekly_current['rmsf_cumulative'] = weekly_current['rmsf_cases'].cumsum()

# Load historical baseline for comparison
historical_2024 = season_data.get("historical_baseline_2024", {})

# Calculate baseline statistics from historical data
wnv_baseline_stats = {
    'median_annual': historical_2024.get('wnv_cases_full_year', 12),
    'peak_month': historical_2024.get('peak_wnv_month', 'August'),
    'mean_annual': wnv_hist['neuroinvasive'].mean() if len(wnv_hist) > 0 else 0,
    'p25': wnv_hist['neuroinvasive'].quantile(0.25) if len(wnv_hist) > 0 else 0,
    'p75': wnv_hist['neuroinvasive'].quantile(0.75) if len(wnv_hist) > 0 else 0,
    'recent_5yr_avg': wnv_hist[wnv_hist['year'] >= SEASON_YEAR - 5]['neuroinvasive'].mean() if len(wnv_hist) > 0 else 0,
}

lyme_baseline_stats = {
    'median_annual': historical_2024.get('lyme_cases_full_year', 119),
    'peak_month': historical_2024.get('peak_lyme_month', 'July'),
    'mean_annual': (lyme_hist['confirmed'].mean() if len(lyme_hist) > 0 else 0),
    'p25': (lyme_hist['confirmed'].quantile(0.25) if len(lyme_hist) > 0 else 0),
    'p75': (lyme_hist['confirmed'].quantile(0.75) if len(lyme_hist) > 0 else 0),
    'recent_5yr_avg': (lyme_hist[lyme_hist['year'] >= SEASON_YEAR - 5]['confirmed'].mean() if len(lyme_hist) > 0 else 0),
}

# Initialize alert_metrics dictionary for downstream cells
alert_metrics = {'wnv': {}, 'lyme': {}}

if len(weekly_current) > 0:
    latest_week = weekly_current.iloc[-1]
    
    # WNV Alert Metrics
    wnv_ytd = latest_week.get('wnv_cumulative', 0)
    wnv_expected_at_this_week = (CURRENT_WEEK / 52) * wnv_baseline_stats['median_annual']
    wnv_deviation_pct = ((wnv_ytd - wnv_expected_at_this_week) / max(wnv_expected_at_this_week, 1)) * 100
    
    alert_metrics['wnv']['ytd_cases'] = wnv_ytd
    alert_metrics['wnv']['expected_at_week'] = wnv_expected_at_this_week
    alert_metrics['wnv']['deviation_pct'] = wnv_deviation_pct
    alert_metrics['wnv']['alert_level'] = (
        '🟢 LOW' if wnv_deviation_pct < -25 else
        '🟡 MODERATE' if wnv_deviation_pct < 0 else
        '🟠 HIGH' if wnv_deviation_pct < 25 else
        '🔴 SEVERE'
    )
    
    # Lyme Alert Metrics
    lyme_ytd = latest_week.get('lyme_cumulative', 0)
    lyme_expected_at_this_week = (CURRENT_WEEK / 52) * lyme_baseline_stats['median_annual']
    lyme_deviation_pct = ((lyme_ytd - lyme_expected_at_this_week) / max(lyme_expected_at_this_week, 1)) * 100
    
    alert_metrics['lyme']['ytd_cases'] = lyme_ytd
    alert_metrics['lyme']['expected_at_week'] = lyme_expected_at_this_week
    alert_metrics['lyme']['deviation_pct'] = lyme_deviation_pct
    alert_metrics['lyme']['alert_level'] = (
        '🟢 LOW' if lyme_deviation_pct < -25 else
        '🟡 MODERATE' if lyme_deviation_pct < 0 else
        '🟠 HIGH' if lyme_deviation_pct < 25 else
        '🔴 SEVERE'
    )
else:
    # No data yet - set defaults
    alert_metrics['wnv']['ytd_cases'] = 0
    alert_metrics['wnv']['expected_at_week'] = 0
    alert_metrics['wnv']['deviation_pct'] = 0
    alert_metrics['wnv']['alert_level'] = '🟢 LOW'
    alert_metrics['lyme']['ytd_cases'] = 0
    alert_metrics['lyme']['expected_at_week'] = 0
    alert_metrics['lyme']['deviation_pct'] = 0
    alert_metrics['lyme']['alert_level'] = '🟢 LOW'

# Display system info
print(f"✓ Python environment initialized")
print(f"  Working directory: {os.getcwd()}")
print(f"  Output directory: {OUTPUT_DIR}")
print(f"  Data directory: {DATA_DIR}")
print(f"  Season year: {SEASON_YEAR}")
print(f"  Current week: {CURRENT_WEEK}")
print(f"  Weekly data rows loaded: {len(weekly_current)}")
print(f"  Historical WNV records: {len(wnv_hist)}")
print(f"  Historical Lyme records: {len(lyme_hist)}")
✓ Loaded 2026 season YTD data
✓ Python environment initialized
  Working directory: /home/runner/work/aedesproject-uif/aedesproject-uif/notebooks
  Output directory: /home/runner/work/aedesproject-uif/aedesproject-uif/notebooks/../processed/Dashboard/current_season
  Data directory: /home/runner/work/aedesproject-uif/aedesproject-uif/notebooks/../data/surveillance
  Season year: 2026
  Current week: 40
  Weekly data rows loaded: 2
  Historical WNV records: 15
  Historical Lyme records: 10
def validate_data_integrity():
    """
    Validation checks to confirm data is ready for analysis.
    Returns tuple of (is_valid, messages)
    """
    messages = []
    
    # Check 1: Data files exist
    if not os.path.exists(season_ytd_path):
        messages.append("⚠ 2026 YTD file not found")
    else:
        messages.append("✓ 2026 YTD file present")
    
    # Check 2: Required columns in current season
    if len(weekly_current) > 0:
        required_cols = ['week', 'wnv_cases', 'lyme_cases']
        missing = [c for c in required_cols if c not in weekly_current.columns]
        if missing:
            messages.append(f"⚠ Missing columns in weekly data: {missing}")
        else:
            messages.append(f"✓ Weekly data has {len(weekly_current)} rows")
    
    # Check 3: Latest week is recent
    if len(weekly_current) > 0:
        latest_week_num = weekly_current['week'].max()
        weeks_stale = CURRENT_WEEK - latest_week_num
        if weeks_stale > 2:
            messages.append(f"⚠ Data is {weeks_stale} weeks old (latest: week {latest_week_num})")
        else:
            messages.append(f"✓ Data is current (latest: week {latest_week_num})")
    
    # Check 4: Non-null key fields
    if len(weekly_current) > 0:
        nulls = weekly_current[['wnv_cases', 'lyme_cases']].isnull().sum().sum()
        if nulls > 0:
            messages.append(f"⚠ Found {nulls} null values in case counts")
        else:
            messages.append("✓ No null values in case counts")
    
    is_valid = all('✓' in m or '⚠' in m for m in messages) and sum(1 for m in messages if '⚠' in m) < 3
    return is_valid, messages


def refresh_season_analysis(force=False):
    """
    Reusable function to refresh analysis with latest data.
    Call this weekly or when new CDC data arrives.
    
    Args:
        force (bool): If True, reload all data fresh. If False, use cache.
    """
    print(f"\n{'=' * 70}")
    print(f"REFRESH SEASON ANALYSIS — {date.today().isoformat()}")
    print(f"{'=' * 70}")
    
    # Validate current data
    is_valid, messages = validate_data_integrity()
    
    print("\nData Integrity Checks:")
    for msg in messages:
        print(f"  {msg}")
    
    if not is_valid:
        print("\n⚠ Data integrity issues detected. Consider:")
        print("  1. Run scripts/fetch_surveillance_data.py to refresh")
        print("  2. Verify CDC NNDSS data availability")
        print("  3. Check data/surveillance/ for file errors")
        return False
    
    print("\n✓ Data integrity validated. Analysis is current.")
    return True


# Run validation on load
print("\n" + "=" * 70)
print("DATA INTEGRITY VALIDATION")
print("=" * 70)
is_valid, validation_msgs = validate_data_integrity()
for msg in validation_msgs:
    print(f"  {msg}")

if is_valid:
    print("\n✓ Notebook is ready to use. All systems nominal.")
    print(f"  Next data update expected: {date.today() + timedelta(days=7)}")
else:
    print("\n⚠ Some data issues detected. Partial analysis available.")

print("=" * 70)
======================================================================
DATA INTEGRITY VALIDATION
======================================================================
  ✓ 2026 YTD file present
  ✓ Weekly data has 2 rows
  ⚠ Data is 20 weeks old (latest: week 20)
  ✓ No null values in case counts

✓ Notebook is ready to use. All systems nominal.
  Next data update expected: 2026-10-08
======================================================================

8. Automate Refresh for New Reporting Weeks¶

# Generate summary tables for dashboard/export
summary_data = {
    'season_year': SEASON_YEAR,
    'current_week': CURRENT_WEEK,
    'current_date': TODAY.isoformat(),
    'wnv_ytd': alert_metrics['wnv']['ytd_cases'],
    'wnv_alert': alert_metrics['wnv']['alert_level'],
    'lyme_ytd': alert_metrics['lyme']['ytd_cases'],
    'lyme_alert': alert_metrics['lyme']['alert_level'],
    'baseline_comparison': {
        'wnv': {
            'ytd': alert_metrics['wnv']['ytd_cases'],
            'expected': alert_metrics['wnv']['expected_at_week'],
            'deviation_pct': alert_metrics['wnv']['deviation_pct'],
            'historical_median': wnv_baseline_stats['median_annual'],
            'recent_5yr_avg': wnv_baseline_stats['recent_5yr_avg']
        },
        'lyme': {
            'ytd': alert_metrics['lyme']['ytd_cases'],
            'expected': alert_metrics['lyme']['expected_at_week'],
            'deviation_pct': alert_metrics['lyme']['deviation_pct'],
            'historical_median': lyme_baseline_stats['median_annual'],
            'recent_5yr_avg': lyme_baseline_stats['recent_5yr_avg']
        }
    }
}

# Export summary to JSON
summary_path = os.path.join(OUTPUT_DIR, 'current_season_summary.json')
with open(summary_path, 'w') as f:
    json.dump(summary_data, f, indent=2, default=str)
print(f"✓ Exported current season summary: {summary_path}")

# Create display table
summary_table = pd.DataFrame({
    'Disease': ['West Nile Virus', 'Lyme Disease'],
    'YTD Cases': [alert_metrics['wnv']['ytd_cases'], alert_metrics['lyme']['ytd_cases']],
    'Expected*': [alert_metrics['wnv']['expected_at_week'], alert_metrics['lyme']['expected_at_week']],
    'Deviation': [
        f"{alert_metrics['wnv']['deviation_pct']:+.1f}%",
        f"{alert_metrics['lyme']['deviation_pct']:+.1f}%"
    ],
    'Alert Level': [alert_metrics['wnv']['alert_level'], alert_metrics['lyme']['alert_level']],
    'Historical Median': [
        wnv_baseline_stats['median_annual'],
        lyme_baseline_stats['median_annual']
    ]
})

print("\n" + "=" * 90)
print(f"CURRENT SEASON ({SEASON_YEAR}) SUMMARY — Week {CURRENT_WEEK}")
print("=" * 90)
print(summary_table.to_string(index=False))
print(f"\n* Expected based on historical average pace through week {CURRENT_WEEK}")
print("=" * 90)

# Export weekly detail
if len(weekly_current) > 0:
    weekly_export = weekly_current[[
        'week', 'date', 'wnv_cases', 'lyme_cases', 'rmsf_cases',
        'wnv_cumulative', 'lyme_cumulative'
    ]].copy()
    weekly_export['date'] = weekly_export['date'].astype(str)
    
    weekly_path = os.path.join(OUTPUT_DIR, f'{SEASON_YEAR}_weekly_cases.csv')
    weekly_export.to_csv(weekly_path, index=False)
    print(f"\n✓ Exported weekly detail: {weekly_path}")
✓ Exported current season summary: /home/runner/work/aedesproject-uif/aedesproject-uif/notebooks/../processed/Dashboard/current_season/current_season_summary.json

==========================================================================================
CURRENT SEASON (2026) SUMMARY — Week 40
==========================================================================================
        Disease  YTD Cases  Expected* Deviation Alert Level  Historical Median
West Nile Virus          0   9.230769   -100.0%       🟢 LOW                 12
   Lyme Disease          2  91.538462    -97.8%       🟢 LOW                119

* Expected based on historical average pace through week 40
==========================================================================================

✓ Exported weekly detail: /home/runner/work/aedesproject-uif/aedesproject-uif/notebooks/../processed/Dashboard/current_season/2026_weekly_cases.csv

7. Generate Current-Season Summary Tables¶

# Calculate alert metrics for current season
alert_metrics = {
    'wnv': {},
    'lyme': {}
}

if len(weekly_current) > 0:
    latest_week = weekly_current.iloc[-1]
    
    # WNV Alerts
    wnv_ytd = latest_week.get('wnv_cumulative', 0)
    wnv_expected_at_this_week = (CURRENT_WEEK / 52) * wnv_baseline_stats['median_annual']
    wnv_deviation_pct = ((wnv_ytd - wnv_expected_at_this_week) / max(wnv_expected_at_this_week, 1)) * 100
    
    alert_metrics['wnv']['ytd_cases'] = wnv_ytd
    alert_metrics['wnv']['expected_at_week'] = wnv_expected_at_this_week
    alert_metrics['wnv']['deviation_pct'] = wnv_deviation_pct
    alert_metrics['wnv']['alert_level'] = (
        '🟢 LOW' if wnv_deviation_pct < -25
        else '🟡 MODERATE' if wnv_deviation_pct < 0
        else '🟠 HIGH' if wnv_deviation_pct < 25
        else '🔴 SEVERE'
    )
    
    # Lyme Alerts
    lyme_ytd = latest_week.get('lyme_cumulative', 0)
    lyme_expected_at_this_week = (CURRENT_WEEK / 52) * lyme_baseline_stats['median_annual']
    lyme_deviation_pct = ((lyme_ytd - lyme_expected_at_this_week) / max(lyme_expected_at_this_week, 1)) * 100
    
    alert_metrics['lyme']['ytd_cases'] = lyme_ytd
    alert_metrics['lyme']['expected_at_week'] = lyme_expected_at_this_week
    alert_metrics['lyme']['deviation_pct'] = lyme_deviation_pct
    alert_metrics['lyme']['alert_level'] = (
        '🟢 LOW' if lyme_deviation_pct < -25
        else '🟡 MODERATE' if lyme_deviation_pct < 0
        else '🟠 HIGH' if lyme_deviation_pct < 25
        else '🔴 SEVERE'
    )

print("=" * 70)
print("IN-SEASON ALERT METRICS")
print("=" * 70)
print(f"\nWeek {CURRENT_WEEK} of {SEASON_YEAR}")
print(f"\nWest Nile Virus:")
print(f"  YTD Cases: {alert_metrics['wnv']['ytd_cases']:.0f}")
print(f"  Expected at this week (historical): {alert_metrics['wnv']['expected_at_week']:.1f}")
print(f"  Deviation: {alert_metrics['wnv']['deviation_pct']:+.1f}%")
print(f"  Alert Level: {alert_metrics['wnv']['alert_level']}")

print(f"\nLyme Disease:")
print(f"  YTD Cases: {alert_metrics['lyme']['ytd_cases']:.0f}")
print(f"  Expected at this week (historical): {alert_metrics['lyme']['expected_at_week']:.1f}")
print(f"  Deviation: {alert_metrics['lyme']['deviation_pct']:+.1f}%")
print(f"  Alert Level: {alert_metrics['lyme']['alert_level']}")

print(f"\nInterpretation:")
print(f"  Negative deviation = Below historical pace")
print(f"  Positive deviation = Above historical pace (watch closely)")
print("=" * 70)
======================================================================
IN-SEASON ALERT METRICS
======================================================================

Week 40 of 2026

West Nile Virus:
  YTD Cases: 0
  Expected at this week (historical): 9.2
  Deviation: -100.0%
  Alert Level: 🟢 LOW

Lyme Disease:
  YTD Cases: 2
  Expected at this week (historical): 91.5
  Deviation: -97.8%
  Alert Level: 🟢 LOW

Interpretation:
  Negative deviation = Below historical pace
  Positive deviation = Above historical pace (watch closely)
======================================================================

6. Add In-Season Alert Metrics¶

# CHART 1: Historical WNV Trend with 2026 YTD projection
fig1 = go.Figure()

# Historical annual totals (all years in dataset)
if len(wnv_hist) > 0:
    fig1.add_trace(go.Bar(
        x=wnv_hist['year'],
        y=wnv_hist['neuroinvasive'],
        name='Annual Total',
        marker_color='rgba(100, 150, 255, 0.6)',
        text=wnv_hist['neuroinvasive'],
        textposition='outside'
    ))
    
    # Add baseline reference line
    fig1.add_hline(
        y=wnv_baseline_stats['median_annual'],
        line_dash="dash",
        line_color="orange",
        annotation_text=f"Median: {wnv_baseline_stats['median_annual']:.0f}",
        annotation_position="right"
    )

fig1.update_layout(
    title=f"West Nile Virus Cases — Historical Trend (2010-2024) with {SEASON_YEAR} Context",
    xaxis_title="Year",
    yaxis_title="Neuroinvasive Cases",
    height=400,
    hovermode='x unified'
)
fig1.show()

# CHART 2: Historical Lyme Trend
fig2 = go.Figure()

if len(lyme_hist) > 0:
    fig2.add_trace(go.Scatter(
        x=lyme_hist['year'],
        y=lyme_hist['confirmed'] + lyme_hist['probable'],
        mode='lines+markers',
        name='Total Cases (Confirmed + Probable)',
        line=dict(color='red', width=2),
        marker=dict(size=8)
    ))
    
    # Baseline reference band
    fig2.add_hline(
        y=lyme_baseline_stats['median_annual'],
        line_dash="dash",
        line_color="orange",
        annotation_text=f"Median: {lyme_baseline_stats['median_annual']:.0f}",
        annotation_position="right"
    )

fig2.update_layout(
    title=f"Lyme Disease Cases — Historical Trend (2015-2024) with {SEASON_YEAR} Context",
    xaxis_title="Year",
    yaxis_title="Total Cases",
    height=400,
    hovermode='x unified'
)
fig2.show()

# CHART 3: Current Season Cumulative vs Historical Projection
if len(weekly_current) > 0:
    fig3 = make_subplots(specs=[[{"secondary_y": False}]])
    
    # Current season cumulative
    fig3.add_trace(go.Scatter(
        x=weekly_current['week'],
        y=weekly_current['wnv_cumulative'],
        mode='lines+markers',
        name=f'{SEASON_YEAR} Cumulative',
        line=dict(color='darkred', width=3),
        marker=dict(size=8)
    ))
    
    # Historical median trajectory (example: assume linear progression)
    weeks_in_season = 52
    historical_trajectory = np.linspace(0, wnv_baseline_stats['median_annual'], weeks_in_season)
    fig3.add_trace(go.Scatter(
        x=list(range(1, weeks_in_season + 1)),
        y=historical_trajectory,
        mode='lines',
        name='Historical Median Trajectory',
        line=dict(color='orange', dash='dash', width=2)
    ))
    
    fig3.update_layout(
        title=f"WNV: {SEASON_YEAR} YTD vs Historical Trajectory",
        xaxis_title="Week of Year",
        yaxis_title="Cumulative Cases",
        height=400,
        hovermode='x unified'
    )
    fig3.show()

print("✓ Visualizations generated")
✓ Visualizations generated

5. Create Current vs Historical Trend Visuals¶

# Create historical baselines by filtering to comparable time periods
# (e.g., WNV peaks Jul-Sep; Lyme peaks May-Jul)

# WNV: Focus on Jul-Sep (weeks 27-39)
wnv_baseline_stats = {
    'median_annual': wnv_hist['neuroinvasive'].median() if len(wnv_hist) > 0 else np.nan,
    'mean_annual': wnv_hist['neuroinvasive'].mean() if len(wnv_hist) > 0 else np.nan,
    'p25': wnv_hist['neuroinvasive'].quantile(0.25) if len(wnv_hist) > 0 else np.nan,
    'p75': wnv_hist['neuroinvasive'].quantile(0.75) if len(wnv_hist) > 0 else np.nan,
    'recent_5yr_avg': wnv_hist[wnv_hist['year'] >= SEASON_YEAR - 5]['neuroinvasive'].mean() if len(wnv_hist) > 0 else np.nan,
}

# Lyme: Annual totals
lyme_baseline_stats = {
    'median_annual': lyme_hist['confirmed'].median() if len(lyme_hist) > 0 else np.nan,
    'mean_annual': lyme_hist['confirmed'].mean() if len(lyme_hist) > 0 else np.nan,
    'p25': lyme_hist['confirmed'].quantile(0.25) if len(lyme_hist) > 0 else np.nan,
    'p75': lyme_hist['confirmed'].quantile(0.75) if len(lyme_hist) > 0 else np.nan,
    'recent_5yr_avg': lyme_hist[lyme_hist['year'] >= SEASON_YEAR - 5]['confirmed'].mean() if len(lyme_hist) > 0 else np.nan,
}

print("=" * 70)
print("HISTORICAL BASELINES (for context)")
print("=" * 70)

print(f"\nWNV Neuroinvasive Cases (Historical):")
print(f"  Median annual (all years): {wnv_baseline_stats['median_annual']:.0f}")
print(f"  Recent 5-year average: {wnv_baseline_stats['recent_5yr_avg']:.1f}")
print(f"  Interquartile range (25th-75th): {wnv_baseline_stats['p25']:.0f}–{wnv_baseline_stats['p75']:.0f}")

print(f"\nLyme Disease Cases (Historical):")
print(f"  Median annual (all years): {lyme_baseline_stats['median_annual']:.0f}")
print(f"  Recent 5-year average: {lyme_baseline_stats['recent_5yr_avg']:.1f}")
print(f"  Interquartile range (25th-75th): {lyme_baseline_stats['p25']:.0f}–{lyme_baseline_stats['p75']:.0f}")

# Store for later use
historical_context = {
    'wnv': wnv_baseline_stats,
    'lyme': lyme_baseline_stats,
    'wnv_history': wnv_hist.to_dict('records') if len(wnv_hist) > 0 else [],
    'lyme_history': lyme_hist.to_dict('records') if len(lyme_hist) > 0 else [],
}
======================================================================
HISTORICAL BASELINES (for context)
======================================================================

WNV Neuroinvasive Cases (Historical):
  Median annual (all years): 14
  Recent 5-year average: 10.5
  Interquartile range (25th-75th): 10–32

Lyme Disease Cases (Historical):
  Median annual (all years): 42
  Recent 5-year average: 58.8
  Interquartile range (25th-75th): 36–56

4. Compute Historical Baselines for Context¶

# Prepare current season dataset with weekly aggregation
if len(current_season_df) > 0:
    # Ensure date column is datetime
    current_season_df['date'] = pd.to_datetime(current_season_df['date'])
    current_season_df['year'] = current_season_df['date'].dt.year
    current_season_df['week'] = current_season_df['date'].dt.isocalendar().week
    
    # Calculate cumulative totals through each week
    current_season_df['wnv_cumulative'] = current_season_df['wnv_cases'].cumsum()
    current_season_df['lyme_cumulative'] = current_season_df['lyme_cases'].cumsum()
    
    weekly_current = current_season_df.copy()
else:
    # Create empty dataframe for structure
    weekly_current = pd.DataFrame({
        'week': [],
        'date': pd.Series([], dtype='datetime64[ns]'),
        'wnv_cases': [],
        'lyme_cases': [],
        'rmnp_cases': [],
        'wnv_cumulative': [],
        'lyme_cumulative': []
    })

# Add climate context to current season
if len(climate_df) > 0 and len(weekly_current) > 0:
    # Calculate weekly climate summary
    climate_df['week'] = climate_df['date'].dt.isocalendar().week
    weekly_climate = climate_df.groupby('week').agg({
        'temp_c': ['min', 'max', 'mean'],
        'precip_mm': 'sum'
    }).reset_index()
    weekly_climate.columns = ['week', 'temp_min', 'temp_max', 'temp_mean', 'precip_total']
    
    # Merge with disease data
    weekly_current = weekly_current.merge(weekly_climate, on='week', how='left')
    print(f"✓ Merged climate data with current season")

print(f"\nCurrent Season ({SEASON_YEAR}) Weekly Summary:")
print(f"  Weeks with data: {len(weekly_current)}")
print(f"  Latest week: {weekly_current['week'].max() if len(weekly_current) > 0 else 'N/A'}")
print(f"  Latest date: {weekly_current['date'].max() if len(weekly_current) > 0 else 'N/A'}")
if len(weekly_current) > 0:
    print(f"\nLatest week snapshot:")
    latest = weekly_current.iloc[-1]
    print(f"  WNV cases (week): {latest.get('wnv_cases', 0):.0f} | YTD: {latest.get('wnv_cumulative', 0):.0f}")
    print(f"  Lyme cases (week): {latest.get('lyme_cases', 0):.0f} | YTD: {latest.get('lyme_cumulative', 0):.0f}")
    if 'temp_mean' in latest:
        print(f"  Avg temp: {latest['temp_mean']:.1f}°C | Precip: {latest['precip_total']:.1f}mm")
Current Season (2026) Weekly Summary:
  Weeks with data: 2
  Latest week: 21
  Latest date: 2026-05-18 00:00:00

Latest week snapshot:
  WNV cases (week): 0 | YTD: 0
  Lyme cases (week): 2 | YTD: 2

3. Build Weekly Current-Season Dataset¶

# Load 2026 current season YTD data
season_ytd_path = os.path.join(DATA_DIR, '2026_season_ytd.json')
if os.path.exists(season_ytd_path):
    with open(season_ytd_path) as f:
        season_data = json.load(f)
    current_season_df = pd.DataFrame(season_data['data'])
    historical_baseline = season_data.get('historical_baseline_2024', {})
    print(f"✓ Loaded 2026 YTD data: {len(current_season_df)} weeks")
else:
    print(f"⚠ 2026 YTD file not found; creating empty placeholder")
    current_season_df = pd.DataFrame({
        'week': [],
        'date': [],
        'wnv_cases': [],
        'lyme_cases': [],
        'rmsf_cases': []
    })
    historical_baseline = {}

# Load historical annual totals (2015-2024 for baseline)
wnv_hist_path = os.path.join(DATA_DIR, 'wnv_colorado.json')
lyme_hist_path = os.path.join(DATA_DIR, 'lyme_colorado.json')

wnv_hist = []
lyme_hist = []

if os.path.exists(wnv_hist_path):
    with open(wnv_hist_path) as f:
        wnv_hist = pd.DataFrame(json.load(f)['data'])
        print(f"✓ Loaded WNV history: {len(wnv_hist)} years (through {wnv_hist['year'].max()})")

if os.path.exists(lyme_hist_path):
    with open(lyme_hist_path) as f:
        lyme_hist = pd.DataFrame(json.load(f)['data'])
        print(f"✓ Loaded Lyme history: {len(lyme_hist)} years (through {lyme_hist['year'].max()})")

# Load current climate data
climate_path = os.path.join(DATA_DIR, 'climate_colorado_90d.json')
if os.path.exists(climate_path):
    with open(climate_path) as f:
        climate_data_raw = json.load(f)['data']
    climate_df = pd.DataFrame(climate_data_raw)
    climate_df['date'] = pd.to_datetime(climate_df['date'].astype(str), format='%Y%m%d')
    print(f"✓ Loaded climate data: {len(climate_df)} days (through {climate_df['date'].max().date()})")
else:
    climate_df = pd.DataFrame()
    print("⚠ Climate data not found")

print(f"\nCurrent season ({SEASON_YEAR}) status:")
print(f"  YTD cases tracked: {current_season_df[['wnv_cases', 'lyme_cases']].sum().to_dict() if len(current_season_df) > 0 else 'No data yet'}")
✓ Loaded 2026 YTD data: 2 weeks
✓ Loaded WNV history: 15 years (through 2024)
✓ Loaded Lyme history: 10 years (through 2024)
✓ Loaded climate data: 88 days (through 2026-09-28)

Current season (2026) status:
  YTD cases tracked: {'wnv_cases': 0, 'lyme_cases': 2}

2. Ingest Latest In-Season Data¶

import sys
import os
import json
import warnings
from datetime import datetime, timedelta, date
from pathlib import Path

import numpy as np
import pandas as pd
import plotly.graph_objects as go
import plotly.express as px
from plotly.subplots import make_subplots

# Project imports — dynamically resolve src path (works in dev and CI/CD)
_cwd = os.getcwd()
_src_path = os.path.join(_cwd, 'src') if os.path.isdir(os.path.join(_cwd, 'src')) else os.path.abspath(os.path.join(_cwd, '..', 'src'))
if _src_path not in sys.path:
    sys.path.insert(0, _src_path)

try:
    from aedesproject_uif.data_extraction.climate.thermal_accumulation import (
        calculate_gdd, cumulative_gdd_from_start_of_year, gdd_advancement_score
    )
    print("✓ Climate modules loaded")
except ImportError as e:
    print(f"⚠ Climate modules unavailable ({e}); using inline fallbacks")
    def calculate_gdd(tmin, tmax, base_temp_c=10):
        return max(0, ((tmin + tmax) / 2) - base_temp_c)
    def cumulative_gdd_from_start_of_year(df, base_temp_c=10, start_month=3, start_day=1):
        return df.apply(lambda r: calculate_gdd(r['min_temp_c'], r['max_temp_c'], base_temp_c), axis=1).cumsum()
    def gdd_advancement_score(current_gdd, baseline_gdd, date_percentile=0):
        return min(1.0, max(0.0, (current_gdd - baseline_gdd) / max(baseline_gdd, 1)))

warnings.filterwarnings('ignore')

# ============================================================================
# SEASON PARAMETERS - DYNAMIC (no hardcoded 2024)
# ============================================================================

# Current date and season
TODAY = date.today()
CURRENT_YEAR = TODAY.year
CURRENT_WEEK = TODAY.isocalendar()[1]
CURRENT_DOY = TODAY.timetuple().tm_yday

# Season year (vector season typically starts Jan 1, peaks Jul-Sep)
SEASON_YEAR = CURRENT_YEAR
SEASON_START = datetime(SEASON_YEAR, 1, 1).date()
SEASON_END = datetime(SEASON_YEAR, 12, 31).date()

# Historical lookback for baseline context (5 years of data for percentile bands)
BASELINE_YEARS = list(range(SEASON_YEAR - 5, SEASON_YEAR))

# Paths — dynamically resolved
PROJECT_ROOT = _cwd if os.path.isdir(os.path.join(_cwd, 'src')) else os.path.abspath(os.path.join(_cwd, '..'))
DATA_DIR = os.path.join(PROJECT_ROOT, 'data', 'surveillance')
OUTPUT_DIR = os.path.join(PROJECT_ROOT, '_site', 'climate_data', 'current_season')
os.makedirs(OUTPUT_DIR, exist_ok=True)

print("=" * 70)
print("CURRENT SEASON MONITORING — DYNAMIC CONFIGURATION")
print("=" * 70)
print(f"Today: {TODAY.strftime('%Y-%m-%d')} (week {CURRENT_WEEK}, day {CURRENT_DOY})")
print(f"Current season: {SEASON_YEAR} (Jan 1 → Dec 31)")
print(f"Baseline years for context: {BASELINE_YEARS}")
print(f"Output directory: {OUTPUT_DIR}")
print("=" * 70)
✓ Climate modules loaded
======================================================================
CURRENT SEASON MONITORING — DYNAMIC CONFIGURATION
======================================================================
Today: 2026-10-01 (week 40, day 274)
Current season: 2026 (Jan 1 → Dec 31)
Baseline years for context: [2021, 2022, 2023, 2024, 2025]
Output directory: /home/runner/work/aedesproject-uif/aedesproject-uif/_site/climate_data/current_season
======================================================================
"""CSV Export: 06_current_season_monitoring"""
print(f"✓ Notebook {'06'} execution complete")
# Data exports integrated throughout notebook cells above
print("  Refer to generated CSV files: *.csv in notebooks/ directory")
✓ Notebook 06 execution complete
  Refer to generated CSV files: *.csv in notebooks/ directory

1. Dynamic Season Parameters (No Hardcoded 2024)¶

🔴 REAL-TIME: Current Season Monitoring (2026)¶

AEDES | Advanced Early Disease Prediction and Exploration Service

What People Care About: Where are we in THIS season, and what does it mean?

This notebook focuses on the current 2026 season with historical context as reference.

  • Current season tracking: Week-by-week cases, climate, vector activity
  • Historical baseline: 2015-2024 patterns for comparison (not the main story)
  • Early warning signals: When current trajectory diverges from historical norms
  • Weekly updates: Automatically advances as new CDC/climate data arrives

Last Updated: 2026-05-18
Current Week: 20 (May 18)
Season Status: Early spring → watch for tick emergence (Lyme) as GDD accelerates