Upload 6 files
Browse files- components/behavioral_calib.py +181 -0
- components/geo_map.py +163 -0
- components/incentive_analytics.py +170 -0
- components/realtime_metrics.py +197 -0
- components/simulation_compare.py +168 -0
- components/traffic_flow.py +183 -0
components/behavioral_calib.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Behavioral calibration visualization component.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import pandas as pd
|
| 6 |
+
import plotly.express as px
|
| 7 |
+
import plotly.graph_objects as go
|
| 8 |
+
|
| 9 |
+
from database import query
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def get_elasticity_data() -> pd.DataFrame:
|
| 13 |
+
"""Get incentive elasticity curve data."""
|
| 14 |
+
sql = """
|
| 15 |
+
SELECT
|
| 16 |
+
incentive_bucket,
|
| 17 |
+
n_trips,
|
| 18 |
+
carpool_rate,
|
| 19 |
+
avg_incentive
|
| 20 |
+
FROM main_marts.metrics_elasticity
|
| 21 |
+
ORDER BY avg_incentive
|
| 22 |
+
"""
|
| 23 |
+
return query(sql)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def create_elasticity_curve(df: pd.DataFrame) -> go.Figure:
|
| 27 |
+
"""Create elasticity curve visualization."""
|
| 28 |
+
if df.empty:
|
| 29 |
+
return go.Figure().add_annotation(text="No data available", showarrow=False)
|
| 30 |
+
|
| 31 |
+
fig = go.Figure()
|
| 32 |
+
|
| 33 |
+
# Main line
|
| 34 |
+
fig.add_trace(go.Scatter(
|
| 35 |
+
x=df['avg_incentive'],
|
| 36 |
+
y=df['carpool_rate'],
|
| 37 |
+
mode='lines+markers',
|
| 38 |
+
name='Carpool Rate',
|
| 39 |
+
line=dict(color='#3498db', width=3),
|
| 40 |
+
marker=dict(size=10),
|
| 41 |
+
hovertemplate='Incentive: $%{x:.2f}<br>Rate: %{y:.1%}<extra></extra>'
|
| 42 |
+
))
|
| 43 |
+
|
| 44 |
+
# Add annotations for buckets
|
| 45 |
+
for _, row in df.iterrows():
|
| 46 |
+
fig.add_annotation(
|
| 47 |
+
x=row['avg_incentive'],
|
| 48 |
+
y=row['carpool_rate'],
|
| 49 |
+
text=row['incentive_bucket'],
|
| 50 |
+
showarrow=True,
|
| 51 |
+
arrowhead=2,
|
| 52 |
+
ax=0,
|
| 53 |
+
ay=-30
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
fig.update_layout(
|
| 57 |
+
title='Incentive Elasticity Curve',
|
| 58 |
+
xaxis_title='Average Incentive ($)',
|
| 59 |
+
yaxis_title='Carpool Participation Rate',
|
| 60 |
+
yaxis=dict(tickformat='.0%'),
|
| 61 |
+
height=400
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
return fig
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def get_model_metrics() -> dict:
|
| 68 |
+
"""Get model performance metrics (placeholder)."""
|
| 69 |
+
# In production, these would come from the ML models table
|
| 70 |
+
return {
|
| 71 |
+
'auc': 0.78,
|
| 72 |
+
'rmse': 0.15,
|
| 73 |
+
'accuracy': 0.82,
|
| 74 |
+
'n_samples': 369831
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def create_model_metrics_display(metrics: dict) -> go.Figure:
|
| 79 |
+
"""Create model metrics display."""
|
| 80 |
+
fig = go.Figure()
|
| 81 |
+
|
| 82 |
+
categories = ['AUC', 'Accuracy', '1-RMSE']
|
| 83 |
+
values = [metrics['auc'], metrics['accuracy'], 1 - metrics['rmse']]
|
| 84 |
+
|
| 85 |
+
fig.add_trace(go.Scatterpolar(
|
| 86 |
+
r=values,
|
| 87 |
+
theta=categories,
|
| 88 |
+
fill='toself',
|
| 89 |
+
name='Model Performance',
|
| 90 |
+
line_color='#3498db'
|
| 91 |
+
))
|
| 92 |
+
|
| 93 |
+
fig.update_layout(
|
| 94 |
+
polar=dict(
|
| 95 |
+
radialaxis=dict(
|
| 96 |
+
visible=True,
|
| 97 |
+
range=[0, 1]
|
| 98 |
+
)
|
| 99 |
+
),
|
| 100 |
+
title=f"Model Performance (n={metrics['n_samples']:,})",
|
| 101 |
+
height=400
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
return fig
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def get_feature_importance() -> pd.DataFrame:
|
| 108 |
+
"""Get feature importance data (placeholder)."""
|
| 109 |
+
return pd.DataFrame({
|
| 110 |
+
'feature': ['incentive_amount', 'distance_miles', 'is_peak_hour',
|
| 111 |
+
'hour_of_day', 'day_of_week', 'avg_speed'],
|
| 112 |
+
'importance': [0.35, 0.25, 0.15, 0.10, 0.08, 0.07]
|
| 113 |
+
})
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def create_feature_importance_chart(df: pd.DataFrame) -> go.Figure:
|
| 117 |
+
"""Create feature importance bar chart."""
|
| 118 |
+
if df.empty:
|
| 119 |
+
return go.Figure().add_annotation(text="No data available", showarrow=False)
|
| 120 |
+
|
| 121 |
+
df = df.sort_values('importance', ascending=True)
|
| 122 |
+
|
| 123 |
+
fig = go.Figure(go.Bar(
|
| 124 |
+
x=df['importance'],
|
| 125 |
+
y=df['feature'],
|
| 126 |
+
orientation='h',
|
| 127 |
+
marker_color='#3498db',
|
| 128 |
+
text=df['importance'].apply(lambda x: f'{x:.1%}'),
|
| 129 |
+
textposition='auto',
|
| 130 |
+
hovertemplate='%{y}<br>Importance: %{x:.1%}<extra></extra>'
|
| 131 |
+
))
|
| 132 |
+
|
| 133 |
+
fig.update_layout(
|
| 134 |
+
title='Feature Importance',
|
| 135 |
+
xaxis_title='Importance',
|
| 136 |
+
yaxis_title='Feature',
|
| 137 |
+
xaxis=dict(tickformat='.0%'),
|
| 138 |
+
height=400
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
return fig
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def create_predicted_vs_actual() -> go.Figure:
|
| 145 |
+
"""Create predicted vs actual scatter plot (placeholder data)."""
|
| 146 |
+
import numpy as np
|
| 147 |
+
|
| 148 |
+
np.random.seed(42)
|
| 149 |
+
n = 100
|
| 150 |
+
actual = np.random.uniform(0, 1, n)
|
| 151 |
+
predicted = actual + np.random.normal(0, 0.1, n)
|
| 152 |
+
predicted = np.clip(predicted, 0, 1)
|
| 153 |
+
|
| 154 |
+
fig = go.Figure()
|
| 155 |
+
|
| 156 |
+
fig.add_trace(go.Scatter(
|
| 157 |
+
x=actual,
|
| 158 |
+
y=predicted,
|
| 159 |
+
mode='markers',
|
| 160 |
+
marker=dict(color='#3498db', size=8, opacity=0.6),
|
| 161 |
+
hovertemplate='Actual: %{x:.2f}<br>Predicted: %{y:.2f}<extra></extra>'
|
| 162 |
+
))
|
| 163 |
+
|
| 164 |
+
# Perfect prediction line
|
| 165 |
+
fig.add_trace(go.Scatter(
|
| 166 |
+
x=[0, 1],
|
| 167 |
+
y=[0, 1],
|
| 168 |
+
mode='lines',
|
| 169 |
+
line=dict(color='red', dash='dash'),
|
| 170 |
+
name='Perfect Prediction'
|
| 171 |
+
))
|
| 172 |
+
|
| 173 |
+
fig.update_layout(
|
| 174 |
+
title='Predicted vs Actual Carpool Rate',
|
| 175 |
+
xaxis_title='Actual Rate',
|
| 176 |
+
yaxis_title='Predicted Rate',
|
| 177 |
+
height=400,
|
| 178 |
+
showlegend=False
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
return fig
|
components/geo_map.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Geospatial map visualization component.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import pandas as pd
|
| 6 |
+
import plotly.express as px
|
| 7 |
+
import plotly.graph_objects as go
|
| 8 |
+
|
| 9 |
+
from database import query
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def get_corridor_data() -> pd.DataFrame:
|
| 13 |
+
"""Get corridor location and metrics data."""
|
| 14 |
+
# Sample I-24 corridor segments
|
| 15 |
+
return pd.DataFrame({
|
| 16 |
+
'segment_id': ['seg_1', 'seg_2', 'seg_3', 'seg_4', 'seg_5'],
|
| 17 |
+
'segment_name': ['I-24 @ Briley Pkwy', 'I-24 @ Harding Pl', 'I-24 @ Downtown',
|
| 18 |
+
'I-24 @ Shelby Ave', 'I-24 @ Spring St'],
|
| 19 |
+
'latitude': [36.08, 36.10, 36.12, 36.14, 36.16],
|
| 20 |
+
'longitude': [-86.70, -86.72, -86.75, -86.77, -86.79],
|
| 21 |
+
'avg_speed_mph': [35, 28, 22, 30, 45],
|
| 22 |
+
'congestion_level': ['Moderate', 'Severe', 'Severe', 'Moderate', 'Light'],
|
| 23 |
+
'vehicle_count': [450, 520, 580, 490, 380]
|
| 24 |
+
})
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def create_corridor_map(df: pd.DataFrame) -> go.Figure:
|
| 28 |
+
"""Create interactive corridor map with Plotly."""
|
| 29 |
+
if df.empty:
|
| 30 |
+
return go.Figure().add_annotation(text="No data available", showarrow=False)
|
| 31 |
+
|
| 32 |
+
# Color scale based on speed
|
| 33 |
+
colors = []
|
| 34 |
+
for speed in df['avg_speed_mph']:
|
| 35 |
+
if speed < 25:
|
| 36 |
+
colors.append('#e74c3c') # Red - congested
|
| 37 |
+
elif speed < 40:
|
| 38 |
+
colors.append('#f39c12') # Orange - slow
|
| 39 |
+
else:
|
| 40 |
+
colors.append('#27ae60') # Green - free flow
|
| 41 |
+
|
| 42 |
+
fig = go.Figure()
|
| 43 |
+
|
| 44 |
+
# Add corridor line
|
| 45 |
+
fig.add_trace(go.Scattermapbox(
|
| 46 |
+
lat=df['latitude'],
|
| 47 |
+
lon=df['longitude'],
|
| 48 |
+
mode='lines+markers',
|
| 49 |
+
line=dict(width=4, color='#3498db'),
|
| 50 |
+
marker=dict(
|
| 51 |
+
size=15,
|
| 52 |
+
color=colors,
|
| 53 |
+
symbol='circle'
|
| 54 |
+
),
|
| 55 |
+
text=df.apply(
|
| 56 |
+
lambda r: f"{r['segment_name']}<br>Speed: {r['avg_speed_mph']} mph<br>"
|
| 57 |
+
f"Volume: {r['vehicle_count']} veh/hr",
|
| 58 |
+
axis=1
|
| 59 |
+
),
|
| 60 |
+
hoverinfo='text',
|
| 61 |
+
name='I-24 Corridor'
|
| 62 |
+
))
|
| 63 |
+
|
| 64 |
+
fig.update_layout(
|
| 65 |
+
mapbox=dict(
|
| 66 |
+
style='carto-positron',
|
| 67 |
+
center=dict(lat=36.12, lon=-86.75),
|
| 68 |
+
zoom=11
|
| 69 |
+
),
|
| 70 |
+
margin=dict(l=0, r=0, t=40, b=0),
|
| 71 |
+
title='I-24 Corridor Traffic Conditions',
|
| 72 |
+
height=500
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
return fig
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def create_heatmap_overlay(df: pd.DataFrame) -> go.Figure:
|
| 79 |
+
"""Create density heatmap for traffic."""
|
| 80 |
+
if df.empty:
|
| 81 |
+
return go.Figure().add_annotation(text="No data available", showarrow=False)
|
| 82 |
+
|
| 83 |
+
fig = go.Figure()
|
| 84 |
+
|
| 85 |
+
fig.add_trace(go.Densitymapbox(
|
| 86 |
+
lat=df['latitude'],
|
| 87 |
+
lon=df['longitude'],
|
| 88 |
+
z=df['vehicle_count'],
|
| 89 |
+
radius=30,
|
| 90 |
+
colorscale='YlOrRd',
|
| 91 |
+
showscale=True,
|
| 92 |
+
colorbar=dict(title='Volume')
|
| 93 |
+
))
|
| 94 |
+
|
| 95 |
+
fig.update_layout(
|
| 96 |
+
mapbox=dict(
|
| 97 |
+
style='carto-positron',
|
| 98 |
+
center=dict(lat=36.12, lon=-86.75),
|
| 99 |
+
zoom=11
|
| 100 |
+
),
|
| 101 |
+
margin=dict(l=0, r=0, t=40, b=0),
|
| 102 |
+
title='Traffic Density Heatmap',
|
| 103 |
+
height=500
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
return fig
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def get_zone_stats() -> pd.DataFrame:
|
| 110 |
+
"""Get statistics by zone."""
|
| 111 |
+
return pd.DataFrame({
|
| 112 |
+
'zone': ['Downtown', 'Southeast', 'East Nashville', 'Antioch'],
|
| 113 |
+
'avg_speed': [25, 35, 40, 45],
|
| 114 |
+
'carpool_rate': [0.25, 0.18, 0.15, 0.12],
|
| 115 |
+
'incentive_uptake': [0.35, 0.28, 0.22, 0.18]
|
| 116 |
+
})
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def create_zone_comparison(df: pd.DataFrame) -> go.Figure:
|
| 120 |
+
"""Create zone comparison chart."""
|
| 121 |
+
if df.empty:
|
| 122 |
+
return go.Figure().add_annotation(text="No data available", showarrow=False)
|
| 123 |
+
|
| 124 |
+
fig = go.Figure()
|
| 125 |
+
|
| 126 |
+
fig.add_trace(go.Bar(
|
| 127 |
+
x=df['zone'],
|
| 128 |
+
y=df['avg_speed'],
|
| 129 |
+
name='Avg Speed (mph)',
|
| 130 |
+
marker_color='#3498db'
|
| 131 |
+
))
|
| 132 |
+
|
| 133 |
+
fig.add_trace(go.Scatter(
|
| 134 |
+
x=df['zone'],
|
| 135 |
+
y=df['carpool_rate'] * 100,
|
| 136 |
+
name='Carpool Rate (%)',
|
| 137 |
+
yaxis='y2',
|
| 138 |
+
mode='lines+markers',
|
| 139 |
+
line=dict(color='#e74c3c', width=2),
|
| 140 |
+
marker=dict(size=10)
|
| 141 |
+
))
|
| 142 |
+
|
| 143 |
+
fig.update_layout(
|
| 144 |
+
title='Zone Performance Comparison',
|
| 145 |
+
xaxis_title='Zone',
|
| 146 |
+
yaxis=dict(title='Speed (mph)', side='left'),
|
| 147 |
+
yaxis2=dict(title='Carpool Rate (%)', side='right', overlaying='y'),
|
| 148 |
+
height=400,
|
| 149 |
+
legend=dict(x=0.7, y=1.1, orientation='h')
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
return fig
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def get_legend_data() -> dict:
|
| 156 |
+
"""Get legend information for map."""
|
| 157 |
+
return {
|
| 158 |
+
'colors': {
|
| 159 |
+
'Free Flow (>40 mph)': '#27ae60',
|
| 160 |
+
'Slow (25-40 mph)': '#f39c12',
|
| 161 |
+
'Congested (<25 mph)': '#e74c3c'
|
| 162 |
+
}
|
| 163 |
+
}
|
components/incentive_analytics.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Incentive analytics visualization component.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import pandas as pd
|
| 6 |
+
import plotly.express as px
|
| 7 |
+
import plotly.graph_objects as go
|
| 8 |
+
|
| 9 |
+
from database import query
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def get_incentive_funnel_data() -> pd.DataFrame:
|
| 13 |
+
"""Get incentive conversion funnel data."""
|
| 14 |
+
sql = """
|
| 15 |
+
SELECT
|
| 16 |
+
incentive_type,
|
| 17 |
+
count(*) as total_offers,
|
| 18 |
+
sum(case when was_accepted then 1 else 0 end) as accepts,
|
| 19 |
+
sum(case when was_completed then 1 else 0 end) as completions
|
| 20 |
+
FROM main_marts.fct_incentive_events
|
| 21 |
+
GROUP BY incentive_type
|
| 22 |
+
"""
|
| 23 |
+
return query(sql)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def create_funnel_chart(df: pd.DataFrame) -> go.Figure:
|
| 27 |
+
"""Create incentive conversion funnel."""
|
| 28 |
+
if df.empty:
|
| 29 |
+
return go.Figure().add_annotation(text="No data available", showarrow=False)
|
| 30 |
+
|
| 31 |
+
# Aggregate across types
|
| 32 |
+
totals = df[['total_offers', 'accepts', 'completions']].sum()
|
| 33 |
+
|
| 34 |
+
fig = go.Figure(go.Funnel(
|
| 35 |
+
y=['Offers', 'Accepted', 'Completed'],
|
| 36 |
+
x=[totals['total_offers'], totals['accepts'], totals['completions']],
|
| 37 |
+
textinfo="value+percent initial",
|
| 38 |
+
marker=dict(color=['#3498db', '#2ecc71', '#27ae60'])
|
| 39 |
+
))
|
| 40 |
+
|
| 41 |
+
fig.update_layout(
|
| 42 |
+
title='Incentive Conversion Funnel',
|
| 43 |
+
height=400
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
return fig
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def get_spend_by_type_data() -> pd.DataFrame:
|
| 50 |
+
"""Get spending by incentive type."""
|
| 51 |
+
sql = """
|
| 52 |
+
SELECT
|
| 53 |
+
incentive_type,
|
| 54 |
+
sum(actual_payout) as total_spend,
|
| 55 |
+
count(*) as n_events,
|
| 56 |
+
avg(actual_payout) as avg_payout
|
| 57 |
+
FROM main_marts.fct_incentive_events
|
| 58 |
+
WHERE was_completed
|
| 59 |
+
GROUP BY incentive_type
|
| 60 |
+
ORDER BY total_spend DESC
|
| 61 |
+
"""
|
| 62 |
+
return query(sql)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def create_spend_chart(df: pd.DataFrame) -> go.Figure:
|
| 66 |
+
"""Create spending breakdown chart."""
|
| 67 |
+
if df.empty:
|
| 68 |
+
return go.Figure().add_annotation(text="No data available", showarrow=False)
|
| 69 |
+
|
| 70 |
+
fig = px.pie(
|
| 71 |
+
df,
|
| 72 |
+
values='total_spend',
|
| 73 |
+
names='incentive_type',
|
| 74 |
+
title='Spending by Incentive Type',
|
| 75 |
+
color_discrete_sequence=px.colors.qualitative.Set2
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
fig.update_traces(
|
| 79 |
+
textposition='inside',
|
| 80 |
+
textinfo='percent+label',
|
| 81 |
+
hovertemplate='%{label}<br>$%{value:,.2f}<br>%{percent}<extra></extra>'
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
fig.update_layout(height=400)
|
| 85 |
+
|
| 86 |
+
return fig
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def get_effectiveness_data() -> pd.DataFrame:
|
| 90 |
+
"""Get incentive effectiveness metrics."""
|
| 91 |
+
sql = """
|
| 92 |
+
SELECT
|
| 93 |
+
incentive_type,
|
| 94 |
+
count(*) as n_completed,
|
| 95 |
+
sum(actual_payout) as total_cost,
|
| 96 |
+
avg(actual_payout) as avg_cost
|
| 97 |
+
FROM main_marts.fct_incentive_events
|
| 98 |
+
WHERE was_completed
|
| 99 |
+
GROUP BY incentive_type
|
| 100 |
+
"""
|
| 101 |
+
return query(sql)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def create_effectiveness_chart(df: pd.DataFrame) -> go.Figure:
|
| 105 |
+
"""Create cost effectiveness chart."""
|
| 106 |
+
if df.empty:
|
| 107 |
+
return go.Figure().add_annotation(text="No data available", showarrow=False)
|
| 108 |
+
|
| 109 |
+
fig = go.Figure()
|
| 110 |
+
|
| 111 |
+
fig.add_trace(go.Bar(
|
| 112 |
+
x=df['incentive_type'],
|
| 113 |
+
y=df['avg_cost'],
|
| 114 |
+
name='Avg Cost per Completion',
|
| 115 |
+
marker_color='#3498db',
|
| 116 |
+
text=df['avg_cost'].apply(lambda x: f'${x:.2f}'),
|
| 117 |
+
textposition='auto',
|
| 118 |
+
hovertemplate='%{x}<br>Avg Cost: $%{y:.2f}<extra></extra>'
|
| 119 |
+
))
|
| 120 |
+
|
| 121 |
+
fig.update_layout(
|
| 122 |
+
title='Average Cost per Completed Incentive',
|
| 123 |
+
xaxis_title='Incentive Type',
|
| 124 |
+
yaxis_title='Average Cost ($)',
|
| 125 |
+
height=400
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
return fig
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def get_uptake_trend_data() -> pd.DataFrame:
|
| 132 |
+
"""Get incentive uptake trend over time."""
|
| 133 |
+
sql = """
|
| 134 |
+
SELECT
|
| 135 |
+
incentive_type,
|
| 136 |
+
final_outcome,
|
| 137 |
+
count(*) as count
|
| 138 |
+
FROM main_marts.fct_incentive_events
|
| 139 |
+
GROUP BY incentive_type, final_outcome
|
| 140 |
+
"""
|
| 141 |
+
return query(sql)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def create_uptake_chart(df: pd.DataFrame) -> go.Figure:
|
| 145 |
+
"""Create uptake by outcome chart."""
|
| 146 |
+
if df.empty:
|
| 147 |
+
return go.Figure().add_annotation(text="No data available", showarrow=False)
|
| 148 |
+
|
| 149 |
+
fig = px.bar(
|
| 150 |
+
df,
|
| 151 |
+
x='incentive_type',
|
| 152 |
+
y='count',
|
| 153 |
+
color='final_outcome',
|
| 154 |
+
title='Incentive Outcomes by Type',
|
| 155 |
+
barmode='stack',
|
| 156 |
+
color_discrete_map={
|
| 157 |
+
'COMPLETED': '#27ae60',
|
| 158 |
+
'ACCEPTED_PENDING': '#f39c12',
|
| 159 |
+
'REJECTED': '#e74c3c',
|
| 160 |
+
'OFFERED_PENDING': '#95a5a6'
|
| 161 |
+
}
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
fig.update_layout(
|
| 165 |
+
xaxis_title='Incentive Type',
|
| 166 |
+
yaxis_title='Count',
|
| 167 |
+
height=400
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
return fig
|
components/realtime_metrics.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Real-time metrics KPI component.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import pandas as pd
|
| 6 |
+
import plotly.graph_objects as go
|
| 7 |
+
|
| 8 |
+
from database import query
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def get_kpi_data() -> dict:
|
| 12 |
+
"""Get current KPI values."""
|
| 13 |
+
# In production, these would be computed from the latest data
|
| 14 |
+
return {
|
| 15 |
+
'vmt_reduction_pct': 12.5,
|
| 16 |
+
'avg_occupancy': 1.85,
|
| 17 |
+
'peak_shift_pct': 8.3,
|
| 18 |
+
'incentive_efficiency': 2.15,
|
| 19 |
+
'carpool_rate': 0.23,
|
| 20 |
+
'avg_speed_improvement': 15.2
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def create_kpi_gauge(value: float, title: str, suffix: str = '%',
|
| 25 |
+
min_val: float = 0, max_val: float = 100,
|
| 26 |
+
thresholds: list = None) -> go.Figure:
|
| 27 |
+
"""Create a gauge chart for a KPI."""
|
| 28 |
+
if thresholds is None:
|
| 29 |
+
thresholds = [max_val * 0.3, max_val * 0.7, max_val]
|
| 30 |
+
|
| 31 |
+
fig = go.Figure(go.Indicator(
|
| 32 |
+
mode="gauge+number",
|
| 33 |
+
value=value,
|
| 34 |
+
number={'suffix': suffix},
|
| 35 |
+
title={'text': title, 'font': {'size': 14}},
|
| 36 |
+
gauge={
|
| 37 |
+
'axis': {'range': [min_val, max_val]},
|
| 38 |
+
'bar': {'color': "#3498db"},
|
| 39 |
+
'steps': [
|
| 40 |
+
{'range': [min_val, thresholds[0]], 'color': "#e74c3c"},
|
| 41 |
+
{'range': [thresholds[0], thresholds[1]], 'color': "#f39c12"},
|
| 42 |
+
{'range': [thresholds[1], max_val], 'color': "#27ae60"}
|
| 43 |
+
],
|
| 44 |
+
'threshold': {
|
| 45 |
+
'line': {'color': "black", 'width': 2},
|
| 46 |
+
'thickness': 0.75,
|
| 47 |
+
'value': value
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
))
|
| 51 |
+
|
| 52 |
+
fig.update_layout(
|
| 53 |
+
height=200,
|
| 54 |
+
margin=dict(l=20, r=20, t=40, b=20)
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
return fig
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def create_sparkline(values: list, title: str) -> go.Figure:
|
| 61 |
+
"""Create a sparkline chart."""
|
| 62 |
+
fig = go.Figure(go.Scatter(
|
| 63 |
+
y=values,
|
| 64 |
+
mode='lines',
|
| 65 |
+
fill='tozeroy',
|
| 66 |
+
line=dict(color='#3498db', width=2),
|
| 67 |
+
fillcolor='rgba(52, 152, 219, 0.2)'
|
| 68 |
+
))
|
| 69 |
+
|
| 70 |
+
fig.update_layout(
|
| 71 |
+
title=dict(text=title, font=dict(size=12)),
|
| 72 |
+
height=100,
|
| 73 |
+
margin=dict(l=10, r=10, t=30, b=10),
|
| 74 |
+
xaxis=dict(visible=False),
|
| 75 |
+
yaxis=dict(visible=False),
|
| 76 |
+
showlegend=False
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
return fig
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def create_metric_card(value: float, label: str, delta: float = None,
|
| 83 |
+
format_str: str = '.1f', prefix: str = '',
|
| 84 |
+
suffix: str = '') -> go.Figure:
|
| 85 |
+
"""Create a metric card with optional delta."""
|
| 86 |
+
delta_ref = None
|
| 87 |
+
if delta is not None:
|
| 88 |
+
delta_ref = {'reference': value - delta, 'relative': True}
|
| 89 |
+
|
| 90 |
+
fig = go.Figure(go.Indicator(
|
| 91 |
+
mode="number+delta" if delta is not None else "number",
|
| 92 |
+
value=value,
|
| 93 |
+
number={
|
| 94 |
+
'prefix': prefix,
|
| 95 |
+
'suffix': suffix,
|
| 96 |
+
'valueformat': format_str
|
| 97 |
+
},
|
| 98 |
+
delta=delta_ref,
|
| 99 |
+
title={'text': label, 'font': {'size': 14}}
|
| 100 |
+
))
|
| 101 |
+
|
| 102 |
+
fig.update_layout(
|
| 103 |
+
height=150,
|
| 104 |
+
margin=dict(l=20, r=20, t=40, b=20)
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
return fig
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def get_trend_data() -> dict:
|
| 111 |
+
"""Get trend data for sparklines."""
|
| 112 |
+
# Placeholder - would be computed from time series
|
| 113 |
+
import numpy as np
|
| 114 |
+
np.random.seed(42)
|
| 115 |
+
|
| 116 |
+
return {
|
| 117 |
+
'vmt': list(np.cumsum(np.random.randn(20)) + 100),
|
| 118 |
+
'speed': list(45 + np.cumsum(np.random.randn(20) * 0.5)),
|
| 119 |
+
'carpool': list(0.2 + np.cumsum(np.random.randn(20) * 0.01))
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def render_kpi_dashboard():
|
| 124 |
+
"""Render KPI dashboard layout."""
|
| 125 |
+
import gradio as gr
|
| 126 |
+
|
| 127 |
+
kpis = get_kpi_data()
|
| 128 |
+
trends = get_trend_data()
|
| 129 |
+
|
| 130 |
+
with gr.Column():
|
| 131 |
+
gr.Markdown("## Key Performance Indicators")
|
| 132 |
+
|
| 133 |
+
with gr.Row():
|
| 134 |
+
with gr.Column(scale=1):
|
| 135 |
+
gr.Plot(
|
| 136 |
+
value=create_kpi_gauge(
|
| 137 |
+
kpis['vmt_reduction_pct'],
|
| 138 |
+
'VMT Reduction',
|
| 139 |
+
'%', 0, 25
|
| 140 |
+
)
|
| 141 |
+
)
|
| 142 |
+
with gr.Column(scale=1):
|
| 143 |
+
gr.Plot(
|
| 144 |
+
value=create_kpi_gauge(
|
| 145 |
+
kpis['avg_occupancy'],
|
| 146 |
+
'Avg Occupancy',
|
| 147 |
+
'', 1, 3,
|
| 148 |
+
[1.5, 2.0, 3.0]
|
| 149 |
+
)
|
| 150 |
+
)
|
| 151 |
+
with gr.Column(scale=1):
|
| 152 |
+
gr.Plot(
|
| 153 |
+
value=create_kpi_gauge(
|
| 154 |
+
kpis['peak_shift_pct'],
|
| 155 |
+
'Peak Shift',
|
| 156 |
+
'%', 0, 20
|
| 157 |
+
)
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
with gr.Row():
|
| 161 |
+
with gr.Column(scale=1):
|
| 162 |
+
gr.Plot(
|
| 163 |
+
value=create_metric_card(
|
| 164 |
+
kpis['incentive_efficiency'],
|
| 165 |
+
'Cost per VMT Reduced',
|
| 166 |
+
prefix='$',
|
| 167 |
+
format_str='.2f'
|
| 168 |
+
)
|
| 169 |
+
)
|
| 170 |
+
with gr.Column(scale=1):
|
| 171 |
+
gr.Plot(
|
| 172 |
+
value=create_metric_card(
|
| 173 |
+
kpis['carpool_rate'] * 100,
|
| 174 |
+
'Carpool Rate',
|
| 175 |
+
suffix='%',
|
| 176 |
+
format_str='.1f'
|
| 177 |
+
)
|
| 178 |
+
)
|
| 179 |
+
with gr.Column(scale=1):
|
| 180 |
+
gr.Plot(
|
| 181 |
+
value=create_metric_card(
|
| 182 |
+
kpis['avg_speed_improvement'],
|
| 183 |
+
'Speed Improvement',
|
| 184 |
+
suffix='%',
|
| 185 |
+
format_str='.1f'
|
| 186 |
+
)
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
gr.Markdown("### Trends")
|
| 190 |
+
|
| 191 |
+
with gr.Row():
|
| 192 |
+
with gr.Column(scale=1):
|
| 193 |
+
gr.Plot(value=create_sparkline(trends['vmt'], 'VMT Trend'))
|
| 194 |
+
with gr.Column(scale=1):
|
| 195 |
+
gr.Plot(value=create_sparkline(trends['speed'], 'Speed Trend'))
|
| 196 |
+
with gr.Column(scale=1):
|
| 197 |
+
gr.Plot(value=create_sparkline(trends['carpool'], 'Carpool Trend'))
|
components/simulation_compare.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Simulation comparison visualization component.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import pandas as pd
|
| 6 |
+
import plotly.express as px
|
| 7 |
+
import plotly.graph_objects as go
|
| 8 |
+
|
| 9 |
+
from database import query
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def get_scenario_list() -> list[str]:
|
| 13 |
+
"""Get list of available scenarios."""
|
| 14 |
+
sql = """
|
| 15 |
+
SELECT DISTINCT scenario_name
|
| 16 |
+
FROM main_marts.fct_simulation_runs
|
| 17 |
+
ORDER BY scenario_name
|
| 18 |
+
"""
|
| 19 |
+
df = query(sql)
|
| 20 |
+
return df['scenario_name'].tolist() if not df.empty else []
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def get_scenario_comparison_data() -> pd.DataFrame:
|
| 24 |
+
"""Get scenario comparison metrics."""
|
| 25 |
+
sql = """
|
| 26 |
+
SELECT
|
| 27 |
+
scenario_name,
|
| 28 |
+
n_agents,
|
| 29 |
+
treatment_avg_speed,
|
| 30 |
+
baseline_avg_speed,
|
| 31 |
+
speed_improvement_pct,
|
| 32 |
+
vmt_reduction_pct,
|
| 33 |
+
peak_reduction_pct,
|
| 34 |
+
treatment_spend
|
| 35 |
+
FROM main_marts.fct_simulation_runs
|
| 36 |
+
"""
|
| 37 |
+
return query(sql)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def create_scenario_comparison_chart(df: pd.DataFrame) -> go.Figure:
|
| 41 |
+
"""Create scenario comparison bar chart."""
|
| 42 |
+
if df.empty:
|
| 43 |
+
return go.Figure().add_annotation(text="No data available", showarrow=False)
|
| 44 |
+
|
| 45 |
+
fig = go.Figure()
|
| 46 |
+
|
| 47 |
+
metrics = ['speed_improvement_pct', 'vmt_reduction_pct', 'peak_reduction_pct']
|
| 48 |
+
colors = ['#3498db', '#2ecc71', '#e74c3c']
|
| 49 |
+
names = ['Speed Improvement', 'VMT Reduction', 'Peak Reduction']
|
| 50 |
+
|
| 51 |
+
for metric, color, name in zip(metrics, colors, names):
|
| 52 |
+
fig.add_trace(go.Bar(
|
| 53 |
+
x=df['scenario_name'],
|
| 54 |
+
y=df[metric],
|
| 55 |
+
name=name,
|
| 56 |
+
marker_color=color,
|
| 57 |
+
text=df[metric].apply(lambda x: f'{x:.1f}%'),
|
| 58 |
+
textposition='auto',
|
| 59 |
+
hovertemplate='%{x}<br>%{y:.1f}%<extra></extra>'
|
| 60 |
+
))
|
| 61 |
+
|
| 62 |
+
fig.update_layout(
|
| 63 |
+
title='Scenario Performance Comparison',
|
| 64 |
+
xaxis_title='Scenario',
|
| 65 |
+
yaxis_title='Improvement (%)',
|
| 66 |
+
barmode='group',
|
| 67 |
+
height=400
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
return fig
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def create_cost_effectiveness_chart(df: pd.DataFrame) -> go.Figure:
|
| 74 |
+
"""Create cost effectiveness comparison."""
|
| 75 |
+
if df.empty:
|
| 76 |
+
return go.Figure().add_annotation(text="No data available", showarrow=False)
|
| 77 |
+
|
| 78 |
+
# Calculate cost per % improvement
|
| 79 |
+
df = df.copy()
|
| 80 |
+
df['cost_per_vmt_pct'] = df['treatment_spend'] / df['vmt_reduction_pct'].clip(lower=0.1)
|
| 81 |
+
|
| 82 |
+
fig = go.Figure()
|
| 83 |
+
|
| 84 |
+
fig.add_trace(go.Scatter(
|
| 85 |
+
x=df['treatment_spend'],
|
| 86 |
+
y=df['vmt_reduction_pct'],
|
| 87 |
+
mode='markers+text',
|
| 88 |
+
marker=dict(
|
| 89 |
+
size=20,
|
| 90 |
+
color=df['speed_improvement_pct'],
|
| 91 |
+
colorscale='Viridis',
|
| 92 |
+
showscale=True,
|
| 93 |
+
colorbar=dict(title='Speed Imp. %')
|
| 94 |
+
),
|
| 95 |
+
text=df['scenario_name'],
|
| 96 |
+
textposition='top center',
|
| 97 |
+
hovertemplate='%{text}<br>Spend: $%{x:,.0f}<br>VMT Reduction: %{y:.1f}%<extra></extra>'
|
| 98 |
+
))
|
| 99 |
+
|
| 100 |
+
fig.update_layout(
|
| 101 |
+
title='Cost vs. VMT Reduction by Scenario',
|
| 102 |
+
xaxis_title='Total Spend ($)',
|
| 103 |
+
yaxis_title='VMT Reduction (%)',
|
| 104 |
+
height=400
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
return fig
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def create_baseline_treatment_comparison(df: pd.DataFrame, scenario: str = None) -> go.Figure:
|
| 111 |
+
"""Create baseline vs treatment comparison for a scenario."""
|
| 112 |
+
if df.empty:
|
| 113 |
+
return go.Figure().add_annotation(text="No data available", showarrow=False)
|
| 114 |
+
|
| 115 |
+
if scenario:
|
| 116 |
+
df = df[df['scenario_name'] == scenario]
|
| 117 |
+
|
| 118 |
+
if df.empty:
|
| 119 |
+
return go.Figure().add_annotation(text="Scenario not found", showarrow=False)
|
| 120 |
+
|
| 121 |
+
row = df.iloc[0]
|
| 122 |
+
|
| 123 |
+
fig = go.Figure()
|
| 124 |
+
|
| 125 |
+
categories = ['Avg Speed (mph)', 'VMT (scaled)', 'Peak Demand (scaled)']
|
| 126 |
+
baseline = [row['baseline_avg_speed'], 100, 100] # Normalized
|
| 127 |
+
treatment = [
|
| 128 |
+
row['treatment_avg_speed'],
|
| 129 |
+
100 - row['vmt_reduction_pct'],
|
| 130 |
+
100 - row['peak_reduction_pct']
|
| 131 |
+
]
|
| 132 |
+
|
| 133 |
+
fig.add_trace(go.Bar(
|
| 134 |
+
name='Baseline',
|
| 135 |
+
x=categories,
|
| 136 |
+
y=baseline,
|
| 137 |
+
marker_color='#95a5a6'
|
| 138 |
+
))
|
| 139 |
+
|
| 140 |
+
fig.add_trace(go.Bar(
|
| 141 |
+
name='Treatment',
|
| 142 |
+
x=categories,
|
| 143 |
+
y=treatment,
|
| 144 |
+
marker_color='#3498db'
|
| 145 |
+
))
|
| 146 |
+
|
| 147 |
+
fig.update_layout(
|
| 148 |
+
title=f'Baseline vs Treatment: {scenario or "All Scenarios"}',
|
| 149 |
+
yaxis_title='Value',
|
| 150 |
+
barmode='group',
|
| 151 |
+
height=400
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
return fig
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def get_metrics_summary(df: pd.DataFrame) -> dict:
|
| 158 |
+
"""Get summary metrics across all scenarios."""
|
| 159 |
+
if df.empty:
|
| 160 |
+
return {}
|
| 161 |
+
|
| 162 |
+
return {
|
| 163 |
+
'avg_speed_improvement': df['speed_improvement_pct'].mean(),
|
| 164 |
+
'avg_vmt_reduction': df['vmt_reduction_pct'].mean(),
|
| 165 |
+
'avg_peak_reduction': df['peak_reduction_pct'].mean(),
|
| 166 |
+
'total_spend': df['treatment_spend'].sum(),
|
| 167 |
+
'n_scenarios': len(df)
|
| 168 |
+
}
|
components/traffic_flow.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Traffic flow visualization component.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import pandas as pd
|
| 6 |
+
import plotly.express as px
|
| 7 |
+
import plotly.graph_objects as go
|
| 8 |
+
|
| 9 |
+
from database import query
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def get_speed_heatmap_data() -> pd.DataFrame:
|
| 13 |
+
"""Get data for speed heatmap."""
|
| 14 |
+
sql = """
|
| 15 |
+
SELECT
|
| 16 |
+
extract(hour from hour_bucket) as hour,
|
| 17 |
+
extract(dow from hour_bucket) as day_of_week,
|
| 18 |
+
avg(avg_speed_mph) as avg_speed
|
| 19 |
+
FROM main_marts.fct_corridor_flows
|
| 20 |
+
GROUP BY 1, 2
|
| 21 |
+
ORDER BY 2, 1
|
| 22 |
+
"""
|
| 23 |
+
return query(sql)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def create_speed_heatmap(df: pd.DataFrame) -> go.Figure:
|
| 27 |
+
"""Create speed heatmap visualization."""
|
| 28 |
+
if df.empty:
|
| 29 |
+
return go.Figure().add_annotation(text="No data available", showarrow=False)
|
| 30 |
+
|
| 31 |
+
# Pivot for heatmap
|
| 32 |
+
pivot = df.pivot(index='day_of_week', columns='hour', values='avg_speed')
|
| 33 |
+
|
| 34 |
+
day_names = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
| 35 |
+
|
| 36 |
+
fig = go.Figure(data=go.Heatmap(
|
| 37 |
+
z=pivot.values,
|
| 38 |
+
x=[f"{h}:00" for h in range(24)],
|
| 39 |
+
y=[day_names[int(d)] for d in pivot.index],
|
| 40 |
+
colorscale='RdYlGn',
|
| 41 |
+
colorbar=dict(title='Speed (mph)'),
|
| 42 |
+
hovertemplate='%{y} %{x}<br>Speed: %{z:.1f} mph<extra></extra>'
|
| 43 |
+
))
|
| 44 |
+
|
| 45 |
+
fig.update_layout(
|
| 46 |
+
title='Average Speed by Hour and Day',
|
| 47 |
+
xaxis_title='Hour of Day',
|
| 48 |
+
yaxis_title='Day of Week',
|
| 49 |
+
height=400
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
return fig
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def get_hourly_volume_data() -> pd.DataFrame:
|
| 56 |
+
"""Get hourly traffic volume data."""
|
| 57 |
+
sql = """
|
| 58 |
+
SELECT
|
| 59 |
+
extract(hour from hour_bucket) as hour,
|
| 60 |
+
time_period,
|
| 61 |
+
sum(vehicle_count) as total_vehicles,
|
| 62 |
+
avg(avg_speed_mph) as avg_speed
|
| 63 |
+
FROM main_marts.fct_corridor_flows
|
| 64 |
+
GROUP BY 1, 2
|
| 65 |
+
ORDER BY 1
|
| 66 |
+
"""
|
| 67 |
+
return query(sql)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def create_hourly_volume_chart(df: pd.DataFrame) -> go.Figure:
|
| 71 |
+
"""Create hourly volume bar chart."""
|
| 72 |
+
if df.empty:
|
| 73 |
+
return go.Figure().add_annotation(text="No data available", showarrow=False)
|
| 74 |
+
|
| 75 |
+
fig = go.Figure()
|
| 76 |
+
|
| 77 |
+
# Add bars for each time period
|
| 78 |
+
colors = {'AM_PEAK': '#e74c3c', 'PM_PEAK': '#e67e22', 'OFF_PEAK': '#27ae60'}
|
| 79 |
+
|
| 80 |
+
for period in df['time_period'].unique():
|
| 81 |
+
period_df = df[df['time_period'] == period]
|
| 82 |
+
fig.add_trace(go.Bar(
|
| 83 |
+
x=period_df['hour'],
|
| 84 |
+
y=period_df['total_vehicles'],
|
| 85 |
+
name=period.replace('_', ' ').title(),
|
| 86 |
+
marker_color=colors.get(period, '#3498db'),
|
| 87 |
+
hovertemplate='Hour: %{x}<br>Vehicles: %{y:,}<extra></extra>'
|
| 88 |
+
))
|
| 89 |
+
|
| 90 |
+
fig.update_layout(
|
| 91 |
+
title='Traffic Volume by Hour',
|
| 92 |
+
xaxis_title='Hour of Day',
|
| 93 |
+
yaxis_title='Total Vehicles',
|
| 94 |
+
barmode='stack',
|
| 95 |
+
height=400,
|
| 96 |
+
showlegend=True
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
return fig
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def get_congestion_timeline_data() -> pd.DataFrame:
|
| 103 |
+
"""Get congestion timeline data."""
|
| 104 |
+
sql = """
|
| 105 |
+
SELECT
|
| 106 |
+
hour_bucket,
|
| 107 |
+
corridor_id,
|
| 108 |
+
avg_speed_mph,
|
| 109 |
+
level_of_service
|
| 110 |
+
FROM main_marts.fct_corridor_flows
|
| 111 |
+
ORDER BY hour_bucket
|
| 112 |
+
"""
|
| 113 |
+
return query(sql)
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def create_congestion_timeline(df: pd.DataFrame) -> go.Figure:
|
| 117 |
+
"""Create congestion timeline chart."""
|
| 118 |
+
if df.empty:
|
| 119 |
+
return go.Figure().add_annotation(text="No data available", showarrow=False)
|
| 120 |
+
|
| 121 |
+
fig = go.Figure()
|
| 122 |
+
|
| 123 |
+
fig.add_trace(go.Scatter(
|
| 124 |
+
x=df['hour_bucket'],
|
| 125 |
+
y=df['avg_speed_mph'],
|
| 126 |
+
mode='lines',
|
| 127 |
+
name='Speed',
|
| 128 |
+
line=dict(color='#3498db', width=2),
|
| 129 |
+
fill='tozeroy',
|
| 130 |
+
fillcolor='rgba(52, 152, 219, 0.2)',
|
| 131 |
+
hovertemplate='%{x}<br>Speed: %{y:.1f} mph<extra></extra>'
|
| 132 |
+
))
|
| 133 |
+
|
| 134 |
+
# Add threshold lines
|
| 135 |
+
fig.add_hline(y=55, line_dash="dash", line_color="green",
|
| 136 |
+
annotation_text="Free Flow (55 mph)")
|
| 137 |
+
fig.add_hline(y=30, line_dash="dash", line_color="orange",
|
| 138 |
+
annotation_text="Congested (30 mph)")
|
| 139 |
+
fig.add_hline(y=15, line_dash="dash", line_color="red",
|
| 140 |
+
annotation_text="Severe (15 mph)")
|
| 141 |
+
|
| 142 |
+
fig.update_layout(
|
| 143 |
+
title='Speed Over Time',
|
| 144 |
+
xaxis_title='Time',
|
| 145 |
+
yaxis_title='Average Speed (mph)',
|
| 146 |
+
height=400,
|
| 147 |
+
yaxis=dict(range=[0, 80])
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
return fig
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def render_traffic_flow_tab():
|
| 154 |
+
"""Render the traffic flow tab content."""
|
| 155 |
+
import gradio as gr
|
| 156 |
+
|
| 157 |
+
with gr.Column():
|
| 158 |
+
gr.Markdown("## Traffic Flow Analysis")
|
| 159 |
+
gr.Markdown("Real-time and historical traffic patterns on I-24 corridor")
|
| 160 |
+
|
| 161 |
+
with gr.Row():
|
| 162 |
+
with gr.Column(scale=2):
|
| 163 |
+
heatmap_data = get_speed_heatmap_data()
|
| 164 |
+
heatmap_plot = gr.Plot(
|
| 165 |
+
value=create_speed_heatmap(heatmap_data),
|
| 166 |
+
label="Speed Heatmap"
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
with gr.Row():
|
| 170 |
+
with gr.Column():
|
| 171 |
+
volume_data = get_hourly_volume_data()
|
| 172 |
+
volume_plot = gr.Plot(
|
| 173 |
+
value=create_hourly_volume_chart(volume_data),
|
| 174 |
+
label="Hourly Volume"
|
| 175 |
+
)
|
| 176 |
+
with gr.Column():
|
| 177 |
+
timeline_data = get_congestion_timeline_data()
|
| 178 |
+
timeline_plot = gr.Plot(
|
| 179 |
+
value=create_congestion_timeline(timeline_data),
|
| 180 |
+
label="Congestion Timeline"
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
return heatmap_plot, volume_plot, timeline_plot
|