An open API service indexing awesome lists of open source software.

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.

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.