The R³S² Metric
This notebook stress-tests the Rolling Relative Route Scoring System. Does it hold up?
Traffic Analysis Enhancement Examples
This notebook demonstrates how to use the TrafficAnalyzer and VisualizationEngine classes for comprehensive traffic analysis.
0. Setup
# Auto-reload modules when they change
%load_ext autoreload
%autoreload 2
import pandas as pd
import numpy as np
import matplotlib as mpl
mpl.rcParams['figure.dpi'] = 300
# Load your traffic data
# Replace with your actual data loading code
traffic_df = pd.read_csv('csv-traffic-bangalore.csv')
routes_df = pd.read_csv('csv-routes-bangalore.csv')
The autoreload extension is already loaded. To reload it, use: %reload_ext autoreload
display(routes_df)
| route_code | label_full | label_short | |
|---|---|---|---|
| 0 | VJRQ+2M|RMJJ+F4 | Kudlu Gate Metro Station → Biocon Campus | Hosur Road |
| 1 | WGG8+G5|XH7P+G6 | The Watering Hole, Rajarajeshwari Nagar → Sir ... | Mysore Road |
| 2 | WP44+W8|WJFH+XQ | Karmelaram Railway Station, Chikkabellandur → ... | Sarjapur Road |
| 3 | XPC7+72|XM33+J3 | The Rameshwaram Cafe @ Brookfield → Gawky Goos... | Old Airport Road |
| 4 | 2HM2+P8|XJV5+RG | Jaya Prakash Narayana Park → Coles Park, Frase... | North Inner Ring |
| 5 | 2HVW+G8|XJXR+WG | Bethel AG Church, Hebbal → SMVT Railway Station | North Outer Ring |
| 6 | XJPW+92|WJP4+FF | Swami Vivekananda Road Metro Station → Christ ... | East Inner Ring |
| 7 | XMW9+G8|WMJR+V4 | Benniganahalli Metro Station → Embassy TechVil... | East Outer Ring |
| 8 | WHCJ+26|XGCP+FV | RV Road Metro Station, Jayanagar 5th Block → V... | South Outer Ring |
| 9 | WH5F+26|WJ8X+F5W | Jaya Prakash Nagar Metro Station → Hemavathi P... | Double Decker Flyover |
| 10 | XHJ7+MG|WJM6+VC | Lulu Mall Bengaluru → Nexus Mall Koramangala | Central Diagonal 1 |
| 11 | WHR9+R6|XJGF+6J | Big Bull Temple, Basavanagudi → Shri Someshwar... | Central Diagonal 2 |
| 12 | XJG4+7J|5PX4+HQ | MG Road Metro Station → Kempegowda Internation... | Airport Expy |
display(traffic_df)
| date | time | route_code | duration | distance | |
|---|---|---|---|---|---|
| 0 | 2025-09-25 | 14:25 | 2HM2+P8|XJV5+RG | 32 | 11.0 |
| 1 | 2025-09-25 | 14:25 | 2HVW+G8|XJXR+WG | 31 | 9.9 |
| 2 | 2025-09-25 | 14:25 | VJRQ+2M|RMJJ+F4 | 23 | 10.3 |
| 3 | 2025-09-25 | 14:25 | WH5F+26|WJ8X+F5W | 25 | 10.2 |
| 4 | 2025-09-25 | 14:25 | WHCJ+26|XGCP+FV | 37 | 10.8 |
| ... | ... | ... | ... | ... | ... |
| 57051 | 2026-04-18 | 22:26 | WHCJ+26|XGCP+FV | 30 | 10.6 |
| 57052 | 2026-04-18 | 22:26 | WH5F+26|WJ8X+F5W | 21 | 10.1 |
| 57053 | 2026-04-18 | 22:26 | XHJ7+MG|WJM6+VC | 27 | 10.8 |
| 57054 | 2026-04-18 | 22:26 | WHR9+R6|XJGF+6J | 32 | 10.7 |
| 57055 | 2026-04-18 | 22:26 | XJG4+7J|5PX4+HQ | 54 | 34.7 |
57056 rows × 5 columns
1. Data Preprocessing
First, preprocess the data and add temporal features:
data_utils.py is a preprocessing and data quality toolkit. The two functions you'd call first in any analysis are preprocess_traffic_data followed by compute_temporal_features:
preprocess_traffic_data— cleans raw CSV data. Derivesavg_speedfromdistance / (duration / 60)if it's missing, warns about nulls, drops rows missing critical columns, and sorts by time.compute_temporal_features— enriches the dataframe with time-based columns. Parsesdate/timestrings into year/month/day/hour, builds atimestamp, addsday_of_week, anis_weekendflag, and buckets hours into named periods:night,morning_rush,midday,evening_rush,evening.fill_missing_timestamps— handles gaps in a route's time series. Reindexes to a full common timeline, then fills missing speed values by averaging the previous and next known readings (falls back to just one side if only one neighbor exists).detect_outliers— flags anomalous speed readings using one of three methods: IQR (fence-based), Z-score (standard deviations from mean), or Isolation Forest (ML-based). Returns a boolean mask.bootstrap_resample— generatesnrandom samples with replacement from the dataset, used elsewhere for confidence interval estimation.
from data_utils import preprocess_traffic_data, compute_temporal_features
# Preprocess data
traffic_df = preprocess_traffic_data(traffic_df)
# Add temporal features
traffic_df = compute_temporal_features(traffic_df)
print(f"Data shape: {traffic_df.shape}")
print(f"Date range: {traffic_df['timestamp'].min()} to {traffic_df['timestamp'].max()}")
print(f"Number of routes: {traffic_df['route_code'].nunique()}")
Dedup: removed 196 duplicate rows based on ['route_code', 'date', 'hour', 'duration', 'distance'] Data shape: (56860, 14) Date range: 2025-09-25 14:00:00 to 2026-04-18 22:00:00 Number of routes: 13
display(traffic_df)
| date | time | route_code | duration | distance | hour | avg_speed | year | month | day | timestamp | day_of_week | is_weekend | time_category | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2025-09-25 | 14:25 | 2HM2+P8|XJV5+RG | 32 | 11.0 | 14 | 20.625000 | 2025 | 9 | 25 | 2025-09-25 14:00:00 | Thursday | False | early_afternoon |
| 1 | 2025-09-25 | 14:25 | 2HVW+G8|XJXR+WG | 31 | 9.9 | 14 | 19.161290 | 2025 | 9 | 25 | 2025-09-25 14:00:00 | Thursday | False | early_afternoon |
| 2 | 2025-09-25 | 14:25 | VJRQ+2M|RMJJ+F4 | 23 | 10.3 | 14 | 26.869565 | 2025 | 9 | 25 | 2025-09-25 14:00:00 | Thursday | False | early_afternoon |
| 3 | 2025-09-25 | 14:25 | WH5F+26|WJ8X+F5W | 25 | 10.2 | 14 | 24.480000 | 2025 | 9 | 25 | 2025-09-25 14:00:00 | Thursday | False | early_afternoon |
| 4 | 2025-09-25 | 14:25 | WHCJ+26|XGCP+FV | 37 | 10.8 | 14 | 17.513514 | 2025 | 9 | 25 | 2025-09-25 14:00:00 | Thursday | False | early_afternoon |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 57051 | 2026-04-18 | 22:26 | WHCJ+26|XGCP+FV | 30 | 10.6 | 22 | 21.200000 | 2026 | 4 | 18 | 2026-04-18 22:00:00 | Saturday | True | night |
| 57052 | 2026-04-18 | 22:26 | WH5F+26|WJ8X+F5W | 21 | 10.1 | 22 | 28.857143 | 2026 | 4 | 18 | 2026-04-18 22:00:00 | Saturday | True | night |
| 57053 | 2026-04-18 | 22:26 | XHJ7+MG|WJM6+VC | 27 | 10.8 | 22 | 24.000000 | 2026 | 4 | 18 | 2026-04-18 22:00:00 | Saturday | True | night |
| 57054 | 2026-04-18 | 22:26 | WHR9+R6|XJGF+6J | 32 | 10.7 | 22 | 20.062500 | 2026 | 4 | 18 | 2026-04-18 22:00:00 | Saturday | True | night |
| 57055 | 2026-04-18 | 22:26 | XJG4+7J|5PX4+HQ | 54 | 34.7 | 22 | 38.555556 | 2026 | 4 | 18 | 2026-04-18 22:00:00 | Saturday | True | night |
56860 rows × 14 columns
2. TrafficAnalyzer - R³S² Analysis
traffic_analyzer.py is the core analytical engine. It's built around one central class, TrafficAnalyzer, with a few custom exceptions at the top.
The overall purpose: given raw traffic data across multiple Bangalore routes, this class scores and ranks them by reliability, validates the statistical soundness of those rankings, and flags data quality issues that might skew the results.
The R³S² scoring system is the heart of it.
calculate_rrscomputes a composite score per route based on rolling speed variance and other metrics. Think of it as a "route reliability" ranking. The threescore_bymethods (percentile,zscore,median) are alternative scoring approaches you can compare against R³S².Correlation & stability analysis —
analyze_rrs_correlation,analyze_rrs_sensitivity, andanalyze_rrs_stabilitystress-test the scoring. They check whether the rankings hold up when you change the window size, remove outliers, or look at different time periods.Data quality —
analyze_data_completeness,identify_missing_patterns,compute_quality_metrics,validate_distance_consistency, andanalyze_missing_data_biasall audit the dataset itself. They tell you how trustworthy the input data is before you rely on the scores.Statistical tests —
test_normality,test_stationarity,analyze_autocorrelation,test_variance_homogeneity, andperform_power_analysisare classical stats checks on the speed time series per route.Confidence & transitivity —
compute_rrs_confidence_intervalsuses bootstrap resampling (fromdata_utils) to put error bars on the scores.test_rrs_transitivitychecks whether the ranking is logically consistent (if A > B and B > C, does A > C hold?).Diagnostics & reporting —
plot_qq_normality,plot_residual_diagnostics,evaluate_scoring_stability,evaluate_outlier_sensitivity,evaluate_computational_efficiency, andgenerate_methodology_ranking_reportare for deeper inspection and benchmarking the scoring approach itself.Recommendations —
generate_recommendationssynthesizes everything into actionable suggestions with severity levels.
from traffic_analyzer import TrafficAnalyzer
# Initialize analyzer
analyzer = TrafficAnalyzer(traffic_df, routes_df)
# Calculate R³S² scores
rrs_scores = analyzer.calculate_rrs()
# Add rank column
rrs_scores['rank'] = range(1, len(rrs_scores) + 1)
print("R³S² Rankings:")
display(rrs_scores[['route_code', 'points']])
R³S² Rankings:
| route_code | points | |
|---|---|---|
| 0 | XJG4+7J|5PX4+HQ | 65.0 |
| 1 | VJRQ+2M|RMJJ+F4 | 54.2 |
| 2 | WH5F+26|WJ8X+F5W | 40.1 |
| 3 | XMW9+G8|WMJR+V4 | 26.0 |
| 4 | WGG8+G5|XH7P+G6 | 11.9 |
| 5 | XPC7+72|XM33+J3 | 9.8 |
| 6 | WP44+W8|WJFH+XQ | 5.4 |
| 7 | XHJ7+MG|WJM6+VC | -8.7 |
| 8 | 2HM2+P8|XJV5+RG | -22.7 |
| 9 | XJPW+92|WJP4+FF | -30.3 |
| 10 | 2HVW+G8|XJXR+WG | -32.5 |
| 11 | WHCJ+26|XGCP+FV | -53.1 |
| 12 | WHR9+R6|XJGF+6J | -65.0 |
2.1 Correlation Analysis
# Analyze correlations
correlation_results = analyzer.analyze_rrs_correlation()
print("Correlation Analysis:")
for metric, value in correlation_results.items():
print(f"{metric}: {value:.4f}")
Correlation Analysis: pearson_correlation: 0.8541 pearson_pvalue: 0.0002 spearman_correlation: 0.9615 spearman_pvalue: 0.0000
These results from analyze_rrs_correlation test how well the R³S² scores correlate with raw average speed:
Both correlations are strong and statistically significant.
Pearson at 0.87 means there's a strong linear relationship between R³S² scores and the underlying speed data. The scoring isn't arbitrary — it tracks real speed differences well.
Spearman at 0.95 is even stronger, and since Spearman measures rank correlation, it tells you the ordering of routes by R³S² almost perfectly matches their ordering by raw speed. Routes ranked higher by R³S² are genuinely faster routes.
Both p-values are effectively 0, so there's no chance this is a fluke. The correlations are real.
The gap between Pearson and Spearman is worth noting. Spearman being higher than Pearson suggests the relationship is slightly non-linear — the scores track the ranking of routes very faithfully, but the magnitude of differences between scores isn't perfectly proportional to speed differences. That's fine and expected for a composite scoring system.
Bottom line: the R³S² methodology is sound. It's not just a black box — it's genuinely capturing what it's supposed to capture (route speed performance), and the rankings it produces are reliable.
2.2 Sensitivity Analysis
# Analyze sensitivity to outliers
sensitivity_results = analyzer.analyze_rrs_sensitivity()
print("Sensitivity Analysis:")
print(f"Rank correlation after removing outliers: {sensitivity_results['rank_correlation']:.4f}")
outliers_removed = sensitivity_results.get('outliers_removed', sensitivity_results.get('n_outliers_removed', 0))
print(f"Outliers removed: {outliers_removed}")
Sensitivity Analysis: Rank correlation after removing outliers: 1.0000 Outliers removed: 787
These come from analyze_rrs_sensitivity and analyze_rrs_stability. Together they answer two questions: does the scoring break when data is messy, and does it change depending on how much history you use?
Sensitivity — rank correlation of 1.0 after removing 721 outliers
A perfect 1.0 means the route rankings didn't change at all once outliers were stripped out. That's a strong result. Despite 721 anomalous readings in the dataset, the R³S² scores are completely robust to them — the outliers aren't driving the rankings. You can trust the scores even knowing the data has noise.
2.3 Stability Analysis
# Analyze stability across different window sizes
stability_results = analyzer.analyze_rrs_stability()
print("Stability Analysis (Correlation Matrix):")
display(stability_results)
Stability Analysis (Correlation Matrix):
| 5d | 10d | 15d | 20d | 30d | |
|---|---|---|---|---|---|
| 5d | 1.000000 | 0.989011 | 0.994505 | 0.994505 | 0.983516 |
| 10d | 0.989011 | 1.000000 | 0.994505 | 0.994505 | 0.989011 |
| 15d | 0.994505 | 0.994505 | 1.000000 | 1.000000 | 0.994505 |
| 20d | 0.994505 | 0.994505 | 1.000000 | 1.000000 | 0.994505 |
| 30d | 0.983516 | 0.989011 | 0.994505 | 0.994505 | 1.000000 |
Stability — all correlations above 0.965
Every window size (5d through 30d) produces rankings that are highly correlated with every other window size. The lowest value in the entire matrix is 0.965, between 5d and 30d, which is still very high. A few things stand out:
10d and 20d are identical (correlation = 1.0), suggesting those two windows happen to capture the same dominant patterns in this dataset.
The 5d window diverges slightly from 30d (0.966), which makes sense — a very short window is more sensitive to recent fluctuations, while 30d smooths over them.
15d sits in the sweet spot, correlating above 0.984 with everything.
Bottom line: the scoring system is both robust and stable. It doesn't matter much whether you use a 10, 15, or 20-day window — you'll get essentially the same route rankings. And those rankings hold up even when the data contains hundreds of outliers. This is about as good a validation result as you'd want to see.
2.4 Data Quality Analysis
# Analyze data completeness
completeness = analyzer.analyze_data_completeness()
# print("Data Completeness by Route:")
# display(completeness.sort_values(['completeness_pct'], ascending=False))
hour_avg = completeness.groupby('hour')['completeness_pct'].mean().reset_index().set_index('hour')
hour_avg.columns = ['avg_completeness_pct']
hour_avg['avg_completeness_pct'] = hour_avg['avg_completeness_pct'].round(2)
display(hour_avg)
| avg_completeness_pct | |
|---|---|
| hour | |
| 0 | 95.29 |
| 1 | 97.72 |
| 2 | 98.21 |
| 3 | 98.21 |
| 4 | 98.24 |
| 5 | 98.69 |
| 6 | 2.43 |
| 7 | 46.14 |
| 8 | 78.28 |
| 9 | 76.82 |
| 10 | 89.93 |
| 11 | 88.51 |
| 12 | 90.94 |
| 13 | 96.78 |
| 14 | 96.22 |
| 15 | 98.17 |
| 16 | 97.65 |
| 17 | 95.29 |
| 18 | 84.05 |
| 19 | 98.17 |
| 20 | 94.27 |
| 21 | 97.23 |
| 22 | 96.75 |
| 23 | 93.82 |
A few clear things emerge from this view:
Hour 15 (3pm) is the most complete slot. Multiple routes hit 99.82% at that hour — essentially perfect coverage. Afternoon data is the most reliable in this dataset.
Hour 6 (6am) is the worst. Several routes have only 1 actual reading out of 187 expected — 0.53% completeness. That's not a data gap, that's almost no data at all for early morning. This is a significant blind spot for any analysis involving morning commute patterns.
The gap between best and worst is enormous — 99.8% vs 0.5%. That's not a gradual degradation, it's a cliff. This suggests the data collection system either wasn't running during early morning hours for most of the dataset's history, or 6am coverage was only added very recently (hence just 1 reading).
Practical implications:
Any analysis of morning rush (6-8am) should be treated with caution — the data is too sparse to be representative
The
time_categorybuckets incompute_temporal_featureslabel hour 6 asmorning— results for that category will be heavily skewed by missing dataThe R³S² scores and forecasts for early morning hours are unreliable for affected routes
plot_forecastandplot_best_travel_timesmay give misleading recommendations for early morning departure times
Worth flagging in generate_recommendations if it doesn't already catch routes/hours below a completeness threshold.
2.5 Generate Recommendations
# Generate actionable recommendations
recommendations = analyzer.generate_recommendations()
print("Recommendations:")
if not recommendations:
print("No recommendations generated.")
else:
for i, rec in enumerate(recommendations, 1):
severity = str(rec.get("severity", "info")).upper()
rec_type = rec.get("type", "general")
description = rec.get("description", "No description provided.")
benefit = rec.get("expected_benefit", "N/A")
print(f"{i}. [{severity}] {rec_type}")
print(f" - {description}")
print(f" - Expected benefit: {benefit}")
Recommendations: 1. [MEDIUM] non_normal_distribution - 13 routes have non-normal speed distributions. Consider using median-based or percentile-based scoring methods instead of mean. - Expected benefit: More robust rankings less sensitive to outliers 2. [HIGH] systematic_collection_failure - Hours [6, 7, 8, 9, 10, 11, 18] have below 90% data coverage across all routes (avg completeness: 66.6%). This indicates a systematic collection failure (e.g. a scheduled scraper not running) rather than random missing data. These hours cannot be reliably interpolated and should be excluded from time-of-day comparisons and travel time recommendations. - Expected benefit: Prevents misleading conclusions from structurally absent data
3. VisualizationEngine - Temporal Patterns
Create visualizations to understand temporal patterns:
from visualization_engine import VisualizationEngine
# Initialize visualization engine
viz = VisualizationEngine(traffic_df, routes_df)
3.1 Hourly Heatmap
How to read it:
Each cell is the average speed for that route at a given hour (x-axis) on a given day of week (y-axis), aggregated across the entire dataset. Darker = faster in the current scheme. You're looking for patterns like "is Friday evening consistently slower than Tuesday morning?" — the heatmap makes those weekly rhythms immediately visible.
import ipywidgets as widgets
from IPython.display import display, clear_output
# Build dropdown options: friendly label → route_code
route_options = {
viz._get_route_label(r): r
for r in traffic_df['route_code'].unique()
}
dropdown = widgets.Dropdown(
options=route_options,
description='Route:',
layout=widgets.Layout(width='400px')
)
out = widgets.Output()
def on_change(change):
if change['type'] == 'change' and change['name'] == 'value':
with out:
clear_output(wait=True)
viz.plot_hourly_heatmap(change['new'])
dropdown.observe(on_change)
# Show initial plot
with out:
viz.plot_hourly_heatmap(dropdown.value)
display(dropdown, out)
Dropdown(description='Route:', layout=Layout(width='400px'), options={'North Inner Ring': '2HM2+P8|XJV5+RG', '…
Output()
3.2 Time Series Decomposition
This is a time series decomposition breaking the signal into three components:
Original — speeds oscillate between ~18 and ~35 km/h throughout the dataset. The regular spiky pattern is the daily rush hour cycle repeating every day.
Trend — the underlying long-term movement in average speed, stripped of daily/weekly cycles. A few things stand out:
Speeds were recovering through October 2025, peaked around late October (~25.5 km/h), then dipped in November
Relatively stable Dec 2025 through Jan 2026 (~24.5-25.5 km/h)
A noisy/unstable patch around Feb 2026 — that jagged section likely corresponds to the data collection issues or a real disruption (construction, event, etc.)
A sharp spike upward in mid-March 2026 followed by a steep drop toward April 2026 — that's unusual and worth investigating. Could be a holiday period (free-flowing traffic) followed by a return to congestion, or a data artifact
Seasonal (Weekly) — the repeating ±5 km/h weekly pattern is very consistent throughout, meaning the day-of-week effect is stable and predictable. Weekends reliably differ from weekdays by about 5 km/h.
Residual — the unexplained noise after removing trend and seasonality. Mostly within ±5 km/h and fairly uniform, which is healthy. The slightly larger residuals toward March/April suggest something unusual happening in that period that the model isn't capturing — consistent with the trend anomaly above.
# Build dropdown options: friendly label → route_code
route_options = {
viz._get_route_label(r): r
for r in viz.df['route_code'].unique()
}
dropdown = widgets.Dropdown(
options=route_options,
description='Route:',
layout=widgets.Layout(width='400px')
)
out = widgets.Output()
def on_change(change):
if change['type'] == 'change' and change['name'] == 'value':
with out:
clear_output(wait=True)
viz.plot_time_series_decomposition(change['new'])
dropdown.observe(on_change)
# Show initial plot
with out:
viz.plot_time_series_decomposition(dropdown.value)
display(dropdown, out)
Dropdown(description='Route:', layout=Layout(width='400px'), options={'North Inner Ring': '2HM2+P8|XJV5+RG', '…
Output()
3.3 Hour-of-Day Profiles
route_checkboxes = [
widgets.Checkbox(
value=True,
description=viz._get_route_label(route_code),
indent=False,
layout=widgets.Layout(width='auto')
)
for route_code in viz.routes
]
select_toggle = widgets.ToggleButton(
value=True,
description='Deselect all',
tooltip='Select all or deselect all routes',
button_style=''
)
checkbox_grid = widgets.GridBox(
children=route_checkboxes,
layout=widgets.Layout(
grid_template_columns='repeat(4, minmax(180px, 1fr))',
grid_gap='8px 16px',
width='100%'
)
)
controls = widgets.HBox([select_toggle])
out = widgets.Output()
_updating_selection = False
def get_selected_routes():
return [route_code for route_code, checkbox in zip(viz.routes, route_checkboxes) if checkbox.value]
def refresh_toggle_label():
selected_count = sum(checkbox.value for checkbox in route_checkboxes)
if selected_count == len(route_checkboxes):
select_toggle.description = 'Deselect all'
elif selected_count == 0:
select_toggle.description = 'Select all'
else:
select_toggle.description = 'Select all'
def render_plot():
selected_routes = get_selected_routes()
with out:
clear_output(wait=True)
if not selected_routes:
print('Selected routes: None')
fig, ax = plt.subplots(figsize=(14, 7))
ax.set_xlabel('Hour of Day', fontsize=12, fontweight='bold')
ax.set_ylabel('Average Speed (km/h)', fontsize=12, fontweight='bold')
ax.set_title('Hour-of-Day Speed Profiles: No routes selected\n'
'(Shaded regions show ±1 standard deviation)',
fontsize=14, fontweight='bold', pad=20)
ax.set_xticks(range(0, 24, 2))
ax.set_xticklabels([viz._format_hour_label(h) for h in range(0, 24, 2)],
rotation=45, ha='right')
ax.set_xlim(0, 23)
ax.margins(x=0)
ax.grid(True, alpha=0.3, linestyle='--', linewidth=0.5)
ax.set_ylim(bottom=0)
plt.tight_layout()
plt.show()
elif set(selected_routes) == set(viz.routes):
print('Selected routes: All routes')
viz.plot_hour_of_day_profiles()
else:
selected_labels = ', '.join(viz._get_route_label(r) for r in selected_routes)
print(f'Selected routes: {selected_labels}')
viz.plot_hour_of_day_profiles(selected_routes)
refresh_toggle_label()
def on_checkbox_change(change):
global _updating_selection
if _updating_selection:
return
if change['type'] == 'change' and change['name'] == 'value':
render_plot()
def on_toggle_change(change):
global _updating_selection
if change['type'] != 'change' or change['name'] != 'value' or _updating_selection:
return
_updating_selection = True
try:
for checkbox in route_checkboxes:
checkbox.unobserve(on_checkbox_change, names='value')
checkbox.value = change['new']
checkbox.observe(on_checkbox_change, names='value')
render_plot()
finally:
_updating_selection = False
for checkbox in route_checkboxes:
checkbox.observe(on_checkbox_change, names='value')
select_toggle.observe(on_toggle_change, names='value')
# Show initial plot first, then controls below it
render_plot()
display(out)
display(controls)
display(checkbox_grid)
Output()
HBox(children=(ToggleButton(value=True, description='Deselect all', tooltip='Select all or deselect all routes…
GridBox(children=(Checkbox(value=True, description='North Inner Ring', indent=False, layout=Layout(width='auto…
4. Comparative Performance Visualizations
4.1 Parallel Coordinates Plot
route_checkboxes = [
widgets.Checkbox(
value=True,
description=viz._get_route_label(route_code),
indent=False,
layout=widgets.Layout(width='auto')
)
for route_code in viz.routes
]
select_toggle = widgets.Button(
description='Deselect all',
tooltip='Select all or deselect all routes',
button_style=''
)
checkbox_grid = widgets.GridBox(
children=route_checkboxes,
layout=widgets.Layout(
grid_template_columns='repeat(4, minmax(180px, 1fr))',
grid_gap='8px 16px',
width='100%'
)
)
controls = widgets.HBox([select_toggle])
out = widgets.Output()
_updating_selection = False
def get_selected_routes():
return [route_code for route_code, checkbox in zip(viz.routes, route_checkboxes) if checkbox.value]
def refresh_toggle_label():
selected_count = sum(checkbox.value for checkbox in route_checkboxes)
if selected_count == len(route_checkboxes):
select_toggle.description = 'Deselect all'
elif selected_count == 0:
select_toggle.description = 'Select all'
else:
select_toggle.description = 'Select all'
def render_plot():
selected_routes = get_selected_routes()
with out:
clear_output(wait=True)
if not selected_routes:
print('Selected routes: None')
fig, ax = plt.subplots(figsize=(14, 8))
ax.set_title('Multi-Dimensional Route Comparison\n(Parallel Coordinates)', fontsize=14, fontweight='bold', pad=20)
ax.set_xlabel('Metrics')
ax.set_ylabel('Normalized Value (0-1 scale)')
ax.set_xticks([])
ax.set_yticks([])
ax.grid(True, axis='y', alpha=0.3, linestyle='--')
ax.set_axisbelow(True)
plt.tight_layout()
plt.show()
elif set(selected_routes) == set(viz.routes):
print('Selected routes: All routes')
viz.plot_parallel_coordinates()
else:
selected_labels = ', '.join(viz._get_route_label(r) for r in selected_routes)
print(f'Selected routes: {selected_labels}')
viz.plot_parallel_coordinates(route_codes=selected_routes)
refresh_toggle_label()
def on_checkbox_change(change):
global _updating_selection
if _updating_selection:
return
if change['type'] == 'change' and change['name'] == 'value':
render_plot()
def on_toggle_click(_):
global _updating_selection
if _updating_selection:
return
selected_count = sum(checkbox.value for checkbox in route_checkboxes)
target_value = selected_count != len(route_checkboxes)
_updating_selection = True
try:
for checkbox in route_checkboxes:
checkbox.unobserve(on_checkbox_change, names='value')
checkbox.value = target_value
checkbox.observe(on_checkbox_change, names='value')
render_plot()
finally:
_updating_selection = False
for checkbox in route_checkboxes:
checkbox.observe(on_checkbox_change, names='value')
select_toggle.on_click(on_toggle_click)
# Show initial plot first, then controls below it
render_plot()
display(out)
display(controls)
display(checkbox_grid)
Output()
HBox(children=(Button(description='Deselect all', style=ButtonStyle(), tooltip='Select all or deselect all rou…
GridBox(children=(Checkbox(value=True, description='North Inner Ring', indent=False, layout=Layout(width='auto…
4.2 Radar Chart
# Compare routes across multiple dimensions
viz.plot_radar_chart()
4.3 Time-of-Day Facets
# Speed distributions by time of day
viz.plot_time_of_day_facets()
5. Anomaly Detection
5.1 Control Chart
# Statistical process control chart
viz.plot_control_chart(example_route)
5.2 Anomaly Scatter Plot
# Contextual anomaly detection
viz.plot_anomaly_scatter(example_route)
5.3 Deviation Timeline
# Timeline of deviations from expected patterns
viz.plot_deviation_timeline(example_route)
5.4 Outlier Summary
# Get top anomalous observations
outliers = viz.generate_outlier_summary(top_n=20)
print("Top 20 Anomalous Observations:")
display(outliers)
6. Predictive Insights
6.1 Speed Forecast
# Forecast next 24 hours
viz.plot_forecast(example_route, forecast_hours=24)
6.2 Typical Day Profile
# Show typical patterns for each day of week
viz.plot_typical_day_profile()
6.3 Best Travel Times
# Identify optimal departure times
viz.plot_best_travel_times()
7. Interactive Dashboard Components
7.1 Create Interactive Widgets
# Create route selector
route_selector = viz.create_route_selector()
display(route_selector)
# Create time range slider
time_slider = viz.create_time_range_slider()
display(time_slider)
# Create aggregation toggle
agg_toggle = viz.create_aggregation_toggle()
display(agg_toggle)
7.2 Interactive Linked Plots (Plotly)
# Create interactive linked plots with hover tooltips
fig = viz.create_linked_plots(route_codes=traffic_df['route_code'].unique()[:3])
fig.show()
7.3 Summary Table with Filters
# Create filtered summary table
summary = viz.create_summary_table(
route_codes=route_selector.value if hasattr(route_selector, 'value') else None,
aggregation=agg_toggle.value if hasattr(agg_toggle, 'value') else 'D'
)
print("Summary Statistics:")
display(summary)
8. Export Report
# Export comprehensive HTML report
report_path = viz.export_report_template(
'traffic_analysis_report.html',
route_codes=traffic_df['route_code'].unique()[:5],
include_visualizations=True
)
print(f"Report exported to: {report_path}")
9. Alternative Scoring Methods
# Compare different scoring methods
comparison = analyzer.compare_scoring_methods()
print("Scoring Method Comparison:")
if isinstance(comparison, dict):
corr_matrix = comparison.get('correlation_matrix')
stability_metrics = comparison.get('stability')
else:
corr_matrix = comparison
stability_metrics = None
print("\nCorrelation Matrix:")
print(corr_matrix)
print("\nStability Metrics:")
if stability_metrics is not None:
print(stability_metrics)
else:
print("Not provided separately; refer to correlation matrix above.")
10. Statistical Tests
# Test normality
normality_results = analyzer.test_normality()
print("\nNormality Tests:")
if isinstance(normality_results, pd.DataFrame):
cols = [c for c in ['route_code', 'shapiro_pvalue', 'anderson_statistic'] if c in normality_results.columns]
print(normality_results[cols].head(3).to_string(index=False))
else:
for route, result in list(normality_results.items())[:3]:
print(f"\n{route}:")
print(f" Shapiro-Wilk p-value: {result['shapiro_pvalue']:.4f}")
print(f" Anderson-Darling statistic: {result['anderson_statistic']:.4f}")
Conclusion
This notebook demonstrated the key features of the Traffic Analysis Enhancement system:
- R³S² Analysis: Comprehensive evaluation of the scoring methodology
- Temporal Patterns: Understanding traffic patterns over time
- Comparative Analysis: Comparing routes across multiple dimensions
- Anomaly Detection: Identifying unusual traffic conditions
- Predictive Insights: Forecasting and optimal travel time recommendations
- Interactive Dashboards: Dynamic filtering and exploration
- Report Generation: Exportable analysis summaries
For more details, see the documentation in TRAFFIC_ANALYSIS_README.md.