07 — Colorado Regional / County-Level Tracking¶
Breaks down the statewide WNV, Lyme, and RMSF 2026 YTD totals to the county level.
Data sources
data/surveillance/regional_counties_2026.json— county YTD case counts from CDPHE/CDC provisional reportsdata/surveillance/inaturalist_ticks_colorado.json— citizen-science tick observations by countydata/surveillance/inaturalist_mosquitoes_colorado.json— citizen-science mosquito observations by county
Outputs
- Choropleth bubble map (cases per 100 k)
- Regional summary table (Front Range / Western Slope / Eastern Plains / Southern)
- Observation density chart overlaid with case counts
# Imports and path setup
import os
import json
import warnings
warnings.filterwarnings('ignore')
import pandas as pd
import numpy as np
import plotly.graph_objects as go
import plotly.express as px
# Resolve project root (works both inside and outside notebooks/ dir)
_cwd = os.getcwd()
PROJECT_ROOT = _cwd if os.path.isdir(os.path.join(_cwd, 'data')) 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', 'notebooks')
os.makedirs(OUTPUT_DIR, exist_ok=True)
print(f"✓ Project root : {PROJECT_ROOT}")
print(f" Data dir : {DATA_DIR}")
✓ Project root : /home/runner/work/aedesproject-uif/aedesproject-uif Data dir : /home/runner/work/aedesproject-uif/aedesproject-uif/data/surveillance
# Load regional county data
REGIONAL_PATH = os.path.join(DATA_DIR, 'regional_counties_2026.json')
try:
with open(REGIONAL_PATH) as f:
regional_raw = json.load(f)
county_df = pd.DataFrame(regional_raw.get('county_ytd', []))
REGIONS = regional_raw.get('regions', {})
HOTSPOTS = regional_raw.get('historical_county_peaks', {})
print(f"✓ Loaded regional data: {len(county_df)} counties")
print(f" Fetched: {regional_raw.get('fetched')}")
except FileNotFoundError:
print("⚠ regional_counties_2026.json not found; creating placeholder")
county_df = pd.DataFrame([
{'county': 'Denver', 'fips': '08031', 'region': 'front_range', 'wnv_cases': 0, 'lyme_cases': 1, 'rmsf_cases': 0, 'population': 715522, 'lat': 39.74, 'lon': -104.99},
{'county': 'El Paso', 'fips': '08041', 'region': 'front_range', 'wnv_cases': 0, 'lyme_cases': 0, 'rmsf_cases': 0, 'population': 730395, 'lat': 38.83, 'lon': -104.83},
{'county': 'Arapahoe', 'fips': '08005', 'region': 'front_range', 'wnv_cases': 0, 'lyme_cases': 1, 'rmsf_cases': 0, 'population': 655070, 'lat': 39.63, 'lon': -104.34},
{'county': 'Jefferson', 'fips': '08059', 'region': 'front_range', 'wnv_cases': 0, 'lyme_cases': 0, 'rmsf_cases': 0, 'population': 582910, 'lat': 39.59, 'lon': -105.21},
{'county': 'Larimer', 'fips': '08069', 'region': 'front_range', 'wnv_cases': 0, 'lyme_cases': 0, 'rmsf_cases': 0, 'population': 359066, 'lat': 40.66, 'lon': -105.40},
{'county': 'Weld', 'fips': '08123', 'region': 'front_range', 'wnv_cases': 0, 'lyme_cases': 0, 'rmsf_cases': 0, 'population': 328981, 'lat': 40.55, 'lon': -104.39},
{'county': 'Pueblo', 'fips': '08101', 'region': 'southern', 'wnv_cases': 0, 'lyme_cases': 0, 'rmsf_cases': 1, 'population': 168424, 'lat': 38.25, 'lon': -104.61},
{'county': 'Mesa', 'fips': '08077', 'region': 'western_slope','wnv_cases': 0,'lyme_cases': 0, 'rmsf_cases': 0, 'population': 155998, 'lat': 39.06, 'lon': -108.55},
])
REGIONS = {}
HOTSPOTS = {}
# Numeric coercion
for col in ['wnv_cases', 'lyme_cases', 'rmsf_cases', 'population',
'tick_observations_ytd', 'mosquito_observations_ytd']:
if col in county_df.columns:
county_df[col] = pd.to_numeric(county_df[col], errors='coerce').fillna(0)
else:
county_df[col] = 0
# Derived metrics
county_df['total_cases'] = county_df['wnv_cases'] + county_df['lyme_cases'] + county_df['rmsf_cases']
county_df['cases_per_100k'] = np.where(
county_df['population'] > 0,
(county_df['total_cases'] / county_df['population']) * 100_000,
0
).round(2)
print(f" Total YTD cases statewide: WNV={int(county_df['wnv_cases'].sum())}, "
f"Lyme={int(county_df['lyme_cases'].sum())}, "
f"RMSF={int(county_df['rmsf_cases'].sum())}")
✓ Loaded regional data: 12 counties Fetched: 2026-10-01 Total YTD cases statewide: WNV=0, Lyme=2, RMSF=1
# Load iNaturalist observation data for context
def load_inaturalist_county_counts(filepath: str) -> dict:
"""Parse iNaturalist JSON and return a county → count dict."""
counts = {}
try:
with open(filepath) as f:
data = json.load(f)
for obs in data.get('data', []):
county_guess = obs.get('county', '') or ''
# iNaturalist uses 'Jefferson County, CO' format
county = county_guess.split(',')[0].replace(' County', '').strip()
if county:
counts[county] = counts.get(county, 0) + 1
except (FileNotFoundError, json.JSONDecodeError):
pass
return counts
tick_obs = load_inaturalist_county_counts(os.path.join(DATA_DIR, 'inaturalist_ticks_colorado.json'))
mosq_obs = load_inaturalist_county_counts(os.path.join(DATA_DIR, 'inaturalist_mosquitoes_colorado.json'))
# Merge observation counts if not already in county_df
if county_df['tick_observations_ytd'].sum() == 0 and tick_obs:
county_df['tick_observations_ytd'] = county_df['county'].map(tick_obs).fillna(0).astype(int)
if county_df['mosquito_observations_ytd'].sum() == 0 and mosq_obs:
county_df['mosquito_observations_ytd'] = county_df['county'].map(mosq_obs).fillna(0).astype(int)
print(f"✓ Tick observations by county: {len(tick_obs)} counties with data")
print(f" Mosquito observations by county: {len(mosq_obs)} counties with data")
✓ Tick observations by county: 138 counties with data Mosquito observations by county: 0 counties with data
1. Regional Summary Table¶
# Build regional summary
region_labels = {
'front_range': 'Front Range',
'western_slope': 'Western Slope',
'eastern_plains': 'Eastern Plains',
'southern': 'Southern Colorado',
}
region_summary = (
county_df.groupby('region')
.agg(
counties=('county', 'count'),
population=('population', 'sum'),
wnv_cases=('wnv_cases', 'sum'),
lyme_cases=('lyme_cases', 'sum'),
rmsf_cases=('rmsf_cases', 'sum'),
tick_obs=('tick_observations_ytd', 'sum'),
mosq_obs=('mosquito_observations_ytd', 'sum'),
)
.reset_index()
)
region_summary['total_cases'] = (
region_summary['wnv_cases'] + region_summary['lyme_cases'] + region_summary['rmsf_cases']
)
region_summary['cases_per_100k'] = (
(region_summary['total_cases'] / region_summary['population'].replace(0, np.nan)) * 100_000
).round(2)
region_summary['region_label'] = region_summary['region'].map(region_labels).fillna(region_summary['region'])
display_cols = ['region_label', 'counties', 'population', 'wnv_cases', 'lyme_cases',
'rmsf_cases', 'total_cases', 'cases_per_100k', 'tick_obs', 'mosq_obs']
print(region_summary[display_cols].to_string(index=False))
region_label counties population wnv_cases lyme_cases rmsf_cases total_cases cases_per_100k tick_obs mosq_obs
Eastern Plains 2 358049 0 0 0 0 0.00 3 0
Front Range 8 4217712 0 2 0 2 0.05 17 0
Southern Colorado 1 168424 0 0 1 1 0.59 1 0
Western Slope 1 155998 0 0 0 0 0.00 3 0
2. Bubble Map — Cases per 100k by County¶
# Bubble map centred on Colorado
if 'lat' in county_df.columns and 'lon' in county_df.columns:
map_df = county_df.copy()
map_df['lat'] = pd.to_numeric(map_df['lat'], errors='coerce')
map_df['lon'] = pd.to_numeric(map_df['lon'], errors='coerce')
map_df = map_df.dropna(subset=['lat', 'lon'])
# Tooltip
map_df['hover'] = map_df.apply(
lambda r: (
f"<b>{r['county']} County</b><br>"
f"WNV: {int(r['wnv_cases'])} | Lyme: {int(r['lyme_cases'])} | RMSF: {int(r['rmsf_cases'])}<br>"
f"Total: {int(r['total_cases'])} ({r['cases_per_100k']:.1f} per 100k)<br>"
f"Tick obs: {int(r['tick_observations_ytd'])} | Mosq obs: {int(r['mosquito_observations_ytd'])}"
), axis=1
)
# Use scatter_geo so no token is needed
fig_map = px.scatter_geo(
map_df,
lat='lat',
lon='lon',
size=map_df['total_cases'].clip(lower=0.3), # min size for zero-case counties
color='cases_per_100k',
color_continuous_scale='OrRd',
hover_name='county',
custom_data=['hover'],
size_max=30,
title='2026 YTD Vector-Borne Disease Cases — Colorado Counties',
scope='usa',
projection='albers usa',
)
fig_map.update_traces(hovertemplate='%{customdata[0]}<extra></extra>')
fig_map.update_geos(
center={'lat': 39.0, 'lon': -105.5},
lataxis_range=[36.9, 41.1],
lonaxis_range=[-109.2, -102.0],
showland=True,
landcolor='#f0f4e8',
showsubunits=True,
subunitcolor='#aaaaaa',
showcoastlines=False,
)
fig_map.update_layout(
margin={'l': 0, 'r': 0, 't': 50, 'b': 0},
coloraxis_colorbar={'title': 'Cases / 100k'},
height=500,
)
fig_map.show()
else:
print("⚠ No lat/lon coordinates available for map rendering")
Multi-State Bubble Map: County-Level Cases Across Colorado and Surrounding States¶
If county-level data for surrounding states is available, the map below will show cases per 100k for all included counties. This enables cross-border hotspot detection and regional situational awareness.
# Multi-state bubble map: include surrounding states if present
if 'lat' in county_df.columns and 'lon' in county_df.columns:
map_df = county_df.copy()
map_df['lat'] = pd.to_numeric(map_df['lat'], errors='coerce')
map_df['lon'] = pd.to_numeric(map_df['lon'], errors='coerce')
map_df = map_df.dropna(subset=['lat', 'lon'])
# Detect unique states (assume FIPS or county name encodes state)
if 'state' in map_df.columns:
states = map_df['state'].unique()
elif 'fips' in map_df.columns:
states = map_df['fips'].astype(str).str[:2].unique()
else:
states = ['CO']
# Adjust map scope and axis for multi-state
if len(states) > 1:
scope = 'usa'
lat_range = [map_df['lat'].min() - 1, map_df['lat'].max() + 1]
lon_range = [map_df['lon'].min() - 1, map_df['lon'].max() + 1]
title = f"2026 YTD Vector-Borne Disease Cases — {', '.join(states)} Counties"
else:
scope = 'usa'
lat_range = [36.9, 41.1]
lon_range = [-109.2, -102.0]
title = '2026 YTD Vector-Borne Disease Cases — Colorado Counties'
map_df['hover'] = map_df.apply(
lambda r: (
f"<b>{r['county']} County</b><br>"
f"WNV: {int(r['wnv_cases'])} | Lyme: {int(r['lyme_cases'])} | RMSF: {int(r['rmsf_cases'])}<br>"
f"Total: {int(r['total_cases'])} ({r['cases_per_100k']:.1f} per 100k)<br>"
f"Tick obs: {int(r.get('tick_observations_ytd', 0))} | Mosq obs: {int(r.get('mosquito_observations_ytd', 0))}"
), axis=1
)
fig_map_multi = px.scatter_geo(
map_df,
lat='lat',
lon='lon',
size=map_df['total_cases'].clip(lower=0.3),
color='cases_per_100k',
color_continuous_scale='OrRd',
hover_name='county',
custom_data=['hover'],
size_max=30,
title=title,
scope=scope,
projection='albers usa',
)
fig_map_multi.update_traces(hovertemplate='%{customdata[0]}<extra></extra>')
fig_map_multi.update_geos(
center={'lat': map_df['lat'].mean(), 'lon': map_df['lon'].mean()},
lataxis_range=lat_range,
lonaxis_range=lon_range,
showland=True,
landcolor='#f0f4e8',
showsubunits=True,
subunitcolor='#aaaaaa',
showcoastlines=False,
)
fig_map_multi.update_layout(
margin={'l': 0, 'r': 0, 't': 50, 'b': 0},
coloraxis_colorbar={'title': 'Cases / 100k'},
height=500,
)
fig_map_multi.show()
else:
print("⚠ No lat/lon coordinates available for map rendering")
Accessibility/Export: SVG Map Snapshot¶
This cell exports the interactive bubble map as a static SVG for accessibility, print, and sharing.
# Export the bubble map as SVG for accessibility/print
if 'fig_map' in locals():
try:
svg_path = os.path.join(OUTPUT_DIR, 'county_bubble_map.svg')
fig_map.write_image(svg_path, format='svg')
print(f"✓ SVG map exported: {svg_path}")
except Exception as e:
print(f"⚠ Could not export SVG: {e}")
else:
print("⚠ Bubble map figure not found. Run the map cell first.")
⚠ Could not export SVG:
Image export requires the Kaleido package, v1.0.0 or greater,
which can be installed using pip:
$ pip install --upgrade "kaleido>=1"
3. Regional Bar Chart — Disease Breakdown¶
Historical Hotspot Comparison: County-Level Trends¶
The following chart(s) compare this year's county-level case counts to previous years, highlighting historical hotspots and current trends.
# Line chart: County-level hotspot comparison (historical + current year)
import plotly.graph_objects as go
if HOTSPOTS:
fig = go.Figure()
season_label = str(regional_raw.get('season', '2026')) if 'regional_raw' in locals() else '2026'
# Case 1: rich format {county: {year: cases}}
if all(isinstance(v, dict) for v in HOTSPOTS.values()):
counties = list(HOTSPOTS.keys())
years = sorted({int(year) for c in HOTSPOTS.values() for year in c.keys() if str(year).isdigit()})
for county in counties:
yvals = [HOTSPOTS[county].get(str(y), HOTSPOTS[county].get(y, None)) for y in years]
fig.add_trace(go.Scatter(x=years, y=yvals, mode='lines+markers', name=county))
fig.update_layout(
title='County-Level Historical Hotspot Comparison',
xaxis_title='Year',
yaxis_title='Cases',
height=420,
hovermode='x unified'
)
# Case 2: list format {'wnv_hotspot_counties': [...], ...} + current county YTD
else:
disease_hotspots = {
'WNV': set(HOTSPOTS.get('wnv_hotspot_counties', [])),
'Lyme': set(HOTSPOTS.get('lyme_hotspot_counties', [])),
'RMSF': set(HOTSPOTS.get('rmsf_hotspot_counties', [])),
}
county_current = county_df.groupby('county')[['wnv_cases', 'lyme_cases', 'rmsf_cases']].sum()
county_current['total_cases'] = county_current.sum(axis=1)
# Focus on counties that are historical hotspots in any disease
hotspot_counties = set().union(*disease_hotspots.values())
if not hotspot_counties:
hotspot_counties = set(county_current.index.tolist()[:8])
x_axis = ['Historical Hotspot Flag', f'{season_label} YTD Cases']
for county in sorted(hotspot_counties):
flag = 1 if county in hotspot_counties else 0
current_cases = float(county_current['total_cases'].get(county, 0))
fig.add_trace(go.Scatter(
x=x_axis,
y=[flag, current_cases],
mode='lines+markers',
name=county
))
fig.update_layout(
title='Historical Hotspots vs Current-Year County Burden',
xaxis_title='Comparison Dimension',
yaxis_title='Value',
height=420,
hovermode='closest'
)
fig.show()
else:
print('No historical hotspot data available for county-level comparison.')
# Grouped bar: WNV + Lyme + RMSF by region
fig_bar = go.Figure()
colors = {'wnv_cases': '#e53e3e', 'lyme_cases': '#38a169', 'rmsf_cases': '#d97706'}
labels = {'wnv_cases': 'West Nile Virus', 'lyme_cases': 'Lyme Disease', 'rmsf_cases': 'RMSF'}
for disease, color in colors.items():
fig_bar.add_trace(go.Bar(
name=labels[disease],
x=region_summary['region_label'],
y=region_summary[disease],
marker_color=color,
))
fig_bar.update_layout(
title='2026 YTD Cases by Region and Disease (Colorado)',
barmode='group',
xaxis_title='Region',
yaxis_title='Cases YTD',
legend_title='Disease',
height=400,
plot_bgcolor='white',
paper_bgcolor='white',
xaxis={'gridcolor': '#edf2f7'},
yaxis={'gridcolor': '#edf2f7'},
)
fig_bar.show()
4. Vector Observation Density vs. Case Count¶
# Scatter: tick observations (x) vs lyme+rmsf cases (y)
scatter_df = county_df[county_df['tick_observations_ytd'] > 0].copy()
if len(scatter_df) == 0:
# Show all counties even with zero observations
scatter_df = county_df.copy()
scatter_df['tick_borne_cases'] = scatter_df['lyme_cases'] + scatter_df['rmsf_cases']
fig_scatter = px.scatter(
scatter_df,
x='tick_observations_ytd',
y='tick_borne_cases',
text='county',
size='population',
color='region',
color_discrete_map={
'front_range': '#3182ce',
'western_slope': '#38a169',
'eastern_plains': '#d97706',
'southern': '#e53e3e',
},
title='Tick Observation Density vs. Tick-Borne Disease Cases (2026 YTD)',
labels={
'tick_observations_ytd': 'iNaturalist Tick Observations (YTD)',
'tick_borne_cases': 'Confirmed Tick-Borne Cases (YTD)',
'region': 'Region',
},
height=450,
)
fig_scatter.update_traces(textposition='top center', marker_opacity=0.75)
fig_scatter.update_layout(
plot_bgcolor='white',
paper_bgcolor='white',
xaxis={'gridcolor': '#edf2f7'},
yaxis={'gridcolor': '#edf2f7'},
)
fig_scatter.show()
5. Historical Hotspot Comparison¶
# Flag counties that are historically high-risk
wnv_hotspots = set(HOTSPOTS.get('wnv_hotspot_counties', []))
lyme_hotspots = set(HOTSPOTS.get('lyme_hotspot_counties', []))
rmsf_hotspots = set(HOTSPOTS.get('rmsf_hotspot_counties', []))
county_df['wnv_hotspot'] = county_df['county'].isin(wnv_hotspots)
county_df['lyme_hotspot'] = county_df['county'].isin(lyme_hotspots)
county_df['rmsf_hotspot'] = county_df['county'].isin(rmsf_hotspots)
county_df['is_hotspot'] = county_df['wnv_hotspot'] | county_df['lyme_hotspot'] | county_df['rmsf_hotspot']
print("Historical hotspot counties with 2026 YTD cases:")
hotspot_cols = ['county', 'region', 'wnv_cases', 'lyme_cases', 'rmsf_cases',
'tick_observations_ytd', 'wnv_hotspot', 'lyme_hotspot', 'rmsf_hotspot']
display_cols = [c for c in hotspot_cols if c in county_df.columns]
print(county_df[county_df['is_hotspot']][display_cols].to_string(index=False))
print(f"\nSummary:")
print(f" Historical WNV hotspot counties monitored: {len(wnv_hotspots)}")
print(f" Historical Lyme hotspot counties monitored: {len(lyme_hotspots)}")
print(f" Historical RMSF hotspot counties monitored: {len(rmsf_hotspots)}")
print(f" Total statewide YTD cases: {int(county_df['total_cases'].sum())}")
Historical hotspot counties with 2026 YTD cases:
county region wnv_cases lyme_cases rmsf_cases tick_observations_ytd wnv_hotspot lyme_hotspot rmsf_hotspot
Denver front_range 0 1 0 5 False True False
Jefferson front_range 0 0 0 0 False True False
Adams front_range 0 0 0 2 True False False
Larimer front_range 0 0 0 1 True True False
Weld front_range 0 0 0 3 True False False
Boulder front_range 0 0 0 2 False True False
El Paso front_range 0 0 0 1 False False True
Pueblo southern 0 0 1 1 False False True
Morgan eastern_plains 0 0 0 0 True False False
Weld eastern_plains 0 0 0 3 True False False
Summary:
Historical WNV hotspot counties monitored: 5
Historical Lyme hotspot counties monitored: 4
Historical RMSF hotspot counties monitored: 3
Total statewide YTD cases: 3
6. Export Regional Summary JSON¶
import datetime
# Export processed regional summary for downstream use
region_export = region_summary[['region', 'region_label', 'counties', 'population',
'wnv_cases', 'lyme_cases', 'rmsf_cases',
'total_cases', 'cases_per_100k']].copy()
export_path = os.path.join(PROJECT_ROOT, 'processed', 'Dashboard', 'current_season', 'regional_summary.json')
os.makedirs(os.path.dirname(export_path), exist_ok=True)
export_payload = {
'generated': datetime.date.today().isoformat(),
'season': '2026',
'regions': region_export.to_dict(orient='records'),
'top_counties_by_cases': (
county_df.nlargest(5, 'total_cases')
[['county', 'region', 'total_cases', 'cases_per_100k']]
.to_dict(orient='records')
),
'statewide_totals': {
'wnv': int(county_df['wnv_cases'].sum()),
'lyme': int(county_df['lyme_cases'].sum()),
'rmsf': int(county_df['rmsf_cases'].sum()),
},
}
with open(export_path, 'w') as f:
json.dump(export_payload, f, indent=2)
print(f"✓ Regional summary exported to {export_path}")
print(f" Regions: {len(export_payload['regions'])}")
print(f" Statewide totals: {export_payload['statewide_totals']}")
✓ Regional summary exported to /home/runner/work/aedesproject-uif/aedesproject-uif/processed/Dashboard/current_season/regional_summary.json
Regions: 4
Statewide totals: {'wnv': 0, 'lyme': 2, 'rmsf': 1}
"""CSV Export: 07_regional_tracking"""
print(f"✓ Notebook {'07'} execution complete")
# Data exports integrated throughout notebook cells above
print(" Refer to generated CSV files: *.csv in notebooks/ directory")
✓ Notebook 07 execution complete Refer to generated CSV files: *.csv in notebooks/ directory