Skip to content

Repository files navigation

Lens

A retrospective analysis tool for auditing police enforcement patterns using open public datasets from San Francisco. Built for civic oversight analysts, investigative journalists, and academic researchers.

The thesis: police incident records do not capture crime — they capture police contact. A heavily-patrolled neighborhood generates more reports, which makes it look like a hotspot, which has historically justified more patrol. Lens makes that feedback loop visible. It normalizes the same incident data three ways (raw count, per-capita using ACS census population, and officer enforcement ratio) so the same data tells three different stories — and surfaces the differences explicitly rather than hiding them.

What this looks like in practice: after Mayor Lurie took office in January 2025, South of Market's officer enforcement ratio rose +79 points compared to the prior period — roughly 4–6× its historical year-over-year baseline. Mission rose +43 points (5× baseline). Tenderloin, by contrast, was lower than its historical average, suggesting a geographic reallocation of enforcement rather than a citywide increase. Lens surfaces this finding in two clicks: select the Lurie preset, read the ranked list.

The same tool applied to the 2026 World Cup shows which neighborhoods are seeing enforcement shift during the event window — a live example of the same analysis applied to a current policy question.

What's next (v2.0): cross-references enforcement data with auxiliary sources — 311 service requests (unmet civic need), business density (explains raw counts in commercial areas), building violations (housing disinvestment), and public health indicators — to further separate genuine need from patrol intensity. An analyst will walk away with a finding like: "District X has 2.1× the citywide discretionary enforcement rate but 0.7× the victim-reported serious crime rate, and burglaries there resolve in a recorded arrest about half as often as the citywide rate."


What v1.0 ships

Lens 1 — Per-capita enforcement density Incident counts normalized by ACS census population per neighborhood. Strips out the trivial fact that more people generate more of everything.

Lens 2 — Officer enforcement ratio Officer-initiated incidents per 100 victim-reported serious crimes, per neighborhood. Separates patrol behavior from underlying crime. Validated against 50+ crime categories using at-filing arrest rates and Coplogic shares to classify officer-initiated vs. victim-reported.

Compare mode Side-by-side Lens 2 view across two user-defined date windows. Surfaces enforcement shifts tied to policy events. Ships with two presets: Mayor Lurie's anti-homelessness campaign (Jan–Sep 2025 vs. the prior 9 months) and the 2026 World Cup event window.

Data ~746,000 deduplicated SF incidents, Jan 2018 – July 2026, loaded from DataSF via Socrata. Raw dataset is ~1M report rows; deduplicated to unique incidents before analysis.

What's not in v1.0 Lens 3 (resolution gap — clearance rate by crime type vs. citywide median) is built and validated but held pending assault-category surgery. It will ship in v2.0.


Screenshots

Lens 1 (per-capita) — South of Market, 2018–2026 Lens 1 per-capita view with South of Market selected

Lens 2 (officer enforcement ratio) — South of Market in compare mode: first 9 months of Mayor Lurie's anti-homelessness campaign vs. the 9 months directly preceding Lens 2 compare mode showing Lurie enforcement shift in South of Market


Stack

Layer Technology
Backend Python / FastAPI
Database PostgreSQL + PostGIS
Migrations Alembic
Frontend Next.js + TypeScript
Map Leaflet / react-leaflet
Charts Recharts
Data pipeline Python / Pandas / GeoPandas
Dev environment Docker Compose

Getting Started

Prerequisites: Docker and Docker Compose.

Full stack (DB + API + frontend)

docker compose up

The frontend will be available at http://localhost:3000. The backend API will be available at http://localhost:8000. Migrations run automatically on backend startup — no manual step required.

Database + API only

docker compose up db backend

Verify everything is working

# API health
curl http://localhost:8000/health

# Lens 1 data (should return 41 neighborhoods)
curl "http://localhost:8000/lens/1?start=2024-01-01&end=2025-01-01"

# PostGIS extension
docker compose exec db psql -U lens -d lens -c "SELECT PostGIS_Version();"

Frontend with hot reload (recommended for active development)

The full docker compose up runs a production build — every CSS or component change requires a full rebuild. For active frontend work, run the frontend outside Docker so changes reload instantly:

# Terminal 1 — DB + backend only
docker compose up db backend

# Terminal 2 — frontend dev server (hot reload)
cd frontend
npm run dev

To avoid setting the env var every time, create frontend/.env.local once:

NEXT_PUBLIC_API_TARGET=http://localhost:8000

Switching back to full Docker (e.g. to test the production build or share via ngrok):

# Stop just the frontend dev server (Ctrl+C in its terminal), then:
docker compose up --build frontend

Switching back to local dev from full Docker:

docker compose stop frontend   # DB and backend keep running
cd frontend && npm run dev

Run locally without Docker

# Backend
pip install -r backend/requirements.txt
DATABASE_URL=postgresql://lens:lens@localhost:5432/lens uvicorn app.main:app --reload

# Frontend
cd frontend
npm install
npm run dev

Running tests

# Backend + pipeline
pytest

# Frontend
cd frontend && npm test

Repo structure

lens/
├── backend/
│   ├── app/
│   │   ├── api/              # route handlers: lens, compare, neighborhoods
│   │   └── main.py           # FastAPI app entry point
│   ├── alembic/              # migrations — never hand-edit schema
│   │   └── versions/
│   ├── tests/                # API endpoint + lens calculation tests
│   └── requirements.txt
├── pipeline/
│   ├── adapters/
│   │   └── sf/               # SF-specific field mappings and ingest script
│   ├── analysis/             # exploratory scripts used to generate spike findings
│   ├── sources/              # Socrata API client
│   └── tests/                # transform unit tests
├── frontend/
│   ├── app/                  # page.tsx — main layout and data fetching
│   ├── components/           # Map, Controls, LensPanel, NeighborhoodPanel, RankingsPanel
│   ├── lib/                  # API helpers, preset events
│   └── types/                # TypeScript types
├── docs/
│   ├── adr/                  # architecture decision records
│   ├── spikes/               # spike findings: what we learned, therefore what we did
│   └── methodology.md        # lens definitions, flag definitions, denominators, limitations
├── docker-compose.yml
└── pytest.ini

Deployment

The app runs on a shared instance — any teammate can access real SF data without cloning the repo or running anything locally.

URL: https://parky-efren-nondynastically.ngrok-free.app

Credentials: none required to view the app. The database uses local dev credentials (lens / lens) and is never exposed outside the host machine.

How it works

docker compose up on the host machine starts the database, backend, and frontend in the correct order, verified by health checks.

Migrations run automatically. The backend startup command chains alembic upgrade head && uvicorn — every time the container starts, any pending migrations are applied before the server accepts traffic.

The frontend runs a production build (next build + next start) rather than the dev server, avoiding the HMR/hydration issues that come with sharing a next dev instance.

The URL is served via ngrok rather than a bare local IP. A local IP only works on the same network, and campus eduroam blocks device-to-device traffic via client isolation. ngrok exposes the instance over a public HTTPS URL that works from anywhere.

About

A retrospective analysis tool for auditing police enforcement patterns using open public datasets from San Francisco. Built for civic oversight analysts, investigative journalists, and academic researchers.

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages