Nabil Khoury
Work / Autonomy / Second Look
Autonomy2026Personal projectPublic NHTSA data

A second look at how AV crashes get labeled

Every crash involving a self-driving or driver-assist car is reported to the federal government, and the company picks the automation label itself. I built a tool that finds the reports where that label doesn’t match the rest of the record, and hands a short list to a person.

Problem

Researchers compare driver-assist and self-driving crashes, but companies set that label themselves and some are wrong.

Insight

Predicting the label is easy and useless. The value is in the few records where independent signals disagree.

Decision

Optimize for a short list a reviewer can trust, not for catching every error.

Result

4,589 reports narrowed to 66 for review. The model’s flags held 0.912 precision on errors I planted.

4,589crash reports checked
66sent to a person for review
3independent checks per record
0.912precision on errors I planted on purpose

Out of 4,589 reports, 66 go to a person

Each dot is one cleaned crash report from the public 2021 to 2025 archive.

Report, no flagFlagged by rules (37)Flagged by the model (20)Model and narrative disagree (9)
The product is the short list, not a verdict on any company. Highlighted positions are illustrative, the counts are real.

01Who it’s for

Journalists and safety researchersCite these crash counts in stories and papers. Need to know which labels to double-check before they quote a number.
People who audit the dataCatch mislabels one case at a time today. Need a ranked list with a reason attached to every flag.
Engineers building on the dataNeed to rerun the same check on every new data release and get the same answer.

02The problem

Every comparison between driver-assist and self-driving crashes depends on one label, and the companies assign it themselves.

NHTSA has said publicly that some reports are misclassified and that it works with companies to correct them. That happens case by case. There was no public way to check the whole dataset, so most people take the label at face value.

03The insight

A model can predict the label very accurately and tell you almost nothing.

One field, who was operating the vehicle, nearly gives the answer away. So high accuracy was the wrong goal. The useful output is the small set of records where independent signals disagree with what was reported. That reframed the product from a classifier into a review queue.

04How I measured success

What I optimized

Precision of the review list, with at least 80% of flags being real problemsA reviewer who finds the first ten flags are noise stops reading. Inside that bar, catch as many errors as possible. There’s no answer key for real mislabels, so I planted errors on purpose and measured against those.

The obvious metric I rejected

Accuracy at predicting the labelIt looks impressive and measures the field that gives the answer away, not the problem.

05How it works

  1. Clean the dataDownload the public archive and keep the latest version of each report.4,589 reports
  2. Plain rulesFields that contradict the label, like how the car was moving before the crash.37 flags
  3. A modelPredicts the label from the other fields and flags strong disagreement.20 flags
  4. Read the storyA small local language model reads the written narrative.9 of 50 disagree
  5. Review queueAll signals merge into one list, each with its reason.66 to review

06Decisions

Flag fewer records, with more confidence
Why
Reviewer trust is the scarce resource. A long, noisy list gets ignored.
Instead of
Calibrated probabilities, which I tested. They made the list longer at lower confidence for about the same precision.
Cost
It finds only about 38% of the planted errors. I chose a list people will finish over one that catches more.
Three independent checks, not one better model
Why
Rules, a model on the fields, and a language model reading the narrative fail in different ways. Where they disagree is where a person should look.
Instead of
One bigger model whose mistakes I couldn’t explain to a reviewer.
Cost
More moving parts to keep reproducible.
Flags, never findings
Why
A result like this gets screenshotted. Every flag says “recommended for review,” and nothing ranks a company on safety.
Instead of
A list of “wrong reports,” which the data can’t support.
Cost
A less dramatic headline. A defensible one.
Read narratives with a local model, on a sample first
Why
It’s federal safety data, and a local model costs nothing per record. Starting with 50 proved the method before I spent the time on all of them.
Instead of
A cloud model on every record at once.
Cost
50 narratives is too few to say much about agreement rates yet.

07What I left out

Left outWhy
Crash rates or “which system is safer”The data has no miles-driven denominator. Any rate would be made up.
Predicting crash severityThat was an earlier draft. I dropped it to answer one question well.
A dashboard or live pipelineResearchers need runs they can reproduce against a dated data release, not a UI.
Guessing redacted fieldsRedactions protect people and companies. The tool respects them as they are.

08What happened

On 50 narratives, all three methods agreed on 41. The other 9 are exactly the records worth a second look.

When I moved to the larger 2021 to 2025 archive, the same precision bar moved the cutoff from 0.91 to 0.59 and recall on planted errors fell from 0.955 to 0.378. I wrote down why instead of retuning until the number looked good again.

I also checked 13 report IDs named in public federal investigations. 7 were in the archive, and none overlapped my flags. That’s a small, honest non-result, and I report it as one.

  • A flag is a reason to look, not a finding.
  • No safety rates. The data can’t support them.

09What’s next

  1. A real answer keyPrecision on planted errors isn’t precision on real ones. Confirmed corrections would be.first
  2. Read every narrative, not 50The narrative check is the most independent signal and has the smallest sample.then
  3. Use narrative text in the modelToday the model only sees structured fields.later

10Built with

PythonpandasLightGBMQwen 2.5 (local)NHTSA SGO data