krz/omaha-metro-blotter

Archive of police activity and ALPR surveillance across the Omaha metro.

clone: git clone https://gitbay.org/krz/omaha-metro-blotter.git

archive:

100644 .gitattributes83
040000 .github/
100644 .gitignore35
100644 LICENSE639
100644 README.nfo4784
100644 analysis.py5744
100644 app.py9894
040000 assets/
100644 ingest.py12359
040000 notebooks/
040000 raw_data/
100644 requirements-ingest.txt8
100644 requirements.txt72
100644 schema.sql1815
040000 screenshots/
┌──────────────────────────────────────────────────────────────┐
│  O M A H A   I N C I D E N T S             [ KRZ ]   krz.sh  │
└──────────────────────────────────────────────────────────────┘

WHAT
  police activity across the omaha metro, pulled from the agencies'
  own feeds and joined against alpr camera locations from
  openstreetmap.

COVERAGE
  omaha pd                dcgis arcgis view, 2022-01-01 onward,
                          nibrs offence records, updated daily.
                          no stop or disposition data.
  bellevue pd             sarpy county publiccrimemap, cad calls
  papillion pd            for service with stop type, disposition
  la vista pd             and category. rolling 12-month window:
  sarpy county so         records age out of the feed, so the local
                          archive is the only long-term copy. gretna
                          and springfield appear as fire only; their
                          police departments do not report to it.
  council bluffs pd       cbpd public cfs feed, refreshed every ten
                          minutes, rolling 12-month window. stop
                          type, disposition, priority, response time.
                          street addresses withheld; points exact.
  ralston pd              no machine-readable feed. absent.

  alpr cameras            openstreetmap via overpass, the same data
                          deflock renders. 168 nodes in the metro
                          bbox. odbl, attribution required.

SETUP
      uv venv .venv
      uv pip install --python .venv/bin/python -r requirements.txt

USE
      .venv/bin/python ingest.py --full        # first run, backfill
      .venv/bin/python ingest.py               # daily, last 30 days
      .venv/bin/python ingest.py cbpd sarpy    # one source at a time
      .venv/bin/python ingest.py opd_csv       # 2015-2023 csv archive
      .venv/bin/python app.py

ARCHIVE
  .github/workflows/daily-pull.yml runs the pull at 11:17 utc and
  keeps the database as metro.db.gz on the "archive" release, so the
  archive does not depend on any one machine. each run restores that
  asset, pulls, refuses to publish if any source came back with fewer
  rows than it started with, then uploads and fails loudly if a feed
  has not moved in seven days.

  first run: trigger it manually with bootstrap enabled, which pulls
  every feed in full and creates the release. after that the restore
  step is mandatory -- a bootstrap over a live archive throws away
  whatever has already aged out of the sarpy and council bluffs
  feeds.

  github disables scheduled workflows after 60 days without repo
  activity, and emails first. that is the most likely way this stops
  quietly.

  to run the pull locally instead:

      0 6 * * *  cd /path/to/omaha-incidents && .venv/bin/python ingest.py

NOTES
  all three arcgis services return utc epochs; their where-clause
  literals do not agree (opd and council bluffs utc, sarpy central).
  ingest.py stores occurred_at in local time.

  each feed has its own taxonomy and none of them are comparable, so
  ingest.py derives one cross-agency flag, is_stop, per source. stop
  outcomes compare citation and arrest rate by substring, which is
  all the two disposition vocabularies support: an agency that
  records warnings less thoroughly shows a higher citation rate for
  that reason alone.

  colour scheme follows prefers-color-scheme. plotly writes colours
  into the figure, so assets/theme.js reports the media query into a
  store and app.py builds each figure from it. restyling after the
  fact does not work: swapping a maplibre basemap at runtime leaves
  it rebuilding with no data layers.

  the camera-proximity panel compares stops against a non-stop
  baseline. cameras and stops both concentrate on arterials, so a
  gap between the curves is a starting point, not a finding.

  raw_data/ingress.db is the old 2015-2023 sqlite build. nothing
  reads it any more.

SCREENSHOTS
  screenshots/*.png are from the previous 2015-2023 dashboard.

┌──────────────────────────────────────────────────────────────┐
│  krz.sh                                                      │
└──────────────────────────────────────────────────────────────┘