Skip to notebook content 🏠 Home ← Previous Tick Disease Surveillance Next →

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

  • Home
  • Notebook Hub
  • Surveillance Dashboard

Quick Links¶

  • Current Situation (Today)
  • Summary of the Analysis
  • Historical Tick-Borne Disease Case Trends (accordion)
  • 1. Seasonal Risk Calendar
  • 2. iNaturalist Tick Observations
  • 3. Early Warning Summary
"""
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.

Back to Home | Back to Top

# 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

Back to Home | Back to Top

# 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.

Back to Home | Back to Top

# 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 #

Back to Home | Back to Top

# 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