Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.md

NJ DOT Car Crash Data

Analysis of NJ DOT car crash data.

Plots:

Methods:

Plots

I've only done a very quick first pass at cleaning and plotting the data here, so take these with a grain of salt.

There is a marked decrease in "injury" and "property damage" crashes since the onset of COVID (≈March 2020), but fatal crashes are roughly flat:

Crashes per Month (Statewide)

Injuries per Month (Statewide)

Property Damage Crashes per Month (Statewide)

Deaths per Month (Statewide)

Crashes per {County, Month}

Injuries per {County, Month}

Property Damage Crashes per {County, Month}

Deaths per {County, Month}

Crashes per Year (Statewide)

Injuries per Year (Statewide)

Property Damage Crashes per Year (Statewide)

Deaths per Year (Statewide)

Crashes per {County, Year}

Injuries per {County, Year}

Property Damage Crashes per {County, Year}

Deaths per {County, Year}

Crash-Type Percentages

Injuries, Property Damage, Deaths (as Percentage of All Crashes)

Deaths (as Percentage of All Crashes)

Methods

rawdata.py is a CLI for downloading+caching .zips, extracting .txts, cleaning+converting to .pqt (Parquet).

./rawdata.py --help
# Usage: rawdata.py [OPTIONS] COMMAND [ARGS]...
# 
# Options:
#   --help  Show this message and exit.
# 
# Commands:
#   check-nj-agg      For one or more years, verify the `NewJersey` file is a
#                     concatenation of the county-specific files
#   parse-fields-pdf  Parse fields+lengths from one of the `*CrashTable.pdf`s,
#                     using Tabula
#   pqt               Convert 1 or more unzipped {year, county} `.txt` files to
#                     `.pqt`s, with some dtypes and cleanup
#   txt               Convert 1 or more {year, county} .zip files (convert each
#                     .zip to a single .txt)
#   zip               Download 1 or more {year, county} .zip file(s)

Example: Download + Clean Data

./rawdata.py zip -r NewJersey  # download statewide-aggregated `.zip`s for [2001,2020] x {Accidents,Drivers,Occupants,Pedestrians,Vehicles}
./rawdata.py txt -r NewJersey  # Extract each `.zip` (to a single `.txt`)
./rawdata.py pqt -r NewJersey  # Clean (parse dates, assign some dtypes) + convert to Parquet

Notebooks

SQLite DBs

njdot compute pqt -f
njdot compute db -f

cmymc.ipynb: generate cmymc.db containing several {county, muni, year, month} aggregation tables.

New Year Data Pipeline

When new crash data is released (typically annually), follow this pipeline to process and publish it:

1. Download Raw Data

# Download zip files for the new year (e.g., 2023)
rawdata zip -r NJ -y 2023

# Or use last 2 digits
rawdata zip -r NJ -y 23

# Force re-download if needed
rawdata zip -r NJ -y 2023 -ff

The rawdata zip command:

  • Downloads 5 record types: Accidents, Drivers, Occupants, Pedestrians, Vehicles
  • Caches HTTP headers (Date, Content-Length, Last-Modified, ETag) in njdot/data/.cache.pqt
  • Only re-downloads when headers change (除了 Date)
  • Stores zips in njdot/data/{year}/{region}{year}{type}.zip

2. Extract Text Files

# Extract zip files to .txt
rawdata txt -r NJ -y 2023

This extracts each zip to a single .txt file with fixed-width comma-delimited records.

3. Check Schema Changes

# Check field patterns to detect schema changes
rawdata fsck fields -r NJ -y 2023

# Check specific record type
rawdata fsck fields -r NJ -y 2023 -t Vehicles

Common schema changes:

  • Field width changes (e.g., 2023 Vehicles: Model field expanded from 20→30 chars)
  • New fields added or removed (e.g., 2023 Vehicles: HazMat Placard removed)
  • Value changes (e.g., 2023: Hit & Run blank→'N' instead of always blank)

If schema changes are detected:

  1. Update njdot/rawdata/fixes.py with appropriate fix functions
  2. Apply fixes in rawdata pqt step via get_fixes() in pqt.py

2023 Data Quality Issues

The 2023 NJDOT data had several data quality regressions not present in prior years (2001-2022):

  1. Empty municipality names (16 records):

    • Records had valid county/municipality codes but blank municipality names
    • Fix: Geocoded using lat/lon coordinates via spatial join with NJGIN municipality boundaries
    • Location: njdot/rawdata/pqt.py (applied during parquet generation)
  2. County name conflicts (5 records):

    • Records with correct county code but wrong county name (e.g., cc=3 Burlington with cn="Middlesex")
    • Fix: Majority voting - use most common county name for each county code
    • Location: njdot/harmonize-muni-codes.ipynb cell [6]
  3. Municipality name typos (2 records):

    • cc=3, mc=24: "Mount Laurel Twp" (1 record) vs. "Mount Holly Twp" (382 records)
    • Fix: Majority voting - use most common municipality name for each (cc, mc, year) tuple
    • Location: njdot/harmonize-muni-codes.ipynb cell [8]
  4. Type suffix variations:

    • Raw data contains both "Township"/"Twp" and "Borough"/"Boro" variations
    • Previously caused noisy "conflicts" (10 in 2023: Deptford Township/Twp, Washington Township/Twp, etc.)
    • Fix: Normalize "Township"→"Twp", "Borough"→"Boro" before conflict detection
    • Location: njdot/harmonize-muni-codes.ipynb cell [2]
  5. Decimal formatting in integer fields (69,536 Accident records):

    • "Distance To Cross Street" field has trailing decimals: '50.0', '0.00', '25.0', etc.
    • Pattern: Values 0-99 formatted with decimals (e.g., '50.0', '0.00'); larger values sometimes have trailing . (e.g., '100.', '135.')
    • 99.996% are whole numbers (69,533); only 3 have fractional parts (0.5 twice, 2.7 once)
    • This field had NO decimals in 2001-2022 data (always formatted as '50', '100', etc.)
    • Fix: Targeted decimal-stripping for this specific field only
      • Verifies fractional value histogram matches expected {0.5: 2, 2.7: 1} (warns if unexpected values found)
      • Strips trailing decimals (.d*) and truncates the 3 fractional values to integers
    • Location: njdot/load.py:load_year_df() (lines 99-130)

Architecture: Structural data issues (empty names) are fixed during parquet generation in pqt.py. Name conflicts and typos are resolved via majority voting in the harmonize-muni-codes.ipynb notebook, which previously asserted zero conflicts but now handles them gracefully. Decimal formatting issues are handled during integer type conversion in load.py.

4. Generate Per-Year Parquet Files

# Generate .pqt files for the new year
rawdata pqt -r NJ -y 2023

# Force regeneration if needed
rawdata pqt -r NJ -y 2023 -f

This creates:

  • njdot/data/{year}/{region}{year}{type}.pqt
  • Applies schema fixes, type conversions, and cleanup
  • Files are typically 10-50MB each

Verify the parquets:

python3 -c "import pandas as pd; df = pd.read_parquet('njdot/data/2023/NewJersey2023Accidents.pqt'); print(f'Rows: {len(df)}')"

5. Generate Combined Parquet Files

# Generate combined parquets across all years
env -u PYTHONPATH njdot compute pqt -f

Note: Run with env -u PYTHONPATH to avoid shadowing PyGithub package.

This performs geo-processing for crashes:

  1. Original location geocoding (olat/olonocc/omc):

    • Creates GeoDataFrame from crash report lat/lon
    • Spatial join with NJ municipality geometries → original county/muni codes
  2. Interpolated location geocoding (sri/mpilat/ilonicc/imc):

    • Uses SRI (Standard Route Identifier) + milepost data
    • Geocodes to lat/lon via nj_sri_mp.db (download from S3 if missing)
    • Spatial join with NJ municipality geometries → interpolated county/muni codes
  3. Deduplication: Resolves duplicate spatial join results using original cc/mc from crash record

Outputs combined parquets in njdot/data/:

  • crashes.parquet (~280MB, includes geo-processing)
  • drivers.parquet (~200MB)
  • occupants.parquet (~200MB)
  • pedestrians.parquet (~5MB)
  • vehicles.parquet (~240MB)

6. Generate SQLite Databases

# Generate SQLite databases for web app
env -u PYTHONPATH njdot compute db -f

This reads the combined parquets and creates indexed SQLite databases in www/public/njdot/:

  • crashes.db (~2.4GB)
  • drivers.db (~1GB)
  • occupants.db (~876MB)
  • pedestrians.db (~19MB)
  • vehicles.db (~1.1GB)

Indexes are created for common query patterns (e.g., (severity, dt, cc, mc) for crashes).

7. Track with DVC

# DVC automatically adds .dvc files during compute db
# Commit the changes
git add njdot/data/2023/*.pqt.dvc
git add njdot/data/*.parquet.dvc
git add www/public/njdot/*.db.dvc
git commit -m "Add 2023 NJDOT crash data"

8. Upload to S3

# Databases are automatically uploaded during compute db
# Manually upload if needed:
dvc push

9. Verify Web App

# Start dev server
cd www
npm run dev

Visit http://localhost:3000/#njdot and verify:

  • New year appears in year selector
  • Crash counts match expected values
  • Maps render correctly

Dependencies

SRI Database (nj_sri_mp.db): Required for milepost geocoding in step 5. Download from S3 if missing:

aws s3 cp s3://nj-crashes/nj_sri_mp.db nj_sri_mp.db

To regenerate or update SRI data:

# Fetch SRI data for all counties (slow, rate-limited)
nj-crashes sri county all fetch-sris --max-num -1

Municipality Geometries: Automatically pulled by DVC when needed. Located at nj_crashes/munis.geojson.

Troubleshooting

HTTP header caching issues: If rawdata zip isn't downloading updated files, check .cache.pqt for stale entries. Can force with -ff flag.

UTF-8 decode errors: Raw crash data contains invalid UTF-8 sequences. Use LC_ALL=C for text processing tools (trim, col, etc.).

Schema changes: Run rawdata fsck fields to detect. Add fixes to njdot/rawdata/fixes.py and update get_fixes() mapping.

Geo-processing errors: Ensure nj_sri_mp.db exists and municipality geometries are available. Check for geopandas version compatibility (spatial join column names changed in recent versions).

Missing 2023 data in DB: Check that combined parquet includes 2023 (step 5) before building DB (step 6). Delete cached parquets to force regeneration.

Caveats / TODOs

The fatal crash stats here also seem to differ from NJSP's data (see the root of this repository) by ≈10%.


Attributions:

TODO: add to www pages