Why Are My Crashes on the Wrong Day of the Week?

On this page

Problem statement

You chart fatal crashes by day of the week and Monday comes out as one of the worst days, nearly as bad as the weekend. It is not. Crash datasets code the weekday as a number, and both FARS and STATS19 start their week on Sunday: 1 is Sunday, 7 is Saturday. Python starts on Monday: pandas' dayofweek is 0 for Monday, and isoweekday is 1 for Monday. Map one convention onto the other and every crash moves one day, so Sunday's crashes are labelled Monday and Saturday's Sunday, with no error and a plausible-looking chart.

This page checks the weekday codes of FARS 2023 and STATS19 2023 against their own dates, shows the chart that the off-by-one produces, and recomputes the weekday from the date, which removes the question entirely.

Quick answer

Compute the weekday from the date instead of trusting a code, or check the code against it:

A["weekday"] = A.date.dt.day_name()
check = pd.crosstab(A.DAY_WEEK, A.weekday)
print(check[["Sunday", "Monday", "Saturday"]].to_string())
print(f"FARS DAY_WEEK 1 is {list(A[A.DAY_WEEK == 1].weekday.unique())}; 7 is {list(A[A.DAY_WEEK == 7].weekday.unique())}")
weekday   Sunday  Monday  Saturday
DAY_WEEK                          
1           6054       0         0
2              0    4872         0
3              0       0         0
4              0       0         0
5              0       0         0
6              0       0         0
7              0       0      6600
FARS DAY_WEEK 1 is ['Sunday']; 7 is ['Saturday']

Every crash coded 1 fell on a Sunday and every crash coded 7 on a Saturday: the code is consistent, just Sunday-first.

Two bar charts of the same 37,769 US fatal crashes in 2023 by weekday: labelled from the DAY_WEEK code with a Monday-first list, Monday shows 6,054 crashes and Sunday 6,600, making Monday look nearly as bad as the weekend; computed from the dates, Monday has 4,872, Saturday 6,600 is the busiest and Sunday 6,054 second. Two bar charts of the same 37,769 US fatal crashes in 2023 by weekday: labelled from the DAY_WEEK code with a Monday-first list, Monday shows 6,054 crashes and Sunday 6,600, making Monday look nearly as bad as the weekend; computed from the dates, Monday has 4,872, Saturday 6,600 is the busiest and Sunday 6,054 second.
Wrong labels and right labels.

See the wrong chart, check STATS19, look at weekends

1. See the wrong chart

A list of names starting on Monday, indexed by the FARS code, labels every day wrongly:

WRONG = dict(zip(range(1, 8), ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]))
wrong = A.DAY_WEEK.map(WRONG).value_counts()
right = A.weekday.value_counts()
order = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
print(pd.DataFrame({"labelled from the code, Monday first": wrong, "from the date": right}).loc[order].to_string())
           labelled from the code, Monday first  from the date
Monday                                     6054           4872
Tuesday                                    4872           4592
Wednesday                                  4592           4854
Thursday                                   4854           4974
Friday                                     4974           5823
Saturday                                   5823           6600
Sunday                                     6600           6054

Labelled Monday-first, Monday gets Sunday's 6,054 crashes and becomes the third-worst day; the real Monday, with 4,872, is the second-quietest. Every bar is one day out, and nothing in the chart looks wrong.

2. Check STATS19 the same way

STATS19's day_of_week uses the same convention, and its dates are written day first:

S["d"] = pd.to_datetime(S.date, format="%d/%m/%Y")
print(pd.crosstab(S.day_of_week, S.d.dt.day_name())[["Sunday", "Monday", "Saturday"]].to_string())
d            Sunday  Monday  Saturday
day_of_week                          
1             11699       0         0
2                 0   14288         0
3                 0       0         0
4                 0       0         0
5                 0       0         0
6                 0       0         0
7                 0       0     14182

STATS19 codes 1 for Sunday and 7 for Saturday too. Its injury collisions have a different weekly shape from US fatal crashes: 14,288 on Mondays against 11,699 on Sundays.

How different systems number the weekday, using Sunday 1 January 2023. Sunday, 1 January 2023, numbered five wayssystemSunday isweek startsrangeFARS DAY_WEEK1Sunday1-7STATS19 day_of_week1Sunday1-7pandas dayofweek6Monday0-6Python isoweekday7Monday1-7strftime %w0Sunday0-6From pandas to FARS: (dayofweek + 1) % 7 + 1 spatialworkflow.io
Five weekday conventions.

3. Look at the weekend properly

With correct labels, the busiest days for fatal crashes are Saturday and Sunday, and Monday is among the quietest:

r = right.loc[order]
print(r.to_string())
print(f"weekend share of fatal crashes: {(r['Saturday'] + r['Sunday']) / r.sum():.1%} (two days in seven: {2 / 7:.1%})")
weekday
Monday       4872
Tuesday      4592
Wednesday    4854
Thursday     4974
Friday       5823
Saturday     6600
Sunday       6054
weekend share of fatal crashes: 33.5% (two days in seven: 28.6%)

With the right labels, Saturday and Sunday hold 33.5% of fatal crashes, against 28.6% for two days in seven; Tuesday is the quietest day.

Three Python weekday conventions

The data used on this page

NHTSA's FARS 2023 crash file and the Department for Transport's STATS19 2023 collision file.

import zipfile
from pathlib import Path

import pandas as pd
import requests

Path("data").mkdir(exist_ok=True)
SRC = {"FARS2023NationalCSV.zip": "https://static.nhtsa.gov/nhtsa/downloads/FARS/2023/National/FARS2023NationalCSV.zip",
       "dft-road-casualty-statistics-collision-2023.csv": "https://data.dft.gov.uk/road-accidents-safety-data/dft-road-casualty-statistics-collision-2023.csv"}
for name, url in SRC.items():
    if not Path("data", name).exists():
        Path("data", name).write_bytes(requests.get(url, timeout=600).content)
A = pd.read_csv(zipfile.ZipFile("data/FARS2023NationalCSV.zip").open("FARS2023NationalCSV/accident.csv"), encoding="latin1", low_memory=False,
                usecols=["ST_CASE", "YEAR", "MONTH", "DAY", "DAY_WEEK", "HOUR"])
A["date"] = pd.to_datetime(dict(year=A.YEAR, month=A.MONTH, day=A.DAY))
S = pd.read_csv("data/dft-road-casualty-statistics-collision-2023.csv", low_memory=False, usecols=["collision_index", "date", "day_of_week", "time"])
print(f"FARS: {len(A):,} crashes, {(A.HOUR == 99).sum()} with an unknown hour; STATS19: {len(S):,} collisions, {S.time.isna().sum()} without a time")
FARS: 37,769 crashes, 271 with an unknown hour; STATS19: 104,258 collisions, 0 without a time

Example 1: the three Python conventions

pandas and the standard library number weekdays in three ways; only an explicit name avoids confusion:

d = pd.Timestamp("2023-01-01")                                         # a Sunday
print(f"{d:%A}: dayofweek {d.dayofweek}, isoweekday {d.isoweekday()}, strftime %w {d:%w}, FARS-style {(d.dayofweek + 1) % 7 + 1}")
print(f"FARS code from pandas: (dayofweek + 1) % 7 + 1 matches DAY_WEEK for {(((A.date.dt.dayofweek + 1) % 7 + 1) == A.DAY_WEEK).mean():.1%} of crashes")
Sunday: dayofweek 6, isoweekday 7, strftime %w 0, FARS-style 1
FARS code from pandas: (dayofweek + 1) % 7 + 1 matches DAY_WEEK for 100.0% of crashes

The same Sunday is 6 in pandas' dayofweek, 7 in isoweekday, 0 in %w and 1 in FARS; (dayofweek + 1) % 7 + 1 converts pandas to FARS exactly.

Grouped bar chart of weekday profiles in 2023 computed from the dates: US fatal crashes rise from about 12 to 13 percent on weekdays to 15.4 on Friday, 17.5 on Saturday and 16.0 on Sunday; Great Britain's injury collisions are highest on weekdays, 13.7 to 16.4 percent, and lowest at weekends, 13.6 on Saturday and 11.2 on Sunday. Grouped bar chart of weekday profiles in 2023 computed from the dates: US fatal crashes rise from about 12 to 13 percent on weekdays to 15.4 on Friday, 17.5 on Saturday and 16.0 on Sunday; Great Britain's injury collisions are highest on weekdays, 13.7 to 16.4 percent, and lowest at weekends, 13.6 on Saturday and 11.2 on Sunday.
Weekday profiles, US and GB.

No single weekday numbering

Date and time checks before counting crashes by day or hour. before counting by day or hourweekday from the datenot from a codecross-tab code against date onceFARS 1 = Sundayday-first datesSTATS19: %d/%m/%Yunknown hour codesFARS HOUR 99: 271 crasheslabels in the fileDAY_WEEKNAME when presentThe off-by-one chart looks as plausible as the right one spatialworkflow.io
Before counting by day or hour.

There is no single convention for numbering weekdays. ISO 8601, which Python's isoweekday follows, counts Monday as 1 and Sunday as 7; pandas' dayofweek counts Monday as 0; C's %w and the US habit of a Sunday-first calendar count Sunday as 0 or 1. FARS and STATS19 both use 1 for Sunday. Each is internally consistent, and an analysis that crosses them without checking shifts every weekday by one.

The error survives because it produces a believable chart. Fatal crashes peak at the weekend; shifted by a day, they peak on Sunday and Monday, and a story about Monday-morning drivers writes itself. The cure is to stop translating codes: compute the weekday from the date, which every crash dataset has, or at least cross-tabulate the code against the computed weekday once, as here. The same habit applies to months that start at 0 in some libraries and to hours where midnight may be 0 or 24.

Unknown hours, day-first dates and night crashes

Weekday and time codes in crash data: problems and fixes. weekday trapsTrapInsteadcodes labelled Monday firstFARS and STATS19: 1 is Sundaydayofweek mixed with codesday_name() from the datedates parsed month firstSTATS19 format="%d/%m/%Y"hours that do not add updrop HOUR 99 and say soFARS and STATS19, 2023 spatialworkflow.io
Weekday traps.
  • Unknown hours: FARS uses 99 for an unknown hour, 271 crashes in 2023; drop them before counting by hour, or the 24 hours will not add up to the total.
  • Dates without times: weekday from a date is safe; weekday from a timestamp needs the right time zone near midnight.
  • Day-first dates: STATS19 writes dates as day/month/year; pass format="%d/%m/%Y" or months and days swap for the first twelve days of each month.
  • Night crashes: a crash at 01:00 on Saturday is a Friday-night crash in most people's minds; define "night of" separately if that matters.
  • Labels in the data: FARS files include DAY_WEEKNAME beside the code; use it when present.

FAQ

What does DAY_WEEK 1 mean in FARS?

Sunday; 7 is Saturday. STATS19's day_of_week uses the same convention.

Why does Monday look like a bad day for crashes in my chart?

Probably because the codes were labelled Monday-first; FARS's Sunday crashes, code 1, then appear as Monday.

How do I get the weekday from a date in pandas?

Use df.date.dt.day_name() for a label, or dt.dayofweek, where Monday is 0 and Sunday 6.

Which day has the most fatal crashes?

In FARS 2023, Saturday, followed by Sunday; the weekend holds about a third of fatal crashes.