https://github.com/stiles/lapd-choppers
Tools for tracking flights made by the Los Angeles Police Department's Air Support Division.
https://github.com/stiles/lapd-choppers
Last synced: 5 months ago
JSON representation
Tools for tracking flights made by the Los Angeles Police Department's Air Support Division.
- Host: GitHub
- URL: https://github.com/stiles/lapd-choppers
- Owner: stiles
- License: mit
- Created: 2025-10-11T19:37:08.000Z (8 months ago)
- Default Branch: main
- Last Pushed: 2025-10-19T17:47:15.000Z (8 months ago)
- Last Synced: 2025-10-30T13:45:21.026Z (7 months ago)
- Language: Python
- Homepage:
- Size: 6.23 MB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# LAPD Choppers
Tools for tracking LAPD helicopter flights using FlightRadar24 data.
## Quick Start
### Setup
```bash
# Install dependencies
uv venv && source .venv/bin/activate
uv pip install -r requirements.txt
# Set API key
export FLIGHTRADAR_API_KEY="your-key-here"
```
### Capture flights
```bash
# Single date
python jobs/capture/capture_flights.py --date 2025-10-10
# Date range (batch)
./scripts/batch_capture.sh 2025-01-01 2025-10-12
# Single helicopter
python jobs/capture/capture_flights.py --date 2025-10-10 --only 230LA
```
Captured data goes to: `data/raw/flights/{icao24}/{YYYY-MM-DD}/`
Logs go to: `logs/capture/{YYYY-MM-DD}.jsonl`
### Normalize data
```bash
# Normalize single date
make normalize-day DATE=2025-10-12
# Normalize entire month
make normalize-month MONTH=2025-09
# Normalize all captured data
make normalize-all
# Run QA checks
make qa-summary
make qa-month MONTH=2025-09
```
Normalized data goes to: `data/processed/points/{year}/{month}/` and `data/processed/flights/{year}/{month}/`
### Aggregate & analyze
```bash
# Monthly aggregates (H3 + divisions + reporting districts + neighborhoods)
make aggregate-month MONTH=2025-09
make aggregate-month-geo MONTH=2025-09 # With GeoJSON export
# Year-level aggregates (all months combined)
make aggregate-year YEAR=2025
make aggregate-year-geo YEAR=2025 # With GeoJSON export
# Individual aggregate types (monthly):
make aggregate-h3 MONTH=2025-09 # H3 hexagons only
make aggregate-divisions MONTH=2025-09 # LAPD divisions only
make aggregate-reporting-districts MONTH=2025-09 # Reporting districts only
# With GeoJSON export:
make aggregate-h3-geo MONTH=2025-09
make aggregate-divisions-geo MONTH=2025-09
make aggregate-reporting-districts-geo MONTH=2025-09
```
**Geographic layers:**
- **H3 hexagons** (~1 mile): Uniform grid, best for spatial analysis
- **LAPD divisions** (21): Official police boundaries
- **Reporting districts** (1,134): Fine-grained police zones (excludes RD 119)
- **LA City neighborhoods** (114): **Most relatable for residents**
**Output formats:**
- **Parquet** (default): Compact, fast queries, no geometries (17-51 KB)
- **GeoJSON** (with `--geojson` flag): Includes geometries for QGIS (160 KB - 4.8 MB)
**Time periods:**
- **Monthly**: `data/aggregates/{type}/{year}/{month}/` - Single month statistics
- **Yearly**: `data/aggregates/{type}/{year}/` - All months combined
**Metrics included:**
- Raw counts: total hours, flights, points, days with activity
- Medians: altitude, ground speed
- **Normalized**: hours per sq mi, flights per sq mi (for divisions & reporting districts)
- Area: square miles for each geographic unit
**Heliport filter:**
By default, all points in LAPD Reporting District 119 (which contains the Air Support Division heliport at 555 Ramirez St) are excluded. This district is primarily industrial/warehouse area, and activity is dominated by heliport operations (takeoffs, landings, maintenance). This removes ~1.4% of points. Use `--no-heliport-filter` to include RD 119.
### Visualize patterns
```bash
# Generate all choropleth maps for a year (default: YlOrRd palette)
make visualize YEAR=2025
# Generate with specific ColorBrewer palette
make visualize YEAR=2025 PALETTE=Reds
make visualize YEAR=2025 PALETTE=Oranges
make visualize YEAR=2025 PALETTE=Blues
# Generate map for specific layer
make visualize-layer YEAR=2025 LAYER=neighborhoods
make visualize-layer YEAR=2025 LAYER=neighborhoods PALETTE=Blues
make visualize-layer YEAR=2025 LAYER=divisions
make visualize-layer YEAR=2025 LAYER=reporting_districts
make visualize-layer YEAR=2025 LAYER=h3
```
**Features:**
- Natural breaks (Jenks) classification for optimal class separation
- ColorBrewer palettes: `YlOrRd` (default), `Reds`, `Oranges`, `Blues`
- Roboto font throughout (bold headlines, regular body, lighter source notes)
- High-resolution PNG output (300 DPI)
- Legend positioned on left side to avoid obscuring geography
**Output files** (saved to `visuals/`):
- `neighborhoods_intensity_{year}.png` - LA City neighborhoods (114)
- `divisions_intensity_{year}.png` - LAPD divisions (21)
- `reporting_districts_intensity_{year}.png` - Reporting districts (1,134)
- `h3_intensity_{year}.png` - H3 hexagonal grid (226 city, 1,180 county)
Files with non-default palettes include palette name: `neighborhoods_intensity_2025_blues.png`
## Project structure
```
lapd-choppers/
├── data/
│ ├── raw/ # Raw pulls by day and by tail (gitignored)
│ ├── processed/ # Normalized parquet (gitignored)
│ ├── aggregates/ # H3, divisions, districts, neighborhoods (gitignored)
│ └── reference/ # Aircraft master, boundaries (version controlled)
├── jobs/
│ ├── capture/ # FlightRadar24 collectors
│ ├── normalize/ # raw → points / flights
│ └── aggregate/ # points → geographic aggregations
├── scripts/ # Small CLIs (boundaries, aircraft master, etc.)
├── visuals/ # Generated choropleth maps (gitignored)
├── notebooks/ # Ad hoc QA only
├── requirements.txt
├── PLANNING.md # Full architecture & roadmap
└── REPORTING.md # Story ideas & findings (gitignored)
```
## Data sources
- **Primary**: FlightRadar24 via pyfr24 (authorized API)
- **Reference**: FAA Releasable Aircraft database (weekly refresh)
- **Context**: LAPD divisions, census tracts, 311 calls (optional)
## Key identifiers
- **N-number** (tail): FAA registration (e.g., N221LA)
- **icao24**: Mode-S code hex (e.g., a1e5b3)
- **unique_id**: FAA unique identifier
The aircraft master maintains the canonical mapping between these IDs.
## Storage Design
**Economical capture strategy:**
- Track **individual flights** (not daily aggregates) for mission-level analysis
- Partition by `icao24` and date to enable incremental collection
- Skip dates that already exist → never re-fetch
- Log all API calls for credit tracking
**Directory structure:**
```
data/
raw/
flights/{icao24}/{YYYY-MM-DD}/
flight_{flight_id}.json # Flight metadata
flight_{flight_id}_tracks.json # Position time-series
processed/
points/{year}/{month}/ # Normalized time-series (parquet)
flights/{year}/{month}/ # Per-flight summaries (parquet)
aggregates/
h3/{year}/{month}/ # Hex-level rollups (parquet + geojson)
divisions/{year}/{month}/ # Division-level rollups (parquet + geojson)
reporting_districts/{year}/{month}/ # District-level rollups (parquet + geojson)
neighborhoods/{year}/{month}/ # Neighborhood-level rollups (parquet + geojson)
reference/
aircraft_master.json # 18 LAPD helicopters
lapd_divisions.geojson # 21 LAPD divisions
lapd_bureaus.geojson # 4 LAPD bureaus
lapd_reporting_districts.geojson # 1,191 reporting districts
la_city_boundary.geojson # LA City boundary
la_county_boundary.geojson # LA County boundary
la_city_boundary_h3_res7.geojson # H3 hexagon grid (city)
la_county_boundary_h3_res7.geojson # H3 hexagon grid (county)
logs/
capture/{YYYY-MM-DD}.jsonl # API usage tracking
```
## Reference Data
Stored in `data/reference/` (version controlled):
**Aircraft:**
- **aircraft_master.json** - 18 LAPD helicopters with N-numbers and icao24 codes
**LAPD Boundaries:**
- **lapd_divisions.geojson** - 21 LAPD patrol divisions
- **lapd_bureaus.geojson** - 4 LAPD bureaus
- **lapd_reporting_districts.geojson** - 1,191 reporting districts
**Geographic Boundaries:**
- **la_city_boundary.geojson** - LA City boundary
- **la_county_boundary.geojson** - LA County boundary
- **la_city_neighborhoods.geojson** - 114 LA City neighborhoods (used for aggregation)
- **la_county_neighborhoods.geojson** - LA County neighborhoods and cities
**H3 Hexagon Grids:**
- **la_city_boundary_h3_res7.geojson** - ~1 mile resolution (226 hexagons)
- **la_county_boundary_h3_res7.geojson** - ~1 mile resolution (1,180 hexagons)
**Note:** All boundaries include `area_sqmi` field for normalization. Area is calculated automatically when boundaries are fetched.
To refresh:
```bash
# Fetch all boundaries
make boundaries
# Or fetch specific types:
make boundaries-lapd # LAPD divisions, bureaus, reporting districts
python scripts/fetch_boundaries.py --only city # Just city boundary
python scripts/fetch_boundaries.py --only county # Just county boundaries
# Generate H3 grids
make hexgrids
```
## License
See [LICENSE](LICENSE) for details.