Railroad Grade Crossing Risk Screener - FRA Accident History avatar

Railroad Grade Crossing Risk Screener - FRA Accident History

Pricing

from $5.50 / 1,000 crossing records

Go to Apify Store
Railroad Grade Crossing Risk Screener - FRA Accident History

Railroad Grade Crossing Risk Screener - FRA Accident History

Screens any US coordinate or geography against the 242,108-crossing FRA/NTAD grade crossing inventory joined to 50 years of FRA Form 57 accident history. Returns warning devices, traffic and train exposure, fatalities and a per-crossing risk score. A zero is a verified negative.

Pricing

from $5.50 / 1,000 crossing records

Rating

0.0

(0)

Developer

Kyle Maloney

Kyle Maloney

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

4 days ago

Last modified

Share

Railroad Grade Crossing Risk Screener — FRA Accident History

Screen any US coordinate, or a whole state / county / city, against the 242,108-crossing FRA National Highway-Rail Crossing Inventory (published by USDOT/BTS in the National Transportation Atlas Database) joined to 50 years of FRA Form 57 highway-rail accident reports — 251,149 incidents running from 1975 to the present.

For every crossing you get the inventory record (warning devices, public/private, at-grade vs grade-separated, traffic and train counts, timetable speed, track layout, operating railroad) plus its actual accident history — how many, how recent, how severe — and an auditable risk score.

The point of the product is the negative. If a crossing next to your site has never had a reported accident, this actor tells you that as a verified fact (

incidents_checked: true
, incidents_count: 0) — and if the FRA source could not be reached, it tells you that instead (incidents_checked: false, incidents_count: null). Those two answers never look the same.


Who it's for

  • Environmental and property due diligence — a rail crossing 300 ft from a site is a noise, vibration, access and liability finding. Phase I ESA and ASTM site-recon workflows already screen pipelines, tanks and floodplains; this is the rail layer.
  • Logistics, industrial and warehouse siting — inbound truck routes that cross an active at-grade crossing with 40 trains a day and crossbucks only are a real operating and insurance cost. exposure_index (AADT × trains/day) is the standard measure.
  • Traffic-safety and municipal engineering — pull every crossing in a county with its device inventory and casualty history to prioritise Section 130 / grade-separation candidates.
  • Insurance and risk underwriting — per-location crossing proximity, protection class and 50-year loss history.
  • Rail, transit and utility route planning — crossing density and protection along a corridor.

Example input

Coordinate mode — screen specific sites:

{
"assets": [
{ "label": "Denver Union Station district", "lat": 39.7392, "lon": -104.9903, "radiusMiles": 1 },
{ "label": "RTD Colorado Station, Denver", "lat": 39.776004, "lon": -104.943325, "radiusMiles": 0.5 }
],
"includeCrossingRows": true
}

Geography mode — every crossing in a county, with accident history:

{ "state": "CO", "county": "DENVER", "includeCrossingRows": true }

Add the individual accident reports (narratives, speeds, casualties, conditions):

{
"assets": [{ "lat": 41.8781, "lon": -87.6298, "radiusMiles": 1 }],
"includeIncidentDetail": true,
"incidentSinceYear": 2005
}

Output

Three record types, all in one dataset, distinguished by record_type:

record_typeOne row perUse it for
asset_summaryscreened siteheadline counts, nearest crossing, radius totals, site risk band
crossinggrade crossingthe full inventory record plus its accident rollup and risk score
incidentFRA Form 57 reportnarrative-level accident detail (opt-in)

173 declared fields. The highlights:

Location & identitycrossing_id (the FRA number, which is also the join key to every other FRA safety product), crossing_distance_miles, crossing_lat / crossing_lon, position_status, street, city_name, county_name, county_fips, state_abbr, fra_inventory_url, fra_accident_url.

What kind of crossingcrossing_position (At Grade / RR Under / RR Over), is_at_grade, crossing_type, is_public, crossing_purpose.

Protectionhas_warning_device, warning_device_type, warning_device_class (active / passive / none), warning_device_fields_conflict, whistle_ban, quiet_zone_indicated.

Exposureannual_average_daily_traffic, aadt_year, total_through_trains_per_day, max_timetable_speed_mph, main_tracks, total_tracks, exposure_index.

Railroadrailroad_code, railroad_name, railroad_division, railroad_subdivision, parent_railroad_code, railroad_milepost, railroad_operation_end_date, operating_railroad_ended.

Accident historyincidents_checked, incidents_count, incidents_fatalities, incidents_injuries, incidents_last_5y, incidents_last_10y, incidents_first_date, incidents_last_date, years_since_last_incident, worst_incident_date, worst_incident_fatalities, hazmat_incidents, pedestrian_incidents, and the full most_recent_incident_* set including the FRA narrative.

Riskrisk_score (0-100), risk_band, risk_score_basis, risk_factors.

Provenance on every rowinventory_source_status, inventory_vintage, incident_source_status, incident_data_updated_at, incident_coverage_end_date, incident_reporting_lag_days, incident_history_complete, data_sources_ok, matched_rows_total, results_truncated.


How this actor refuses to give you a confident wrong answer

Every item below is a measured property of the upstream data, not a hypothetical.

A zero is either verified or it is null — never both. incidents_count: 0 means FRA answered and holds no report. If the incident source fails, the count is null, incident_history_complete is false, and risk_score is null — the actor will not publish a "low risk" derived from a query that never returned.

16.3% of the inventory is not at grade. 21,251 crossings are RR Under and 18,157 are RR Over. Counting all 242,108 records as at-grade conflict points overstates real exposure by about a fifth, so is_at_grade ships on every row and the summary splits the counts. A live check on every run fails the actor if the upstream ever stops publishing grade-separated records, because at that moment is_at_grade would silently become a constant true.

A blank warning-device code does not mean "unprotected." WDCODE is blank on 106,597 crossings (44.0%) — and 44,704 of those have SignsOrSignals = Yes, i.e. a device exists and its type was simply not reported. A further 13,610 crossings name a device while SignsOrSignals says No; that contradiction is surfaced in warning_device_fields_conflict rather than silently resolved in one direction.

288 crossings have no usable position. BTS moves a crossing to latitude/longitude 0,0 when its reported coordinate falls outside its own designated state. Those records get position_status: "unlocatable_moved_to_null_island", a null position and a null distance — the raw reported pair still ships in reported_latitude / reported_longitude, but the actor never quietly falls back to the coordinate BTS itself rejected. Radius queries exclude them server-side, so a search near the Gulf of Guinea does not return all 288.

33.7% of crossings publish no traffic count. An unreported AADT is null, never 0, and exposure_index is null rather than a fabricated zero.

The accident file runs about 2.5 months behind. FRA refreshed the dataset today, but the most recent incident in it is from 2026-05-31. Every row publishes incident_coverage_end_date and incident_reporting_lag_days, and every recency window (incidents_last_5y, years_since_last_incident) is anchored to that date rather than to the clock — so a figure cannot drift on its own between runs.

Not every accident joins to a live crossing. In Colorado, 1,302 distinct crossing IDs appear in the accident file but only 932 (71.6%) are still in the current inventory; 23.3% of Colorado incidents therefore cannot be attributed to an open crossing. That is because the crossing was closed, consolidated or re-identified after the accident — or because the ID predates the modern 6-digit-plus-letter format (466D0, 0253087). A further 5,513 incidents nationally (2.2%) carry no crossing ID at all. So a per-crossing count is an honest lower bound on the crossing as it exists today, not a count of everything that ever happened at that spot.

The two-digit year is pivoted from evidence, not assumption. incidentyear is text and two digits. Its distinct set is exactly {75..99} ∪ {00..26} and each band was cross-checked against the file's own date column — 52 of 52 agree. Rule:

yy >= 75 → 19yy
, otherwise 20yy. The band 27-74 is ambiguous and currently empty; a live check fails the run if FRA ever back-loads a value into it, rather than letting a 1974 record be published as 2074.

Nothing is billed unless a live gate passes first. Before any row is written the actor measures both upstreams and checks: inventory count floor, the presence of both at-grade and grade-separated records, the closed CrossingPosition and CrossingType vocabularies, the full field contract, inventory freshness, incident count floor, incident freshness, century-pivot integrity, a positive canary (crossing 945876D at Colorado Station in Denver, and its 2026-05-30 fatality, which also proves the crossing-ID join is still intact), and a negative control (a 50-mile radius in the open Pacific must return zero). Any failure ends the run with an explanatory message and zero rows, zero charges.

A partial read is an error, not a result. Crossing retrieval is paged to completion and then reconciled against the service's own count; a shortfall aborts rather than publishing an arbitrary subset. If you cap the result with maxCrossings, matched_rows_total still reports the true total and results_truncated is set.


Notes and limits

  • risk_score is editorial. FRA publishes no official per-crossing risk rating in this dataset. risk_score_basis names the model and risk_factors itemises every contribution so you can audit or re-weight it. It is a screening aid, not a regulatory determination.
  • reason_code (FRA Form 71 reason-for-update) ships verbatim with
    reason_code_label: null
    and reason_code_label_basis: "unmapped_fra_reason_code". FRA ships no decode table inside the NTAD extract, and a guessed label across 242,108 rows would be worse than no label.
  • Geography mode matches the FRA spelling exactly: uppercase, no "County" suffix (state: "CO", county: "DENVER").
  • Coverage is the 50 states, DC and territories present in the FRA inventory. There is no crossing data for locations outside it, and a radius there correctly returns zero.
  • An empty run input fails loudly with an actionable message and bills nothing.

Use as an MCP tool

This actor is callable by AI agents through Apify's MCP server at https://mcp.apify.com. Billing is unchanged when it is called as a tool, and a run that cannot answer fails without billing.

{
"mcpServers": {
"apify": {
"command": "npx",
"args": ["-y", "@apify/actors-mcp-server", "--actors", "malonestar/ntad-fra-rail-crossing-risk-screener"],
"env": { "APIFY_TOKEN": "your-apify-token" }
}
}
}

Worked agent prompt:

"Screen 1200 W 47th Ave, Denver CO for railroad exposure. Give me every grade crossing within a mile, whether each is at grade, what warning devices it has, and its FRA accident history — and tell me explicitly which crossings were verified to have no accidents versus which could not be checked."

Chains naturally with the rest of the site-diligence shelf — pipelines, tanks, floodplain, wetlands, contaminated sites — since all of them screen the same coordinate.

FAQ

Where does the crossing data come from? The FRA National Highway-Rail Crossing Inventory, published as the USDOT/BTS NTAD "Railroad Grade Crossings" layer. The vintage is read from the service on every run and published as inventory_vintage.

Where does the accident data come from? FRA "Highway-Rail Grade Crossing Incident Data (Form 57)", the official railroad-reported accident file, 1975 to present.

Does a crossing with zero incidents really mean zero? It means FRA holds no Form 57 report joined to that crossing ID through incident_coverage_end_date. Read the caveats on join rates above — and check incidents_checked before trusting any zero.

Can I get every crossing in a state? Yes — { "state": "TX" }. Use maxCrossings to bound the cost; matched_rows_total always reports the true total.

How is this priced? Pay per result row. An asset_summary row plus one row per crossing found; incident rows only if you ask for them.