{"id":38551593,"url":"https://github.com/stiles/lapd-choppers","last_synced_at":"2026-01-17T07:35:20.851Z","repository":{"id":321590854,"uuid":"1074437068","full_name":"stiles/lapd-choppers","owner":"stiles","description":"Tools for tracking flights made by the Los Angeles Police Department's Air Support Division. ","archived":false,"fork":false,"pushed_at":"2025-10-19T17:47:15.000Z","size":6537,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-10-30T13:45:21.026Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/stiles.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-11T19:37:08.000Z","updated_at":"2025-10-19T17:47:19.000Z","dependencies_parsed_at":null,"dependency_job_id":"ba19f08c-6c26-472f-b36e-5573db723a52","html_url":"https://github.com/stiles/lapd-choppers","commit_stats":null,"previous_names":["stiles/lapd-choppers"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/stiles/lapd-choppers","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stiles%2Flapd-choppers","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stiles%2Flapd-choppers/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stiles%2Flapd-choppers/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stiles%2Flapd-choppers/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stiles","download_url":"https://codeload.github.com/stiles/lapd-choppers/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stiles%2Flapd-choppers/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28504356,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-17T06:57:29.758Z","status":"ssl_error","status_checked_at":"2026-01-17T06:56:03.931Z","response_time":85,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-01-17T07:35:20.767Z","updated_at":"2026-01-17T07:35:20.838Z","avatar_url":"https://github.com/stiles.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# LAPD Choppers\n\nTools for tracking LAPD helicopter flights using FlightRadar24 data.\n\n## Quick Start\n\n### Setup\n\n```bash\n# Install dependencies\nuv venv \u0026\u0026 source .venv/bin/activate\nuv pip install -r requirements.txt\n\n# Set API key\nexport FLIGHTRADAR_API_KEY=\"your-key-here\"\n```\n\n### Capture flights\n\n```bash\n# Single date\npython jobs/capture/capture_flights.py --date 2025-10-10\n\n# Date range (batch)\n./scripts/batch_capture.sh 2025-01-01 2025-10-12\n\n# Single helicopter\npython jobs/capture/capture_flights.py --date 2025-10-10 --only 230LA\n```\n\nCaptured data goes to: `data/raw/flights/{icao24}/{YYYY-MM-DD}/`  \nLogs go to: `logs/capture/{YYYY-MM-DD}.jsonl`\n\n### Normalize data\n\n```bash\n# Normalize single date\nmake normalize-day DATE=2025-10-12\n\n# Normalize entire month\nmake normalize-month MONTH=2025-09\n\n# Normalize all captured data\nmake normalize-all\n\n# Run QA checks\nmake qa-summary\nmake qa-month MONTH=2025-09\n```\n\nNormalized data goes to: `data/processed/points/{year}/{month}/` and `data/processed/flights/{year}/{month}/`\n\n### Aggregate \u0026 analyze\n\n```bash\n# Monthly aggregates (H3 + divisions + reporting districts + neighborhoods)\nmake aggregate-month MONTH=2025-09\nmake aggregate-month-geo MONTH=2025-09             # With GeoJSON export\n\n# Year-level aggregates (all months combined)\nmake aggregate-year YEAR=2025\nmake aggregate-year-geo YEAR=2025                  # With GeoJSON export\n\n# Individual aggregate types (monthly):\nmake aggregate-h3 MONTH=2025-09                    # H3 hexagons only\nmake aggregate-divisions MONTH=2025-09             # LAPD divisions only\nmake aggregate-reporting-districts MONTH=2025-09   # Reporting districts only\n\n# With GeoJSON export:\nmake aggregate-h3-geo MONTH=2025-09\nmake aggregate-divisions-geo MONTH=2025-09\nmake aggregate-reporting-districts-geo MONTH=2025-09\n```\n\n**Geographic layers:**\n- **H3 hexagons** (~1 mile): Uniform grid, best for spatial analysis\n- **LAPD divisions** (21): Official police boundaries\n- **Reporting districts** (1,134): Fine-grained police zones (excludes RD 119)\n- **LA City neighborhoods** (114): **Most relatable for residents**\n\n**Output formats:**\n- **Parquet** (default): Compact, fast queries, no geometries (17-51 KB)\n- **GeoJSON** (with `--geojson` flag): Includes geometries for QGIS (160 KB - 4.8 MB)\n\n**Time periods:**\n- **Monthly**: `data/aggregates/{type}/{year}/{month}/` - Single month statistics\n- **Yearly**: `data/aggregates/{type}/{year}/` - All months combined\n\n**Metrics included:**\n- Raw counts: total hours, flights, points, days with activity\n- Medians: altitude, ground speed\n- **Normalized**: hours per sq mi, flights per sq mi (for divisions \u0026 reporting districts)\n- Area: square miles for each geographic unit\n\n**Heliport filter:**\nBy 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.\n\n### Visualize patterns\n\n```bash\n# Generate all choropleth maps for a year (default: YlOrRd palette)\nmake visualize YEAR=2025\n\n# Generate with specific ColorBrewer palette\nmake visualize YEAR=2025 PALETTE=Reds\nmake visualize YEAR=2025 PALETTE=Oranges\nmake visualize YEAR=2025 PALETTE=Blues\n\n# Generate map for specific layer\nmake visualize-layer YEAR=2025 LAYER=neighborhoods\nmake visualize-layer YEAR=2025 LAYER=neighborhoods PALETTE=Blues\nmake visualize-layer YEAR=2025 LAYER=divisions\nmake visualize-layer YEAR=2025 LAYER=reporting_districts\nmake visualize-layer YEAR=2025 LAYER=h3\n```\n\n**Features:**\n- Natural breaks (Jenks) classification for optimal class separation\n- ColorBrewer palettes: `YlOrRd` (default), `Reds`, `Oranges`, `Blues`\n- Roboto font throughout (bold headlines, regular body, lighter source notes)\n- High-resolution PNG output (300 DPI)\n- Legend positioned on left side to avoid obscuring geography\n\n**Output files** (saved to `visuals/`):\n- `neighborhoods_intensity_{year}.png` - LA City neighborhoods (114)\n- `divisions_intensity_{year}.png` - LAPD divisions (21)\n- `reporting_districts_intensity_{year}.png` - Reporting districts (1,134)\n- `h3_intensity_{year}.png` - H3 hexagonal grid (226 city, 1,180 county)\n\nFiles with non-default palettes include palette name: `neighborhoods_intensity_2025_blues.png`\n\n## Project structure\n\n```\nlapd-choppers/\n├── data/\n│   ├── raw/                 # Raw pulls by day and by tail (gitignored)\n│   ├── processed/           # Normalized parquet (gitignored)\n│   ├── aggregates/          # H3, divisions, districts, neighborhoods (gitignored)\n│   └── reference/           # Aircraft master, boundaries (version controlled)\n├── jobs/\n│   ├── capture/             # FlightRadar24 collectors\n│   ├── normalize/           # raw → points / flights\n│   └── aggregate/           # points → geographic aggregations\n├── scripts/                 # Small CLIs (boundaries, aircraft master, etc.)\n├── visuals/                 # Generated choropleth maps (gitignored)\n├── notebooks/               # Ad hoc QA only\n├── requirements.txt\n├── PLANNING.md              # Full architecture \u0026 roadmap\n└── REPORTING.md             # Story ideas \u0026 findings (gitignored)\n```\n\n## Data sources\n\n- **Primary**: FlightRadar24 via pyfr24 (authorized API)\n- **Reference**: FAA Releasable Aircraft database (weekly refresh)\n- **Context**: LAPD divisions, census tracts, 311 calls (optional)\n\n## Key identifiers\n\n- **N-number** (tail): FAA registration (e.g., N221LA)\n- **icao24**: Mode-S code hex (e.g., a1e5b3)  \n- **unique_id**: FAA unique identifier\n\nThe aircraft master maintains the canonical mapping between these IDs.\n\n## Storage Design\n\n**Economical capture strategy:**\n- Track **individual flights** (not daily aggregates) for mission-level analysis\n- Partition by `icao24` and date to enable incremental collection\n- Skip dates that already exist → never re-fetch\n- Log all API calls for credit tracking\n\n**Directory structure:**\n```\ndata/\n  raw/\n    flights/{icao24}/{YYYY-MM-DD}/\n      flight_{flight_id}.json                # Flight metadata\n      flight_{flight_id}_tracks.json         # Position time-series\n  processed/\n    points/{year}/{month}/                   # Normalized time-series (parquet)\n    flights/{year}/{month}/                  # Per-flight summaries (parquet)\n  aggregates/\n    h3/{year}/{month}/                       # Hex-level rollups (parquet + geojson)\n    divisions/{year}/{month}/                # Division-level rollups (parquet + geojson)\n    reporting_districts/{year}/{month}/      # District-level rollups (parquet + geojson)\n    neighborhoods/{year}/{month}/            # Neighborhood-level rollups (parquet + geojson)\n  reference/\n    aircraft_master.json                     # 18 LAPD helicopters\n    lapd_divisions.geojson                   # 21 LAPD divisions\n    lapd_bureaus.geojson                     # 4 LAPD bureaus\n    lapd_reporting_districts.geojson         # 1,191 reporting districts\n    la_city_boundary.geojson                 # LA City boundary\n    la_county_boundary.geojson               # LA County boundary\n    la_city_boundary_h3_res7.geojson         # H3 hexagon grid (city)\n    la_county_boundary_h3_res7.geojson       # H3 hexagon grid (county)\nlogs/\n  capture/{YYYY-MM-DD}.jsonl                 # API usage tracking\n```\n\n## Reference Data\n\nStored in `data/reference/` (version controlled):\n\n**Aircraft:**\n- **aircraft_master.json** - 18 LAPD helicopters with N-numbers and icao24 codes\n\n**LAPD Boundaries:**\n- **lapd_divisions.geojson** - 21 LAPD patrol divisions\n- **lapd_bureaus.geojson** - 4 LAPD bureaus\n- **lapd_reporting_districts.geojson** - 1,191 reporting districts\n\n**Geographic Boundaries:**\n- **la_city_boundary.geojson** - LA City boundary\n- **la_county_boundary.geojson** - LA County boundary\n- **la_city_neighborhoods.geojson** - 114 LA City neighborhoods (used for aggregation)\n- **la_county_neighborhoods.geojson** - LA County neighborhoods and cities\n\n**H3 Hexagon Grids:**\n- **la_city_boundary_h3_res7.geojson** - ~1 mile resolution (226 hexagons)\n- **la_county_boundary_h3_res7.geojson** - ~1 mile resolution (1,180 hexagons)\n\n**Note:** All boundaries include `area_sqmi` field for normalization. Area is calculated automatically when boundaries are fetched.\n\nTo refresh:\n```bash\n# Fetch all boundaries\nmake boundaries\n\n# Or fetch specific types:\nmake boundaries-lapd     # LAPD divisions, bureaus, reporting districts\npython scripts/fetch_boundaries.py --only city    # Just city boundary\npython scripts/fetch_boundaries.py --only county  # Just county boundaries\n\n# Generate H3 grids\nmake hexgrids\n```\n\n## License\n\nSee [LICENSE](LICENSE) for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstiles%2Flapd-choppers","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstiles%2Flapd-choppers","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstiles%2Flapd-choppers/lists"}