-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathdays_with_no_cancellations.py
More file actions
143 lines (126 loc) · 5.46 KB
/
Copy pathdays_with_no_cancellations.py
File metadata and controls
143 lines (126 loc) · 5.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
"""Days without cancellations quality score dashboard card.
Integrates the "orion" group materials from orion/days_with_no_cancellations.py.
"""
from __future__ import annotations
import base64
import datetime
import pandas as pd
from openbus_hack import (
AnalysisRequest,
AnalysisResult,
Table,
analysis,
bar_chart,
metrics,
)
from orion.days_with_no_cancellations import (
daily_report,
operator_line_scores,
plot_operator_scores,
)
@analysis(
name="days-with-no-cancellations",
title="Days with zero cancellations",
description="Quality score based on how many days all buses operated as expected (zero cancellations). "
"A day with >=1 cancellation counts as 0, a day with full operations is 1.",
author="orion",
tags=["reliability", "cancellations", "data.gov.il"],
inputs=["lines", "operators", "dates"],
)
def run(req: AnalysisRequest):
days_back = 15 # Core metric looks at last 15 days
# Default window end date (retaining LAG_DAYS to be consistent with data freshness)
end_date = min(req.date_to, datetime.date.today() - datetime.timedelta(days=1))
# ── Method 1: Single Line ────────────────────────────────────────────────
if req.line:
operators = req.operators if req.operators else None
df = daily_report(req.line, days=days_back, end=end_date, operators=operators)
if df.empty:
return metrics(
("No data", 0),
notes=[
f"No planned rides for line {req.line} in the last {days_back} days.",
"Ensure you have selected the correct operator or clear filters.",
]
)
# Reshape to long format for bar_chart (Operated vs Cancelled)
df["date_str"] = df["date"].astype(str)
df["Operated"] = df["planned"] - df["cancelled"]
df_long = df.melt(
id_vars="date_str",
value_vars=["Operated", "cancelled"],
var_name="Status",
value_name="rides",
)
good_days = int(df["good"].sum())
total_days = len(df)
score = good_days / total_days if total_days > 0 else 0.0
return bar_chart(
df_long,
x="date_str",
y="rides",
series="Status",
stacked=True,
title=f"Line {req.line} — Daily cancellations and runs",
subtitle=f"Score: {score:.2f} ({good_days}/{total_days} clean days in the last {days_back} days)",
notes=[
"A 'clean day' is one with absolutely zero cancellations.",
"Data source is the Ministry's rides execution endpoint (/rides_execution/list), "
"where scheduled departures with no matching GPS arrival record represent cancellations.",
],
)
# ── Method 2: Operator Overview ──────────────────────────────────────────
else:
operator = req.operator or "סופרבוס" # Fallback to Superbus
scores = operator_line_scores(
operator, days=days_back, end=end_date, max_lines=15, progress=False
)
if scores.empty:
return metrics(
("No data", 0),
notes=[f"No lines with planned rides found for operator {operator}."]
)
# Sort scores so that worst performing lines (lowest scores) are at the top
scores_sorted = scores.sort_values("score", ascending=True).copy()
# Expose precise numbers as the relief table
t = Table(
columns=["line", "score", "good_days", "days_scored", "planned", "cancelled"],
rows=[
[
row.line,
f"{row.score:.2f}",
int(row.good_days),
int(row.days_scored),
int(row.planned),
int(row.cancelled),
]
for row in scores.itertuples()
],
)
notes = [
"Each bar represents the fraction of days (out of last 15) with zero cancellations (lower is worse!).",
"Lines with zero actual GPS reports are shown with score 1.0 but hatched in draft to indicate data gaps rather than actual 100% cancellations.",
"Toggle 'Table view' at the top-right of the card to see exact planned, operated, and cancelled counts for each line.",
f"Data retrieved live from /rides_execution/list.",
]
# Return primary interactive React bar chart, with fallback table and image_png
res = bar_chart(
scores_sorted,
x="line",
y="score",
horizontal=True,
title=f"{operator} — Days without cancellations",
subtitle=f"Scores for worst performing lines, last {days_back} days",
x_label="line",
y_label="days with zero cancellations / days scored",
notes=notes,
)
res.table = t
try:
fig_path = plot_operator_scores(scores, operator, days=days_back)
png_bytes = fig_path.read_bytes()
res.image_png = base64.b64encode(png_bytes).decode("ascii")
except Exception as exc:
notes.append(f"Matplotlib render failed: {exc}")
res.notes = notes
return res