Skip to content

Commit 2ef1bbb

Browse files
authored
Update for Daily Delay Tracking
* Daily Delay Updates Main Update: Added support for new daily delays stored in a new dynamo table. Weekly updates now call data from the new daily dynamo table for aggregation. * QoL and cloud config updates to daily delay tracking Fixed PR Comments: - Removed authorship comment - Stores initialized delay type dict as a constant for accessing in process.py (reducing duplicate code) - Delayed weekly delay update by 15 minutes to allow daily update to run - Cloud config support added in Chalice json files
1 parent 3b3a67c commit 2ef1bbb

8 files changed

Lines changed: 222 additions & 45 deletions

File tree

ingestor/.chalice/config.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,12 @@
106106
"lambda_memory_size": 256,
107107
"lambda_timeout": 90
108108
},
109-
"update_alert_delays": {
109+
"update_weekly_alert_delays": {
110+
"iam_policy_file": "policy-alert-delays.json",
111+
"lambda_memory_size": 256,
112+
"lambda_timeout": 300
113+
},
114+
"update_daily_alert_delays": {
110115
"iam_policy_file": "policy-alert-delays.json",
111116
"lambda_memory_size": 256,
112117
"lambda_timeout": 300

ingestor/.chalice/policy-alert-delays.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,15 @@
1414
"Effect": "Allow",
1515
"Action": ["dynamodb:BatchWriteItem"],
1616
"Resource": [
17-
"arn:aws:dynamodb:us-east-1:473352343756:table/AlertDelaysWeekly"
17+
"arn:aws:dynamodb:us-east-1:473352343756:table/AlertDelaysWeekly",
18+
"arn:aws:dynamodb:us-east-1:473352343756:table/AlertDelaysDaily"
19+
]
20+
},
21+
{
22+
"Effect": "Allow",
23+
"Action": ["dynamodb:Query"],
24+
"Resource": [
25+
"arn:aws:dynamodb:us-east-1:473352343756:table/AlertDelaysDaily"
1826
]
1927
}
2028
]

ingestor/.chalice/resources.json

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,28 @@
199199
}
200200
}
201201
},
202-
"UpdateAlertDelays": {
202+
"UpdateWeeklyAlertDelays": {
203+
"Type": "AWS::Serverless::Function",
204+
"Properties": {
205+
"Environment": {
206+
"Variables": {
207+
"DD_API_KEY": {
208+
"Ref": "DDApiKey"
209+
},
210+
"DD_VERSION": {
211+
"Ref": "GitVersion"
212+
},
213+
"DD_TAGS": {
214+
"Ref": "DDTags"
215+
},
216+
"DD_GIT_REPOSITORY_URL": {
217+
"Ref": "DDGitRepositoryUrl"
218+
}
219+
}
220+
}
221+
}
222+
},
223+
"UpdateDailyAlertDelays": {
203224
"Type": "AWS::Serverless::Function",
204225
"Properties": {
205226
"Environment": {
@@ -488,6 +509,39 @@
488509
]
489510
}
490511
},
512+
"AlertDelaysDailyDB": {
513+
"Type": "AWS::DynamoDB::Table",
514+
"Properties": {
515+
"TableName": "AlertDelaysDaily",
516+
"KeySchema": [
517+
{
518+
"AttributeName": "line",
519+
"KeyType": "HASH"
520+
},
521+
{
522+
"AttributeName": "date",
523+
"KeyType": "RANGE"
524+
}
525+
],
526+
"AttributeDefinitions": [
527+
{
528+
"AttributeName": "line",
529+
"AttributeType": "S"
530+
},
531+
{
532+
"AttributeName": "date",
533+
"AttributeType": "S"
534+
}
535+
],
536+
"BillingMode": "PAY_PER_REQUEST",
537+
"Tags": [
538+
{
539+
"Key": "service",
540+
"Value": "t-performance-dash"
541+
}
542+
]
543+
}
544+
},
491545
"DeliveredTripMetricsMonthlyDB": {
492546
"Type": "AWS::DynamoDB::Table",
493547
"Properties": {

ingestor/app.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,13 +122,22 @@ def update_trip_metrics(event):
122122
trip_metrics.ingest_recent_trip_metrics(lookback_days=7)
123123

124124

125-
# 8:30am UTC -> 3:30/4:30am ET every Monday and Tuesday
125+
# 8:30am UTC -> 3:45/4:45am ET every Monday and Tuesday
126126
# There's shouldn't be any benefit to running it more frequently.
127-
@app.schedule(Cron(30, 8, "?", "*", "MON,TUE", "*"))
128-
def update_alert_delays(event):
127+
# Runs 15 minutes after the daily update
128+
@app.schedule(Cron(45, 8, "?", "*", "MON,TUE", "*"))
129+
def update_weekly_alert_delays(event):
129130
today = datetime.now()
130131
one_week_ago = (today - timedelta(days=15)).date()
131-
delays.update_table(one_week_ago, today.date())
132+
delays.update_weekly_from_daily(one_week_ago, today.date())
133+
134+
135+
# for daily delay uploads
136+
@app.schedule(Cron(30, 8, "*", "*", "?", "*"))
137+
def update_daily_alert_delays(event):
138+
today = datetime.now()
139+
yesterday = (today - timedelta(days=1)).date()
140+
delays.update_table(yesterday, today.date())
132141

133142

134143
# Manually triggered lambda for populating daily trip metric tables. Only needs to be ran once.

ingestor/chalicelib/constants.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,3 +338,22 @@ def get_weekly_table_update_start():
338338
"line-orange": ["line-orange"],
339339
"line-mattapan": ["line-mattapan"],
340340
}
341+
342+
# Initializing all Delay types at 0 for processing
343+
DELAY_BY_TYPE = {
344+
"disabled_vehicle": 0,
345+
"signal_problem": 0,
346+
"power_problem": 0,
347+
"door_problem": 0,
348+
"brake_problem": 0,
349+
"switch_problem": 0,
350+
"track_issue": 0,
351+
"mechanical_problem": 0,
352+
"track_work": 0,
353+
"car_traffic": 0,
354+
"police_activity": 0,
355+
"medical_emergency": 0,
356+
"fire": 0,
357+
"flooding": 0,
358+
"other": 0,
359+
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
__all__ = ["update_table"]
1+
__all__ = ["update_table", "update_weekly_from_daily"]
22

3-
from .process import update_table
3+
from .process import update_table, update_weekly_from_daily

ingestor/chalicelib/delays/aggregate.py

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,50 @@ def group_weekly_data(df: pd.DataFrame, start_date: str):
4040
df_weekly = df_weekly.fillna(0)
4141
# Pandas resample uses the end date of the range as the index. So we subtract 6 days to convert to first date of the range.
4242
df_weekly.index = df_weekly.index - pd.Timedelta(days=6)
43-
# Drop the first week if it is incomplete
44-
if datetime.fromisoformat(start_date).weekday() != 0:
45-
df_weekly = df_weekly.tail(-1)
43+
4644
# Convert date back to string.
4745
df_weekly["date"] = df_weekly.index.strftime("%Y-%m-%d")
4846
df_weekly = df_weekly[df_weekly["line"] != 0]
4947
return df_weekly.to_dict(orient="records")
48+
49+
50+
# Added by Icebox00 (temporary note)
51+
def group_daily_data(df: pd.DataFrame, start_date: str):
52+
# Formats daily data on delays for Dynamo
53+
54+
df_daily = df.resample("D").agg(
55+
{
56+
"total_delay_time": np.sum,
57+
"disabled_vehicle": np.sum,
58+
"signal_problem": np.sum,
59+
"power_problem": np.sum,
60+
"brake_problem": np.sum,
61+
"door_problem": np.sum,
62+
"switch_problem": np.sum,
63+
"track_issue": np.sum,
64+
"track_work": np.sum,
65+
"car_traffic": np.sum,
66+
"mechanical_problem": np.sum,
67+
"police_activity": np.sum,
68+
"medical_emergency": np.sum,
69+
"flooding": np.sum,
70+
"fire": np.sum,
71+
"other": np.sum,
72+
"line": "min",
73+
}
74+
)
75+
76+
# Fill the empties with 0
77+
df_daily = df_daily.fillna(0)
78+
79+
# Convert date back to string.
80+
df_daily["date"] = df_daily.index.strftime("%Y-%m-%d")
81+
82+
df_daily = df_daily.reset_index(drop=True)
83+
84+
df_daily = df_daily[df_daily["line"] != 0]
85+
86+
# Select only the columns we need for storage
87+
daily_records = df_daily.to_dict(orient="records")
88+
89+
return daily_records

ingestor/chalicelib/delays/process.py

Lines changed: 75 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from datetime import date, timedelta
22
from decimal import Decimal
33
import json
4+
from boto3.dynamodb.conditions import Key
45
import re
56
from typing import List
67
from urllib.parse import urlencode
@@ -9,10 +10,11 @@
910
import requests
1011

1112
from chalicelib import constants, dynamo
12-
from chalicelib.delays.aggregate import group_weekly_data
13+
from chalicelib.delays.aggregate import group_weekly_data, group_daily_data
1314
from chalicelib.delays.types import Alert, AlertsRequest
1415

15-
TABLE_NAME = "AlertDelaysWeekly"
16+
WEEKLY_TABLE_NAME = "AlertDelaysWeekly"
17+
DAILY_TABLE_NAME = "AlertDelaysDaily"
1618

1719

1820
def generate_requests(start_date: date, end_date: date, lines=constants.ALL_LINES) -> List[AlertsRequest]:
@@ -193,23 +195,8 @@ def process_delay_time(alerts: List[Alert]):
193195
}
194196
)
195197
total_delay = 0
196-
delay_by_type = {
197-
"disabled_vehicle": 0,
198-
"signal_problem": 0,
199-
"power_problem": 0,
200-
"door_problem": 0,
201-
"brake_problem": 0,
202-
"switch_problem": 0,
203-
"track_issue": 0,
204-
"mechanical_problem": 0,
205-
"track_work": 0,
206-
"car_traffic": 0,
207-
"police_activity": 0,
208-
"medical_emergency": 0,
209-
"fire": 0,
210-
"flooding": 0,
211-
"other": 0,
212-
}
198+
delay_by_type = constants.DELAY_BY_TYPE.copy()
199+
213200
for delay in delays:
214201
if (delay is None) or (len(delay) == 0):
215202
continue
@@ -227,16 +214,21 @@ def process_requests(requests: List[AlertsRequest], lines=constants.ALL_LINES):
227214

228215
for request in requests:
229216
data = process_single_day(request)
217+
# Initializing at 0 regardless of condition
218+
total_delay = 0
219+
delay_by_type = constants.DELAY_BY_TYPE.copy()
220+
230221
if data is not None and len(data) != 0:
231222
total_delay, delay_by_type = process_delay_time(data)
232-
all_data[request.route].append(
233-
{
234-
"date": request.date.isoformat(),
235-
"line": request.route,
236-
"total_delay_time": total_delay,
237-
"delay_by_type": delay_by_type,
238-
}
239-
)
223+
# We should always append zero records just in case
224+
all_data[request.route].append(
225+
{
226+
"date": request.date.isoformat(),
227+
"line": request.route,
228+
"total_delay_time": total_delay,
229+
"delay_by_type": delay_by_type,
230+
}
231+
)
240232

241233
df_data = {}
242234
for line in lines:
@@ -249,6 +241,55 @@ def process_requests(requests: List[AlertsRequest], lines=constants.ALL_LINES):
249241
return df_data
250242

251243

244+
def get_daily_data_for_week(start_date: date, end_date: date, lines=constants.ALL_LINES):
245+
"""
246+
Query daily data from DynamoDB for weekly aggregation.
247+
Done as opposed to performing another API call for weekly aggregation.
248+
Also guarantees complete info for every day.
249+
"""
250+
daily_records = []
251+
252+
for line in lines:
253+
# Convert dates to ISO format strings to match DynamoDB storage format
254+
start_date_str = start_date.isoformat()
255+
end_date_str = end_date.isoformat()
256+
257+
# Query DynamoDB for this line and date range
258+
params = {
259+
"KeyConditionExpression": Key("line").eq(line)
260+
& Key("date").between(start_date_str, end_date_str) # Use string format
261+
}
262+
try:
263+
line_data = dynamo.query_dynamo(params, DAILY_TABLE_NAME)
264+
daily_records.extend(line_data)
265+
except Exception as e:
266+
print(f"Error querying {line}: {e}")
267+
268+
return daily_records
269+
270+
271+
def update_weekly_from_daily(start_date: date, end_date: date, lines=constants.ALL_LINES):
272+
"""
273+
Update weekly table by aggregating daily data from DynamoDB.
274+
Avoids another api call.
275+
"""
276+
277+
# Get daily data from DynamoDB instead of API
278+
daily_records = get_daily_data_for_week(start_date, end_date, lines)
279+
280+
# Convert to DataFrame and process
281+
df = pd.DataFrame(daily_records)
282+
df["date"] = pd.to_datetime(df["date"])
283+
df.set_index("date", inplace=True)
284+
285+
# Aggregate weekly for each line
286+
weekly_data = []
287+
for line, line_df in df.groupby("line"):
288+
weekly_data.extend(group_weekly_data(line_df, start_date.isoformat()))
289+
290+
dynamo.dynamo_batch_write(json.loads(json.dumps(weekly_data), parse_float=Decimal), WEEKLY_TABLE_NAME)
291+
292+
252293
def update_table(start_date: date, end_date: date, lines=constants.ALL_LINES):
253294
"""
254295
Update the table with rapid transit data
@@ -258,12 +299,13 @@ def update_table(start_date: date, end_date: date, lines=constants.ALL_LINES):
258299

259300
grouped_data = []
260301
for line, df in all_data.items():
261-
grouped_data.extend(group_weekly_data(df, start_date.isoformat()))
262-
263-
dynamo.dynamo_batch_write(json.loads(json.dumps(grouped_data), parse_float=Decimal), TABLE_NAME)
302+
grouped_data.extend(group_daily_data(df, start_date.isoformat()))
303+
dynamo.dynamo_batch_write(json.loads(json.dumps(grouped_data), parse_float=Decimal), DAILY_TABLE_NAME)
264304

265305

306+
# Testing daily updates. Using random dates. Feel free to change and uncomment as needed.
266307
if __name__ == "__main__":
267-
start_date = date(2025, 6, 1)
268-
end_date = date(2025, 8, 10)
269-
update_table(start_date, end_date, constants.ALL_LINES)
308+
start_date = date(2025, 9, 15)
309+
end_date = date(2025, 9, 21)
310+
# update_table(start_date, end_date, constants.ALL_LINES)
311+
# update_weekly_from_daily(start_date, end_date, constants.ALL_LINES)

0 commit comments

Comments
 (0)