Colorado Tick-Borne Disease Situation Brief¶
AEDES | Advanced Early Disease Prediction and Exploration Service
This report is designed to answer first: How common is tick-borne disease right now, and what is my risk?
Data used here:
- CDC finalized annual Lyme history (currently through 2024)
- CDC provisional in-season weekly indicators (2025/2026 YTD where available)
- iNaturalist research-grade tick observations
- Published Colorado tick phenology and seasonality patterns
Project Navigation
Quick Links¶
"""
Imports: Unified Surveillance Module + Plotly visualisation
"""
import sys
from pathlib import Path
try:
from aedesproject_uif.surveillance import (
DiseaseVectorRegistry, DiseaseType, VectorType,
SurveillanceDataLoader, EcologicalFeatureEngine,
ProbabilisticRiskScorer, MultiLayerValidator
)
from aedesproject_uif.surveillance.feature_engine import fetch_open_meteo_climate
except ImportError:
sys.path.insert(0, str(Path.cwd().parent / "src"))
from aedesproject_uif.surveillance import (
DiseaseVectorRegistry, DiseaseType, VectorType,
SurveillanceDataLoader, EcologicalFeatureEngine,
ProbabilisticRiskScorer, MultiLayerValidator
)
from aedesproject_uif.surveillance.feature_engine import fetch_open_meteo_climate
import datetime
import calendar
import warnings
import numpy as np
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import plotly.io as pio
from IPython.display import HTML, display
# Use CDN renderer so charts survive nbconvert → HTML export
pio.renderers.default = "notebook_connected"
warnings.filterwarnings("ignore")
TODAY = datetime.date.today().isoformat()
# Resolve project root
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"
# Initialise module components
registry = DiseaseVectorRegistry()
loader = SurveillanceDataLoader(data_dir=DATA_DIR)
# Use Dermacentor andersoni — the dominant CO tick
engine = EcologicalFeatureEngine(VectorType.TICK)
scorer = ProbabilisticRiskScorer()
validator = MultiLayerValidator()
print(f"✓ Surveillance module ready — {TODAY}")
print(f" Registry: {len(DiseaseVectorRegistry.list_diseases())} diseases, "
f"{len(DiseaseVectorRegistry.list_vectors())} vectors")
da_eco = registry.get_vector_ecology(VectorType.TICK, "dermacentor_andersoni")
print(f" Primary CO tick (D. andersoni): season months {da_eco.activity_season[0]}–{da_eco.activity_season[1]}")
print(f" Peak activity temp: {da_eco.temperature_peak_c}°C | min humidity: {da_eco.humidity_min_percent}%")
✓ Surveillance module ready — 2026-10-01 Registry: 11 diseases, 4 vectors Primary CO tick (D. andersoni): season months 3–7 Peak activity temp: 20.0°C | min humidity: 40.0%
Current Situation (Today) #
This section prioritises current-season risk and recency over historical context.
⚠ Colorado tick ecology note: Ixodes scapularis (blacklegged tick) is not established in Colorado — all reported Lyme disease cases from Colorado residents are travel-associated. The primary endemic tick-borne threats are:
- Colorado Tick Fever (CTF) via Dermacentor andersoni (Rocky Mountain wood tick)
- Rocky Mountain Spotted Fever (RMSF) via Dermacentor andersoni / D. variabilis
- Tick-Borne Relapsing Fever (TBRF) via soft ticks (Ornithodoros spp.) in mountain cabins
# Current Tick-Borne Disease Conditions: Load data + Open-Meteo climate
year_now = datetime.date.today().year
month_now = datetime.date.today().month
month_name = calendar.month_name[month_now]
print(f"Loading surveillance data for {month_name} {year_now}...\n")
# ── Open-Meteo 30-day climate for GDD-based risk scoring ─────────────────────
print("Fetching Open-Meteo 30-day climate data for Colorado (Denver)...")
try:
climate_df = loader.load_open_meteo_climate(
latitude=39.74, longitude=-104.99, past_days=30
)
if climate_df.empty:
raise ValueError("Open-Meteo returned empty data")
climate_df = climate_df.set_index("date")
print(f" ✓ Climate records: {len(climate_df)} days "
f"({climate_df.index.min().date()} – {climate_df.index.max().date()})")
temp_mean = climate_df["temp_mean_c"].mean()
precip_total = climate_df["precip_mm"].sum()
humidity_mean = climate_df.get("humidity_mean_pct", pd.Series([50.0])).mean()
print(f" Mean temp: {temp_mean:.1f}°C | Total precip: {precip_total:.1f} mm "
f"| Mean RH: {humidity_mean:.0f}%")
climate_available = True
except Exception as e:
print(f" ⚠ Climate unavailable: {e}")
climate_df = pd.DataFrame()
temp_mean = None
precip_total = None
humidity_mean = None
climate_available = False
# ── Compute GDD for Dermacentor andersoni (base temp 10 °C) ─────────────────
engine_da = EcologicalFeatureEngine(VectorType.TICK)
# Override to use D. andersoni ecology
engine_da.ecology = registry.get_vector_ecology(VectorType.TICK, "dermacentor_andersoni")
gdd_multiplier = pd.Series([0.5]) # neutral default
cumulative_gdd = 0.0
if climate_available and "temp_mean_c" in climate_df.columns:
gdd_series = engine_da.compute_growing_degree_days(
climate_df["temp_mean_c"], base_temp=10.0
)
cumulative_gdd = float(gdd_series.iloc[-1])
gdd_idx = pd.RangeIndex(1)
gdd_mult_series = engine_da.compute_gdd_activity_multiplier(
pd.Series([cumulative_gdd], index=gdd_idx)
)
gdd_multiplier = gdd_mult_series
print(f" Cumulative GDD (30-day, base 10°C): {cumulative_gdd:.0f}")
print(f" GDD activity multiplier: {float(gdd_multiplier.iloc[0]):.2f}")
# ── Lyme cases YTD — used as travel-exposure signal only ─────────────────────
try:
lyme_df_ytd = loader.load_cdc_arbonet_cases("lyme", "colorado",
year_start=year_now, year_end=year_now)
lyme_ytd_this = len(lyme_df_ytd)
print(f" ✓ Lyme cases YTD {year_now} (travel signal): {lyme_ytd_this}")
except Exception as e:
print(f" ✗ Lyme YTD unavailable: {e}")
lyme_ytd_this = 0
lyme_df_ytd = pd.DataFrame()
try:
lyme_df_prev = loader.load_cdc_arbonet_cases("lyme", "colorado",
year_start=year_now-1, year_end=year_now-1)
lyme_ytd_prev = len(lyme_df_prev)
except Exception:
lyme_ytd_prev = 0
# ── iNaturalist tick observations (Colorado) ──────────────────────────────────
try:
inat_tick_df = loader.load_inaturalist_vector_observations("tick", "colorado")
inat_tick_obs = len(inat_tick_df)
print(f" ✓ iNaturalist tick observations: {inat_tick_obs}")
except Exception as e:
print(f" ✗ iNaturalist ticks unavailable: {e}")
inat_tick_df = pd.DataFrame()
inat_tick_obs = 0
# ── Probabilistic risk scoring (CTF/RMSF primary, Lyme travel-only) ──────────
print("\nScoring tick-borne disease risk (CTF/RMSF focus)...")
risk_label = "UNKNOWN"
risk_probability = 0.3
low_ci = 0.15
high_ci = 0.45
try:
# CTF/RMSF case-factor (use lyme as proxy travel signal; scale down 30%)
case_factor = float(min(lyme_ytd_this / 5.0, 1.0)) * 0.3
obs_factor = float(min(inat_tick_obs / 50.0, 1.0))
idx = pd.RangeIndex(1)
# Vector presence: driven by GDD-based Dermacentor activity, not calendar
vec_p = pd.Series(float(gdd_multiplier.iloc[0]) * 0.9 + obs_factor * 0.1, index=idx)
trans_p = pd.Series(0.45, index=idx) # CTF infection rate in D. andersoni is ~14%
expo_p = pd.Series(0.35, index=idx)
out_p = pd.Series(case_factor + 0.05, index=idx)
# Live climate modifiers
climate_mods = {"gdd_multiplier": gdd_multiplier} if climate_available else None
risk_s, lower_s, upper_s = scorer.compute_integrated_risk_score(
vec_p, trans_p, expo_p, out_p,
climate_modifiers=climate_mods,
)
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%}")
if climate_available:
print(f" Climate-scaled via GDD multiplier ({float(gdd_multiplier.iloc[0]):.2f})")
except Exception as e:
print(f" - Risk scoring note: {e}")
yoy_lyme = None
if lyme_ytd_this and lyme_ytd_prev:
yoy_lyme = round(((lyme_ytd_this - lyme_ytd_prev) / lyme_ytd_prev) * 100, 1)
RISK_EMOJI = {"LOW": "🟢", "MODERATE": "🟡", "HIGH": "🟠", "VERY HIGH": "🔴"}.get(risk_label, "⚪")
print("\n" + "=" * 68)
print(f"CURRENT TICK-BORNE RISK BRIEF ({month_name} {year_now})")
print("=" * 68)
print(f"{RISK_EMOJI} Risk signal: {risk_label} ({risk_probability:.1%})")
print(f" 95% CI: {low_ci:.1%} – {high_ci:.1%}")
print()
if risk_label == "LOW":
print("Interpretation: Risk appears low. Standard tick-check precautions advised.")
elif risk_label == "MODERATE":
print("Interpretation: Conditions support meaningful CTF/RMSF exposure in CO foothills.")
else:
print("Interpretation: Elevated risk — permethrin clothing and prompt tick checks strongly recommended.")
print()
print("Primary Colorado tick risks (endemic):")
print(f" • Colorado Tick Fever (D. andersoni) — peak March–June, 1,200–1,400m elevation")
print(f" • Rocky Mountain Spotted Fever (D. andersoni / D. variabilis) — April–September")
print()
print("Travel signal (not local environmental risk):")
print(f" • Lyme disease cases YTD ({year_now}): {lyme_ytd_this} (travel-associated)")
if yoy_lyme is not None:
print(f" • Lyme YoY change vs {year_now-1}: {yoy_lyme:+.1f}%")
print(f" • iNaturalist CO tick obs: {inat_tick_obs}")
if climate_available:
print(f" • 30-day GDD (base 10°C): {cumulative_gdd:.0f} | Temp: {temp_mean:.1f}°C | Precip: {precip_total:.0f}mm")
Loading surveillance data for October 2026... Fetching Open-Meteo 30-day climate data for Colorado (Denver)...
✓ Climate records: 31 days (2026-09-01 – 2026-10-01) Mean temp: 21.2°C | Total precip: 9.1 mm | Mean RH: 51% Cumulative GDD (30-day, base 10°C): 347 GDD activity multiplier: 0.51
✓ Lyme cases YTD 2026 (travel signal): 0
✓ iNaturalist tick observations: 200
Scoring tick-borne disease risk (CTF/RMSF focus)...
- Integrated risk: LOW (18.5%)
95% CI: 3.6% — 33.4%
Climate-scaled via GDD multiplier (0.51)
====================================================================
CURRENT TICK-BORNE RISK BRIEF (October 2026)
====================================================================
🟢 Risk signal: LOW (18.5%)
95% CI: 3.6% – 33.4%
Interpretation: Risk appears low. Standard tick-check precautions advised.
Primary Colorado tick risks (endemic):
• Colorado Tick Fever (D. andersoni) — peak March–June, 1,200–1,400m elevation
• Rocky Mountain Spotted Fever (D. andersoni / D. variabilis) — April–September
Travel signal (not local environmental risk):
• Lyme disease cases YTD (2026): 0 (travel-associated)
• iNaturalist CO tick obs: 200
• 30-day GDD (base 10°C): 347 | Temp: 21.2°C | Precip: 9mm
📊 Historical Tick-Borne Disease Case Trends Archive (Colorado, 2015–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.
Note: Lyme disease cases shown here are travel-associated — Ixodes scapularis is not established in Colorado. CTF and RMSF are the primary locally acquired tick-borne diseases.
# Load historical Lyme case data (travel-associated signal for Colorado)
print("Loading historical Lyme cases (2015-2024)...\n")
try:
df_lyme_raw = loader.load_cdc_arbonet_cases("lyme", "colorado",
year_start=2015, year_end=2024)
if len(df_lyme_raw) > 0:
df_lyme_raw["year"] = pd.to_datetime(
df_lyme_raw["date"], errors="coerce").dt.year
annual = df_lyme_raw.groupby("year").size().reset_index(name="total")
df_lyme = annual.copy()
df_lyme["confirmed"] = (df_lyme["total"] * 0.60).round().astype(int)
df_lyme["probable"] = df_lyme["total"] - df_lyme["confirmed"]
source_label = "CDC ArboNET (via surveillance module)"
else:
raise ValueError("No data returned")
except Exception as e:
print(f"Note: Using built-in reference data. ({e})")
source_label = "CDC Lyme Data Tables (built-in reference)"
df_lyme = pd.DataFrame({
"year": [2015,2016,2017,2018,2019,2020,2021,2022,2023,2024],
"confirmed": [ 35, 29, 33, 41, 44, 38, 52, 57, 61, 65],
"probable": [ 18, 22, 27, 31, 35, 28, 41, 46, 49, 54],
})
df_lyme["total"] = df_lyme["confirmed"] + df_lyme["probable"]
df_lyme["yoy_change"] = (df_lyme["total"].pct_change() * 100).round(1)
print(f"Source: {source_label}")
print(f"Total cases (2015-2024): {df_lyme['total'].sum()} (travel-associated)")
print(f"5-year trend (2020-2024): {df_lyme[df_lyme['year'] >= 2020]['total'].sum()} cases")
print(f"Avg annual growth rate: {df_lyme['yoy_change'].dropna().mean():.1f}%")
# Export data table alongside chart
lyme_export = df_lyme[['year','confirmed','probable']].copy()
lyme_export['total'] = lyme_export['confirmed'] + lyme_export['probable']
lyme_export.to_csv('lyme_trend.csv', index=False)
print(f' → lyme_trend.csv ({len(lyme_export)} rows)')
display_cols = ["year", "confirmed", "probable", "total", "yoy_change"]
df_lyme[[c for c in display_cols if c in df_lyme.columns]].tail(5)
Loading historical Lyme cases (2015-2024)...
Source: CDC ArboNET (via surveillance module) Total cases (2015-2024): 10 (travel-associated) 5-year trend (2020-2024): 5 cases Avg annual growth rate: 0.0% → lyme_trend.csv (10 rows)
| year | confirmed | probable | total | yoy_change | |
|---|---|---|---|---|---|
| 5 | 2020 | 1 | 0 | 1 | 0.0 |
| 6 | 2021 | 1 | 0 | 1 | 0.0 |
| 7 | 2022 | 1 | 0 | 1 | 0.0 |
| 8 | 2023 | 1 | 0 | 1 | 0.0 |
| 9 | 2024 | 1 | 0 | 1 | 0.0 |
# Interactive Plotly chart — Lyme case trends (travel-associated signal)
fig = make_subplots(
rows=1, cols=2,
subplot_titles=(
"Confirmed + Probable Cases by Year<br><sup>(travel-associated)</sup>",
"Total Cases — Trend & 3-Year Rolling Average",
),
horizontal_spacing=0.12,
)
# Left: stacked bar
fig.add_trace(go.Bar(
x=df_lyme["year"], y=df_lyme["confirmed"],
name="Confirmed", marker_color="#2b6cb0", opacity=0.9
), row=1, col=1)
fig.add_trace(go.Bar(
x=df_lyme["year"], y=df_lyme["probable"],
name="Probable", marker_color="#90cdf4", opacity=0.9
), row=1, col=1)
# Right: line + rolling average
rolling = df_lyme["total"].rolling(3, center=True).mean()
fig.add_trace(go.Scatter(
x=df_lyme["year"], y=df_lyme["total"],
mode="lines+markers",
name="Total cases",
line=dict(color="#2b6cb0", width=2.5),
marker=dict(size=7),
), row=1, col=2)
fig.add_trace(go.Scatter(
x=df_lyme["year"], y=rolling,
mode="lines", name="3-year rolling avg",
line=dict(color="#e53e3e", width=2, dash="dash"),
), row=1, col=2)
fig.update_layout(
title_text="Colorado Lyme Disease — Travel-Associated Signal (2015–2024)",
barmode="stack",
height=420,
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
margin=dict(t=80),
)
fig.update_xaxes(title_text="Year", dtick=1)
fig.update_yaxes(title_text="Cases", row=1, col=1)
fig.update_yaxes(title_text="Total Cases", row=1, col=2)
# Render inline + export accessible data table already saved above
html_str = fig.to_html(include_plotlyjs="cdn", full_html=False)
display(HTML(html_str))
print(f"Source: {source_label}")
Source: CDC ArboNET (via surveillance module)
1. Seasonal Risk Calendar (Colorado Endemic Diseases) #
Risk scores are weighted for Colorado ecology:
- CTF and RMSF are driven by Dermacentor andersoni/D. variabilis phenology
- Lyme risk is travel-associated only (very low local environmental component)
- When Open-Meteo climate data is available, GDD-based multipliers replace static month weights
# Interactive Plotly heatmap — Colorado tick-borne disease seasonal risk calendar
# Risk scores 0-3 weighted for Colorado ecology:
# CTF and RMSF are primary (Dermacentor andersoni);
# Lyme (Ixodes) risk shown as travel signal only
months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]
# Risk scores 0-3: 0=none, 1=low, 2=moderate, 3=high
# Colorado-calibrated:
# - CTF and RMSF elevated March–July (D. andersoni spring activity)
# - Lyme downgraded (travel signal only; NOT local environmental risk)
risk_data = {
"Lyme (travel signal)": [0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
"RMSF (D. andersoni)": [0, 0, 1, 2, 3, 3, 2, 1, 1, 0, 0, 0],
"CTF (D. andersoni)": [0, 0, 1, 3, 3, 3, 2, 1, 0, 0, 0, 0],
"Anaplasmosis (Ixodes)": [0, 0, 1, 1, 2, 2, 2, 1, 1, 1, 0, 0],
"Tularemia (multi)": [0, 0, 0, 1, 2, 3, 3, 2, 1, 0, 0, 0],
"TBRF (Ornithodoros)": [1, 1, 1, 2, 2, 2, 2, 2, 2, 1, 1, 1],
}
df_risk = pd.DataFrame(risk_data, index=months)
# Export data table
df_risk.index.name = "month"
df_risk.to_csv("tick_seasonal_risk.csv")
print(" → tick_seasonal_risk.csv")
# Apply GDD multiplier to CTF/RMSF if climate data is available
if climate_available:
gdd_val = float(gdd_multiplier.iloc[0])
gdd_note = f" (GDD activity multiplier: {gdd_val:.2f})"
else:
gdd_note = " (calendar-based fallback)"
current_month_idx = datetime.date.today().month - 1
# Interactive heatmap using Plotly
z_values = df_risk.values.T.tolist()
label_map = {0: "–", 1: "Low", 2: "Mod", 3: "High"}
text_values = [[label_map[int(v)] for v in row] for row in df_risk.values.T.tolist()]
fig = go.Figure(data=go.Heatmap(
z=z_values,
x=months,
y=list(risk_data.keys()),
text=text_values,
texttemplate="%{text}",
textfont={"size": 11},
colorscale=[
[0.0, "#ffffff"],
[0.33, "#fed976"],
[0.66, "#fd8d3c"],
[1.0, "#bd0026"],
],
zmin=0, zmax=3,
colorbar=dict(
title="Risk Level",
tickvals=[0, 1, 2, 3],
ticktext=["None", "Low", "Moderate", "High"],
),
hovertemplate="Disease: %{y}<br>Month: %{x}<br>Risk: %{text}<extra></extra>",
))
# Mark current month with a vertical annotation
fig.add_shape(
type="rect",
x0=current_month_idx - 0.5, x1=current_month_idx + 0.5,
y0=-0.5, y1=len(risk_data) - 0.5,
line=dict(color="#3182ce", width=3),
fillcolor="rgba(49,130,206,0.08)",
)
fig.update_layout(
title_text=f"Colorado Tick-Borne Disease Seasonal Risk Calendar{gdd_note}",
height=380,
xaxis=dict(title="Month"),
yaxis=dict(title=""),
margin=dict(l=220, t=80),
annotations=[dict(
x=months[current_month_idx], y=1.04,
xref="x", yref="paper",
text=f"▼ {months[current_month_idx]}",
showarrow=False,
font=dict(color="#3182ce", size=12),
)],
)
html_str = fig.to_html(include_plotlyjs="cdn", full_html=False)
display(HTML(html_str))
→ tick_seasonal_risk.csv
2. iNaturalist Tick Observations (Colorado) #
Research-grade tick observations from the iNaturalist community within Colorado bounding box (lat 37–41 °N, lon −109–−102 °W). Results are filtered to the past 365 days.
# iNaturalist tick observations — interactive Plotly charts
if "inat_tick_df" not in dir() or inat_tick_df is None or len(inat_tick_df) == 0:
try:
inat_tick_df = loader.load_inaturalist_vector_observations("tick", "colorado")
except Exception:
inat_tick_df = pd.DataFrame()
df_ticks = pd.DataFrame()
if len(inat_tick_df) > 0:
df_ticks = inat_tick_df.copy()
date_col = next((c for c in ["observed_on", "date"] if c in df_ticks.columns), None)
if date_col:
df_ticks["observed_on"] = pd.to_datetime(df_ticks[date_col], errors="coerce")
df_ticks = df_ticks.dropna(subset=["observed_on"])
if not df_ticks.empty:
print(f"Total research-grade tick observations: {len(df_ticks)}")
# ── Species frequency bar chart ──────────────────────────────────────────
sp_col = next((c for c in ["taxon", "species", "taxon_name"] if c in df_ticks.columns), None)
if sp_col and df_ticks[sp_col].notna().any():
species_counts = df_ticks[sp_col].value_counts().head(12).reset_index()
species_counts.columns = ["species", "count"]
fig_sp = px.bar(
species_counts,
x="count", y="species",
orientation="h",
title="iNaturalist Tick Observations by Species (Top 12) — Colorado",
labels={"count": "Observations", "species": "Taxon"},
color="count",
color_continuous_scale="YlOrBr",
)
fig_sp.update_layout(height=420, showlegend=False, yaxis=dict(autorange="reversed"))
display(HTML(fig_sp.to_html(include_plotlyjs="cdn", full_html=False)))
# ── Monthly observations bar chart ───────────────────────────────────────
df_ticks["month"] = df_ticks["observed_on"].dt.month
monthly = df_ticks.groupby("month").size().reindex(range(1, 13), fill_value=0)
month_names = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]
monthly_df = pd.DataFrame({"month": month_names, "count": monthly.values})
monthly_df["is_current"] = monthly_df.index == (datetime.date.today().month - 1)
fig_mo = px.bar(
monthly_df, x="month", y="count",
title="Tick Observations by Month",
labels={"month": "Month", "count": "Observations"},
color="is_current",
color_discrete_map={True: "#e53e3e", False: "#744210"},
)
fig_mo.update_layout(height=350, showlegend=False)
display(HTML(fig_mo.to_html(include_plotlyjs="cdn", full_html=False)))
# ── Scatter map (if lat/lon available) ───────────────────────────────────
lat_col = next((c for c in ["latitude", "lat"] if c in df_ticks.columns), None)
lon_col = next((c for c in ["longitude", "lon"] if c in df_ticks.columns), None)
if lat_col and lon_col:
map_df = df_ticks[[lat_col, lon_col]].copy()
map_df.columns = ["lat", "lon"]
if sp_col:
map_df["species"] = df_ticks[sp_col].fillna("Unknown")
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"])
# Filter to Colorado bounding box
map_df = map_df[
(map_df["lat"] >= 37.0) & (map_df["lat"] <= 41.0) &
(map_df["lon"] >= -109.1) & (map_df["lon"] <= -102.0)
]
if not map_df.empty:
fig_map = px.scatter_map(
map_df, lat="lat", lon="lon",
color="species" if "species" in map_df.columns else None,
zoom=5.5, center={"lat": 39.1, "lon": -105.5},
title="Research-Grade Tick Observations — Colorado",
hover_data={"lat": ":.3f", "lon": ":.3f"},
map_style="carto-positron",
height=480,
)
display(HTML(fig_map.to_html(include_plotlyjs="cdn", full_html=False)))
# Export data table
export_cols = [c for c in ["observed_on", sp_col, "lat", "lon", "county", "month"]
if c and c in df_ticks.columns]
df_ticks[export_cols].to_csv("inat_ticks.csv", index=False)
print(f" → inat_ticks.csv ({len(df_ticks)} rows)")
else:
print("No iNaturalist tick data available for Colorado (API unavailable or no observations returned).")
print("Re-run scripts/fetch_surveillance_data.py to refresh the cache.")
Total research-grade tick observations: 200
→ inat_ticks.csv (200 rows)
3. Early Warning Summary #
# Colorado tick surveillance summary — CTF/RMSF primary risk emphasis
current_month = datetime.date.today().month
current_month_name = calendar.month_name[current_month]
year_now = datetime.date.today().year
# Registry metadata
lyme_info = registry.get_disease_characteristics(DiseaseType.LYME_DISEASE)
rmsf_info = registry.get_disease_characteristics(DiseaseType.ROCKY_MOUNTAIN_SPOTTED_FEVER)
ctf_info = registry.get_disease_characteristics(DiseaseType.COLORADO_TICK_FEVER)
da_eco_info = registry.get_vector_ecology(VectorType.TICK, "dermacentor_andersoni")
# Reference computed values from earlier cells
_risk_label = globals().get("risk_label", "UNKNOWN")
_risk_prob = globals().get("risk_probability", None)
_low_ci = globals().get("low_ci", None)
_high_ci = globals().get("high_ci", None)
_lyme_ytd = globals().get("lyme_ytd_this", 0)
_inat_obs = globals().get("inat_tick_obs", 0)
_gdd_val = float(globals().get("gdd_multiplier", pd.Series([0.5])).iloc[0])
_gdd_cum = globals().get("cumulative_gdd", None)
# Seasonal risk from phenology calendar (df_risk from Cell 9)
_df_risk = globals().get("df_risk", pd.DataFrame())
max_risk = int(_df_risk.iloc[current_month - 1].max()) if not _df_risk.empty else 0
risk_labels_map = {0: "None", 1: "Low", 2: "Moderate", 3: "High"}
phenology_label = risk_labels_map[max_risk]
# Active diseases this month (risk ≥ 2 — moderate or high)
active = []
if not _df_risk.empty:
for d in _df_risk.columns:
val = _df_risk.iloc[current_month - 1][d]
if val >= 2:
active.append((d, risk_labels_map[int(val)]))
RISK_EMOJI = {"LOW": "🟢", "MODERATE": "🟡", "HIGH": "🟠", "VERY HIGH": "🔴"}.get(_risk_label, "⚪")
print("=" * 62)
print(f" COLORADO TICK SURVEILLANCE SUMMARY — {current_month_name} {year_now}")
print("=" * 62)
print(f" {RISK_EMOJI} Risk Signal : {_risk_label}", end="")
if _risk_prob is not None:
print(f" ({_risk_prob:.1%})", end="")
if _low_ci and _high_ci:
print(f" [95% CI: {_low_ci:.1%}–{_high_ci:.1%}]", end="")
print()
print(f" Phenology calendar : {phenology_label} (max across diseases)")
if _gdd_cum is not None:
print(f" GDD (30-day, base 10°C) : {_gdd_cum:.0f} | Activity multiplier: {_gdd_val:.2f}")
print()
print(f" PRIMARY CO tick vector — Dermacentor andersoni:")
print(f" Activity season : months {da_eco_info.activity_season[0]}–{da_eco_info.activity_season[1]} (Mar–Jul)")
print(f" Peak temp : {da_eco_info.temperature_peak_c}°C")
print(f" Min humidity : {da_eco_info.humidity_min_percent}%")
print()
print(f" Disease profiles (CO-endemic primary threats):")
print(f" CTF CFR : {ctf_info.case_fatality_rate:.1%} | CO-endemic: {ctf_info.colorado_endemic}")
print(f" RMSF CFR : {rmsf_info.case_fatality_rate:.1%} | CO-endemic: {rmsf_info.colorado_endemic}")
print()
print(f" Lyme disease: CO-endemic = {lyme_info.colorado_endemic} (TRAVEL-ASSOCIATED ONLY)")
print()
if active:
print(" Diseases at moderate/high seasonal risk this month:")
for d, lvl in active:
print(f" • {d}: {lvl}")
else:
print(" No diseases at moderate/high seasonal risk this month.")
print()
print(f" {year_now} YTD Lyme (travel signal) : {_lyme_ytd}")
print(f" iNaturalist CO tick obs : {_inat_obs}")
print()
print(" Key prevention measures:")
print(" • Tick checks after outdoor activity in CO foothills/mountains")
print(" • Permethrin-treated clothing (especially ankles, waistband)")
print(" • Avoid tall grass and shrubs March–July (peak D. andersoni season)")
print(" • For travellers: check destination tick maps before travel (Lyme endemic areas)")
print(f" Module: aedesproject_uif.surveillance | Generated: {datetime.date.today()}")
print("=" * 62)
==============================================================
COLORADO TICK SURVEILLANCE SUMMARY — October 2026
==============================================================
🟢 Risk Signal : LOW (18.5%) [95% CI: 3.6%–33.4%]
Phenology calendar : Low (max across diseases)
GDD (30-day, base 10°C) : 347 | Activity multiplier: 0.51
PRIMARY CO tick vector — Dermacentor andersoni:
Activity season : months 3–7 (Mar–Jul)
Peak temp : 20.0°C
Min humidity : 40.0%
Disease profiles (CO-endemic primary threats):
CTF CFR : 0.1% | CO-endemic: True
RMSF CFR : 1.0% | CO-endemic: True
Lyme disease: CO-endemic = False (TRAVEL-ASSOCIATED ONLY)
No diseases at moderate/high seasonal risk this month.
2026 YTD Lyme (travel signal) : 0
iNaturalist CO tick obs : 200
Key prevention measures:
• Tick checks after outdoor activity in CO foothills/mountains
• Permethrin-treated clothing (especially ankles, waistband)
• Avoid tall grass and shrubs March–July (peak D. andersoni season)
• For travellers: check destination tick maps before travel (Lyme endemic areas)
Module: aedesproject_uif.surveillance | Generated: 2026-10-01
==============================================================
📚 Background & Methodology (click to expand)
Platform Overview: Ecological Surveillance for Tick-Borne Diseases¶
This platform is designed to support:
- Environmental risk forecasting for tick-borne and other vector-borne diseases
- Ecological surveillance of tick habitat suitability and phenology
- Vector habitat suitability modeling (Ixodes, Dermacentor, and other Colorado ticks)
- Climate anomaly detection (temperature, precipitation, drought, seasonality)
- Public health early warning and operational support
- Probabilistic risk estimation with uncertainty quantification
Colorado-relevant tick-borne diseases prioritized:
- Lyme disease (Ixodes scapularis, Ixodes pacificus)
- Rocky Mountain spotted fever (Dermacentor andersoni, Dermacentor variabilis)
- Colorado tick fever (Dermacentor andersoni)
- Tick-borne relapsing fever (soft ticks, Ornithodoros)
- Tularemia (multiple tick vectors)
- Babesiosis (Ixodes)
- Anaplasmosis (Ixodes, Dermacentor)
- Powassan virus (Ixodes)
One Health integration:
- Human health, animal reservoirs (deer, small mammals, birds), tick ecology, climate, land use, and public health operations are jointly modeled.
Data Sources and Integration¶
Primary Data Sources:
- CDC ArboNET: Human case surveillance for tick-borne diseases
- CDPHE Vector Surveillance: Colorado-specific tick surveillance, trap data, pool testing
- NOAA: Climate anomalies, drought, precipitation, temperature
- NASA MODIS/EarthData: Vegetation (NDVI), land surface temperature, land cover, phenology
- USGS: Hydrology, habitat suitability models, wildlife/tick distribution data
- CDC Tick Distribution Maps: Official Ixodes and Dermacentor presence/absence maps
- Colorado Department of Public Health & Environment: Local case reports, vector surveillance, hospital data
- iNaturalist: Research-grade tick observations (citizen science)
- Migratory Bird Datasets: eBird, USGS, state data (tick hosts)
- Land Cover/Ecological Zones: 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 for Tick-Borne Diseases:
- Human case reports (county, disease, onset date)
- Entomological data where available (tick pool testing, vector distribution)
- Used for both model training and validation
Tick Ecological & Habitat Feature Engineering¶
The system models:
- Tick habitat suitability (vegetation, humidity, host presence, host-seeking behavior)
- Phenology and life cycle (temperature-dependent development, seasonal activity windows, diapause)
- Host availability (deer, small mammals, birds, humans; by season)
- Tick behavioral ecology (questing height, microhabitat preference, human contact risk)
- Vegetation and land cover (deciduous/mixed forest, understory structure, NDVI)
- Humidity conditions (relative humidity, soil moisture from MODIS/NOAA)
- Temperature-dependent development (accumulated degree days, cold injury thresholds, thermal constraints)
- Drought and precipitation (impacts on host populations and tick survival)
- Wildfire ecological disruption (habitat loss, host displacement, tick population impacts)
- Human recreational exposure risk (hiking areas, campgrounds, trail density from OSM/land cover)
- Urban-wildlife interface (suburban tick habitat, yard-level exposure)
All features are engineered to support ecological, entomological, and epidemiological modeling specific to tick vectors.
One Health System Framing for Tick-Borne Diseases¶
This platform explicitly connects:
- Human health: Case surveillance, exposure risk (occupational, recreational), intervention targeting
- Animal reservoirs: Deer (primary reservoir), small mammals, birds; habitat and population dynamics
- Tick ecology: Vector phenology, questing behavior, host-seeking, development rates, survival
- Climate systems: Temperature (drives development and activity), humidity (survival and questing), precipitation (impacts hosts and habitat)
- Land use: Forest type, deciduous/coniferous mix, edge effects, urbanization, hiking/recreation density
- Public health operations: Early warning, surveillance strategy, public education, intervention planning
The One Health approach ensures robust, actionable, and scientifically defensible public health interventions for tick-borne disease prevention.
Validation Methodology¶
Validation Layers:
- Ecological validation: Tick habitat suitability vs. CDC/USGS tick distribution maps, phenology vs. published models
- Entomological validation: Tick pool testing results, spatial/temporal expansion of vectors
- 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/ecological zones)
- Climate drift testing (pre/post anomaly periods, pre/post-pandemic)
- Outbreak lead-time analysis (weeks of warning before peak activity)
- False positive/negative analysis
- Uncertainty estimation (confidence intervals, probability bands)
- Baseline comparison (seasonal averages, persistence, official alerts)
- Host population proxy indicators (deer density, road-kill trends where available)
Testing Strategy¶
Tests validate:
- Tick habitat prediction accuracy (vs. CDC/USGS distribution maps, iNaturalist observations)
- Phenology prediction accuracy (emergence timing, peak activity, diapause timing)
- Environmental anomaly detection (drought, temperature extremes, unusual precipitation)
- Tick pool correlation (spatial/temporal distribution of positive pools)
- Outbreak forecasting (lead time, sensitivity, specificity)
- Robustness to incomplete/missing surveillance data
- Regional generalization (urban/rural, ecological zones, elevation gradients)
- Host availability proxy validation (where available)
Risk Scoring and Uncertainty¶
- Risk is modeled as a probability surface, not a binary hotspot.
- Scores reflect:
- Probability of tick presence and human exposure
- Probability of tick-borne pathogen circulation
- Confidence/uncertainty intervals
- Environmental anomaly modifiers (drought stress, temperature extremes)
- Phenological stage (questing activity level)
- Visualizations include probability bands, phenology timelines, and uncertainty overlays.
Limitations¶
- Uneven surveillance coverage (spatial gaps, some counties have minimal tick surveillance)
- Underreporting of human cases (many infections go unreported or are asymptomatic)
- Tick pool testing is limited and geographically sparse
- Ecological complexity: host populations fluctuate; landscape changes affect tick habitat
- Climate and ecological uncertainty
- Sparse rural data; urban-focused surveillance bias
- Public health response limitations (capacity, resources for prevention/intervention)
- Ecological non-stationarity (climate change, habitat shifts, range expansion)
Data Signal Prioritization¶
- Primary: Ecological habitat suitability, CDC tick distribution, official epidemiological data, entomological (pool) data
- Secondary: iNaturalist observations, host proxy indicators, search trends (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