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