How the Stuckness Radar Works
We turn FAST-41 permitting data into a “stuckness” score per project so you can see which projects are at risk of delay and how they compare to their sector.
What it is
The Stuckness Radar is a dashboard over the federal FAST-41 permitting dataset (Fixing America’s Surface Transportation Act, Title 41). FAST-41 tracks major infrastructure projects and their permitting milestones. We load a snapshot of that data (~22K projects), compute a stuckness score [0–1] for each project, and aggregate by state, sector, and agency. The Radar map and charts show where projects are piling up and which sectors or agencies have the highest average stuckness.
Where the data comes from
We read a CSV file (other_data/FAST-41_Projects_Data_20260221.csv) at startup. Each row is a project with fields like Project ID, name, lead agency, bureau, category, sector, status, state, county, city, and lat/lon. We deduplicate by Project ID and cache the enriched list (with stuckness scores) in memory for one hour so the API stays fast.
How the stuckness score is calculated
Each project has a status in the FAST-41 data (e.g. Paused, In Progress, Planned, Complete). We map status to a base score:
| Status | Base score |
|---|---|
| Paused | 1.0 |
| Class of Action Changed | 0.65 |
| In Progress | 0.40 |
| Planned | 0.20 |
| Cancelled | 0.10 |
| Complete | 0.0 |
Higher score = more “stuck.” Paused is the riskiest; Complete is done.
For projects with status In Progress, we apply a small sector adjustment: we compare the project’s sector to the global average “stuck” rate (share of projects in Paused / In Progress / Planned / Class of Action Changed). If the sector is healthier than average, we nudge the score up slightly (this project stands out as more at risk within a healthier sector). If the sector is worse than average, we nudge the score down. The nudge is base + (global_rate - sector_rate) * 0.2, then we clamp the final score to [0, 1].
Labels: High Risk, At Risk, Monitor, On Track
We turn the numeric score into a label for the UI:
| Score | Label |
|---|---|
| ≥ 0.85 | High Risk |
| ≥ 0.55 | At Risk |
| ≥ 0.25 | Monitor |
| < 0.25 | On Track |
What the Radar page shows
- KPI strip: Total projects, active count, paused count, average stuckness, and risk breakdown (high / at risk / monitor / on track).
- Map: Choropleth by state—you can switch between average stuckness, % paused, or project count. Color scale from low (cool) to high (red).
- OPEF Copilot: Optional LLM narration (Ollama or OpenAI). You can ask a question or click “Narrate” to get a short analysis of the current view—e.g. which sectors are stuck, which states have the most paused projects. The prompt tells the model to act as an “OPEF permitting analyst copilot” and cite numbers.
- Charts: Stuckness by sector and top agencies by stuckness (bar charts).
- Project list: Filterable table (status, state, sector, min score) with each project’s name, agency, status, stuckness score, and label. Click a row for peer comparison (same sector).
What we don’t use (yet)
The score is based only on status and a sector-relative nudge. We do not use time-in-stage, milestone dates, or deadlines. A project that’s been “In Progress” for five years gets the same base score as one that just started. Adding time-based logic (e.g. “stuck in this stage longer than sector median”) would make the score more predictive of actual delay risk.
Key files to look at
backend/api/radar.py— CSV load, _compute_stuckness, overview / by-state / by-sector / projects / narrateother_data/FAST-41_Projects_Data_20260221.csv— Source FAST-41 project datafrontend/radar.html— Map, charts, project table, OPEF Copilot UI
In one sentence
We load FAST-41 project data, assign each project a stuckness score from its status (with a small sector adjustment for “In Progress”), label them High Risk / At Risk / Monitor / On Track, and surface the results on a map, charts, and table—with optional LLM narration.