covid_bokeh_11.py

text/x-python
"""
Trend Decomposition Dashboard (Bokeh)

Bokeh server app for decomposing COVID-19 death trends into components.

Run with: bokeh serve covid_bokeh_11.py
"""

from bokeh.io import curdoc
from bokeh.layouts import column, row
from bokeh.models import (ColumnDataSource, CheckboxGroup, HoverTool, 
                         Span, Label)
from bokeh.plotting import figure
from statsmodels.tsa.seasonal import STL

from covid_data_prep import (prepare_full_dataset, MAJOR_EVENTS, 
                              TRUMP_START, TRANSITION_DATE)


print("Loading data for Decomposition Dashboard...")
full_data = prepare_full_dataset()

# Filter to national data in analysis period
national = full_data[
    (full_data['state'] == 'National') &
    (full_data['date'] >= TRUMP_START) & 
    (full_data['daily_deaths'].notna())
].copy()

# Set date as index for STL
decomp_data = national.set_index('date')

# Perform two-stage STL decomposition on raw daily deaths
print("Performing STL decomposition...")

# Stage 1: Extract weekly seasonality
stl_weekly = STL(
    decomp_data['daily_deaths'],
    seasonal=7,
    period=7,
    trend=None
)
result_weekly = stl_weekly.fit()
weekly_seasonal = result_weekly.seasonal
detrended_weekly = decomp_data['daily_deaths'] - weekly_seasonal

# Stage 2: Extract annual seasonality with moderate trend
stl_annual = STL(
    detrended_weekly,
    seasonal=53,
    period=7,
    trend=91
)
result_annual = stl_annual.fit()

annual_seasonal = result_annual.seasonal
trend = result_annual.trend
residual = result_annual.resid

# Total seasonal component
total_seasonal = weekly_seasonal + annual_seasonal

# Verify reconstruction
reconstructed = trend + total_seasonal + residual
print(f"Max reconstruction error: {abs(decomp_data['daily_deaths'] - reconstructed).max():.2f}")

# Create data sources for each component
source_original = ColumnDataSource(data={
    'date': decomp_data.index,
    'value': decomp_data['daily_deaths'].values
})

source_trend = ColumnDataSource(data={
    'date': decomp_data.index,
    'value': trend.values
})

source_weekly = ColumnDataSource(data={
    'date': decomp_data.index,
    'value': weekly_seasonal.values
})

source_annual = ColumnDataSource(data={
    'date': decomp_data.index,
    'value': annual_seasonal.values
})

source_total_seasonal = ColumnDataSource(data={
    'date': decomp_data.index,
    'value': total_seasonal.values
})

source_residual = ColumnDataSource(data={
    'date': decomp_data.index,
    'value': residual.values
})

# Create figure
p = figure(
    title='RAW Daily Deaths Decomposition with Health & Political Events',
    x_axis_type='datetime',
    width=1100,
    height=550,
    tools='pan,wheel_zoom,box_zoom,reset,save'
)

# Plot all components (store references for visibility control)
line_original = p.line('date', 'value', source=source_original, 
                       line_width=1.5, color='#2c3e50', alpha=0.7, 
                       legend_label='Raw Daily Deaths', visible=True)

line_trend = p.line('date', 'value', source=source_trend, 
                    line_width=3, color='#e74c3c', alpha=0.9, 
                    legend_label='Trend (90 days)', visible=True)

line_weekly = p.line('date', 'value', source=source_weekly, 
                     line_width=1.5, color='#3498db', alpha=0.8,
                     legend_label='Weekly Pattern', visible=True)

line_annual = p.line('date', 'value', source=source_annual, 
                     line_width=2, color='#27ae60', alpha=0.8,
                     legend_label='Annual (Winter/Summer)', visible=False)

line_total_seasonal = p.line('date', 'value', source=source_total_seasonal,
                             line_width=2, color='#16a085', alpha=0.7, line_dash='dashed',
                             legend_label='Total Seasonal', visible=False)

line_residual = p.line('date', 'value', source=source_residual, 
                       line_width=1.5, color='#9b59b6', alpha=0.8,
                       legend_label='Residual', visible=True)

# Store line references for callback
lines = [line_original, line_trend, line_weekly, line_annual, line_total_seasonal, line_residual]

# Add event markers and labels
for event in MAJOR_EVENTS:
    # Style based on event type
    if event['type'] == 'political':
        line_dash = 'dotted'
        line_width = 2
    elif event['type'] == 'transition':
        line_dash = 'dashed'
        line_width = 3
    else:
        line_dash = 'dotted'
        line_width = 1.5
    
    span = Span(
        location=event['date'],
        dimension='height',
        line_color=event['color'],
        line_dash=line_dash,
        line_width=line_width,
        line_alpha=0.7
    )
    p.add_layout(span)
    
    # Add label
    label = Label(
        x=event['date'],
        y=4500,
        text=event['label'],
        text_font_size='8pt',
        text_color=event['color'],
        text_alpha=0.9,
        angle=90,
        angle_units='deg',
        text_baseline='bottom',
        text_align='left'
    )
    p.add_layout(label)

# Add zero reference line
zero_span = Span(location=0, dimension='width', line_color='gray', 
                 line_dash='solid', line_width=1, line_alpha=0.3)
p.add_layout(zero_span)

# Style the plot
p.yaxis.axis_label = 'Daily Deaths (RAW, not smoothed)'
p.xaxis.axis_label = 'Date'
p.title.text_font_size = '12pt'
p.title.align = 'center'
p.xgrid.grid_line_alpha = 0.3
p.ygrid.grid_line_alpha = 0.3

# Configure legend
p.legend.location = "top_left"
p.legend.click_policy = "hide"
p.legend.background_fill_alpha = 0.95

# Add hover tool
hover = HoverTool(
    tooltips=[
        ('Date', '@date{%F}'),
        ('Deaths', '@value{0,0}')
    ],
    formatters={'@date': 'datetime'},
    mode='vline'
)
p.add_tools(hover)

# Create checkbox widget for toggling visibility
checkbox = CheckboxGroup(
    labels=['Raw Deaths', 'Trend', 'Weekly', 'Annual', 'Total Seasonal', 'Residual'],
    active=[0, 1, 2, 5],
    width=600
)

# Python callback for visibility control
def update_visibility(attr, old, new):
    """Toggle line visibility based on checkbox selection."""
    for i, line in enumerate(lines):
        line.visible = i in checkbox.active

checkbox.on_change('active', update_visibility)

# Print residual statistics
trump_residuals = residual[residual.index < TRANSITION_DATE]
biden_residuals = residual[residual.index >= TRANSITION_DATE]

print("\n=== RESIDUAL STATISTICS ===")
print(f"Trump period: mean={trump_residuals.mean():.1f}, std={trump_residuals.std():.1f}")
print(f"Biden period: mean={biden_residuals.mean():.1f}, std={biden_residuals.std():.1f}")

# Layout
layout = column(
    p,
    row(column(checkbox, width=600))
)

# Add to document
curdoc().add_root(layout)
curdoc().title = "COVID-19 Decomposition"

print("Decomposition Dashboard ready!")
Maths.pm, par

pointcarre.app

Codes sources
Logo licence AGPLv3
Contenus
Logo licence Creative Commons