What is actually true about this dataset, as of the last nightly run. The headline is the first table: every figure the dashboard publishes, recomputed here in SQL from the same artifacts by a different route. Nine of those are row counts and seven compare the output of a rule that had to be reimplemented, number for number. Everything below that is either a defect worth knowing about or a characteristic that surprises people and is nonetheless correct.
build_aggregates.py counts in pandas and writes
coverage_summary.json, which the dashboard prints. These models
recount the same things in SQL from the same files. Where the two agree, a
published figure is corroborated by an independent implementation rather than
merely asserted. The value checks matter most: a count check passes even if the
heatmap put every incident in the wrong hour, because moving weight between
hours conserves the total.
| Measure | Published | Recounted | Verdict |
|---|---|---|---|
| total_rows | 23,052.0 | 23,052.0 | agree |
| valid_rows | 23,045.0 | 23,045.0 | agree |
| year_1900_sentinel | 5.0 | 5.0 | agree |
| future_dates | 2.0 | 2.0 | agree |
| zone_assigned_rows | 21,904.0 | 21,904.0 | agree |
| zone_unassigned_rows | 1,141.0 | 1,141.0 | agree |
| time_darkness_rows_used | 22,986.0 | 22,986.0 | agree |
| calendar_days | 3,835.0 | 3,835.0 | agree |
| weather_days_used | 3,810.0 | 3,810.0 | agree |
| darkness_overall_fraction | 0.366 | 0.36581462349822436 | differ |
| darkness_overall_baseline | 0.439 | 0.43864870645900467 | differ |
| darkness_overall_bend | -0.073 | -0.07283408296077942 | differ |
| darkness_by_offense_max_diff | 0.0 | 0.00048277230682014505 | differ |
| darkness_by_offense_count_mismatches | 0.0 | 0.0 | agree |
| heatmap_max_cell_diff | 0.0 | 0.0049867218915835565 | differ |
| heatmap_cells_beyond_tolerance | 0.0 | 0.0 | agree |
All 168 hour-by-weekday cells. An incident reported as a range contributes a fraction to each hour it covers rather than a whole incident to the first hour, which is why the cells hold decimals. The 168 cells sum to exactly the number of timed incidents, and a test enforces that.
Mean incidents per day inside each cohort, against the all-days baseline of 6.01 incidents per day. Warm bars run above the baseline, cool bars below. These cohorts overlap: a day can be freezing and a weekend at once, so the rows share days and must never be added together. Each is only comparable to the baseline.
Across all incidents the mean dark fraction is 0.366 against a baseline of 0.439: the average incident falls in civil darkness about 7 points less than its own day's night length would predict. Below, the offenses that bend furthest either way. Positive means the offense happens in darkness more than the season alone explains. Read it with the caveat that a wide reported window spreads its weight uniformly, so offenses usually discovered later rather than witnessed live drift darker than they were.
The open-data stream carries a precise occur_datetime, which invites
reading it as a real instant. Times pile up on round values far past anything a
real distribution would produce. Exactly midnight carries
55 times the uniform
per-minute rate and exactly noon
50 times. Those
two are the signature of a date-only record or a form defaulting to 12:00 AM,
not of 535 incidents happening at
the same second. Across 45 clustered values, 5,707 incidents sit
on a value carrying at least three times the uniform rate, and 4,293 of
those sit exactly on the hour: 31% of every timestamped incident.
Read any hour-of-day figure with that in mind.
Two independent sources feed this dataset upstream, and they overlap only partly. The dashboard-only share is the reason the second stream exists, and the reason a third of incidents have no narrative text for topics or keyword flags to read.
Things that are wrong and could in principle be fixed. Classes overlap, so the counts do not sum: the flagged-rows tile above counts distinct incidents rather than adding these rows. Note the grain column, because not every defect is counted in incidents.
| Defect | Count | Grain | Share | What it means |
|---|---|---|---|---|
| pdf_dashboard_date_mismatch | 2,360 | incident | 10.24% | The PDF bulletin and the open-data dashboard disagree on the date by at least a day. Upstream keeps the PDF date and flags the row. |
| midnight_or_noon_timestamp | 1,023 | incident | 4.44% | Dashboard timestamps landing exactly on 00:00 or 12:00, at roughly fifty times the uniform per-minute rate. Almost certainly date-only or defaulted entries rather than incidents at that instant. |
| weather_gap_days | 25 | day | Calendar days in range with no station observation. Any per-day weather cohort silently excludes these. | |
| ungeocoded | 17 | incident | 0.07% | No coordinates, so the incident cannot be placed on the map or assigned to a zone. |
| published_count_disagreement | 5 | measure | Measures where this project recounted the published coverage summary and got a different answer. | |
| date_sentinel | 5 | incident | 0.02% | The date parser could not read a date at all and fell back to 1900-01-01. |
| dead_gazetteer_flag | 3 | flag | Gazetteer flags that never fire on any narrative. Either the terms never appear in this corpus or the pattern is wrong. | |
| date_future | 2 | incident | 0.01% | Reported date is later than today, which means a typo at the source. |
How often each gazetteer flag fires, over the 15,508 narratives that exist to be scanned. The terms behind each flag stay private to the pipeline; only the names and the counts are here. 3 flags never fire at all, which means the dashboard offers filters that return nothing: outcome_flags_refused_charges, firearm_terms_firearm_recovery, items_stolen_firearm_taken.
The numbers people ask about. None of these is a defect. They are here because every one of them has been mistaken for one at least once.
| Characteristic | Rows | Share | What it means |
|---|---|---|---|
| usable_rows | 23,045 | 100.00% | Rows that survive the date filters and back every published count. |
| zone_assigned | 21,904 | 95.05% | Incidents inside one of the eight policing zone polygons. |
| post_regime_change | 16,091 | 69.82% | On or after 2022-01-01, when open-data ingestion began. Before that, coverage is PDF bulletins only. |
| has_narrative | 15,508 | 67.29% | Rows with narrative text. Everything downstream of language, topics and flags alike, can only see these. |
| topic_assigned | 15,508 | 67.29% | Narratives BERTopic placed in a theme, noise bucket included. |
| source_pdf_only | 8,996 | 39.04% | Only the PDF activity bulletin reported this incident. |
| time_window_is_range | 8,136 | 35.30% | Reported as a span rather than a moment. The heatmap spreads these across the hours they cover. |
| no_narrative | 7,537 | 32.71% | Rows with no narrative. Dashboard-only incidents arrive as structured NIBRS charges with no prose. |
| topic_unassigned | 7,537 | 32.71% | No topic row at all, because there was no narrative to embed. |
| source_dashboard_only | 7,534 | 32.69% | Only the open-data dashboard reported it. Mostly post-2022, and the reason both streams exist. |
| source_both | 6,515 | 28.27% | Both streams reported it and upstream merged them on the complaint number. |
| time_overnight_window | 3,084 | 13.38% | Time window crosses midnight, so the hour arithmetic has to wrap. |
| topic_noise | 2,909 | 12.62% | Narratives BERTopic declined to cluster. A real answer about the text, not a failure. |
| zone_unassigned | 1,141 | 4.95% | Geocoded outside every zone polygon, or not geocoded at all. An Oak Park address on a boundary street can land just outside. |
| multi_charge_incidents | 398 | 1.73% | NIBRS incidents carrying more than one charge, collapsed upstream into one row with arrays. |
| time_unknown_sentinel | 26 | 0.11% | Time recorded as unknown. Excluded from every hour-of-day and darkness measure. |
dbt docs renders test definitions but never their results, so this is the half the docs site cannot show. Warnings are deliberate and documented in the test file itself; a failure here would be a genuine regression.
| Test | Target | Status | Failures |
|---|---|---|---|
| singular | assert_no_dead_gazetteer_flags | warn | 3 |
| singular | assert_hour_dow_weight_conserved | pass | |
| singular | assert_topic_shares_sum_to_one | pass | |
| singular | assert_zone_population_rollup_consistent | pass | |
| singular | assert_coverage_parity | pass | |
| singular | assert_trend_marts_conserve_totals | pass | |
| not_null | agg_darkness_by_offense.offense | pass | |
| unique | agg_darkness_by_offense.offense | pass | |
| accepted_range | agg_flag_prevalence.prevalence | pass | |
| accepted_values | agg_flag_prevalence.flag_family | pass | |
| not_null | agg_flag_prevalence.flag_name | pass | |
| unique | agg_flag_prevalence.flag_name | pass | |
| accepted_range | agg_incidents_by_hour_dow.day_of_week | pass | |
| accepted_range | agg_incidents_by_hour_dow.hour_of_day | pass | |
| not_null | agg_incidents_by_hour_dow.day_of_week | pass | |
| not_null | agg_incidents_by_hour_dow.hour_of_day | pass | |
| unique_combination_of_columns | agg_incidents_by_hour_dow | pass | |
| unique_combination_of_columns | agg_incidents_by_zone_month | pass | |
| unique_combination_of_columns | agg_offense_trends | pass | |
| accepted_range | agg_time_precision.hour_of_day | pass | |
| not_null | agg_time_precision.incidents | pass | |
| not_null | agg_time_precision.time_of_day | pass | |
| unique | agg_time_precision.time_of_day | pass | |
| unique_combination_of_columns | agg_topic_trends | pass | |
| accepted_values | agg_weather_cohorts.cohort_kind | pass | |
| not_null | agg_weather_cohorts.cohort | pass | |
| unique | agg_weather_cohorts.cohort | pass | |
| accepted_values | data_parity_summary.check_kind | pass | |
| not_null | data_parity_summary.dbt_value | pass | |
| not_null | data_parity_summary.measure | pass | |
| not_null | data_parity_summary.published_value | pass | |
| unique | data_parity_summary.measure | pass | |
| not_null | data_profile_summary.characteristic | pass | |
| not_null | data_profile_summary.row_count | pass | |
| unique | data_profile_summary.characteristic | pass | |
| accepted_values | data_quality_summary.grain | pass | |
| not_null | data_quality_summary.defect_class | pass | |
| unique | data_quality_summary.defect_class | pass | |
| not_null | dim_date.date_day | pass | |
| not_null | dim_date.incidents | pass | |
| unique | dim_date.date_day | pass | |
| not_null | dim_offense.incidents | pass | |
| not_null | dim_offense.offense | pass | |
| unique | dim_offense.offense | pass | |
| not_null | dim_topic.incidents | pass | |
| not_null | dim_topic.topic_id | pass | |
| unique | dim_topic.topic_id | pass | |
| not_null | dim_zone.population | pass | |
| not_null | dim_zone.zone_id | pass | |
| not_null | dim_zone.zone_name | pass | |
| unique | dim_zone.zone_id | pass | |
| accepted_values | fct_incidents.date_class | pass | |
| accepted_values | fct_incidents.source | pass | |
| not_null | fct_incidents.date_day | pass | |
| not_null | fct_incidents.record_id | pass | |
| unique | fct_incidents.record_id | pass | |
| accepted_range | int_incident_darkness.dark_fraction | pass | |
| not_null | int_incident_darkness.dark_fraction | pass | |
| not_null | int_incident_darkness.record_id | pass | |
| unique | int_incident_darkness.record_id | pass | |
| accepted_values | int_incidents_dated.date_class | pass | |
| not_null | int_incidents_dated.record_id | pass | |
| unique | int_incidents_dated.record_id | pass | |
| not_null | int_incidents_enriched.record_id | pass | |
| unique | int_incidents_enriched.record_id | pass | |
| not_null | int_month_spine.month_start | pass | |
| unique | int_month_spine.month_start | pass | |
| not_null | int_zone_reference.zone_id | pass | |
| unique | int_zone_reference.zone_id | pass | |
| not_null | stg_calendar.date_day | pass | |
| unique | stg_calendar.date_day | pass | |
| not_null | stg_coverage_summary.total_rows | pass | |
| not_null | stg_incident_details.record_id | pass | |
| relationships | stg_incident_details.record_id | pass | |
| unique | stg_incident_details.record_id | pass | |
| accepted_values | stg_incidents.source | pass | |
| not_null | stg_incidents.date_day | pass | |
| not_null | stg_incidents.record_id | pass | |
| unique | stg_incidents.record_id | pass | |
| not_null | stg_narrative_flags.record_id | pass | |
| relationships | stg_narrative_flags.record_id | pass | |
| unique | stg_narrative_flags.record_id | pass | |
| accepted_range | stg_narrative_topics.topic_prob | pass | |
| not_null | stg_narrative_topics.record_id | pass | |
| relationships | stg_narrative_topics.record_id | pass | |
| unique | stg_narrative_topics.record_id | pass | |
| not_null | stg_policing_zones.zone_id | pass | |
| not_null | stg_policing_zones.zone_name | pass | |
| unique | stg_policing_zones.zone_id | pass | |
| not_null | stg_published_darkness.offense | pass | |
| unique | stg_published_darkness.offense | pass | |
| not_null | stg_published_heatmap.published_weight | pass | |
| not_null | stg_topic_words.topic_id | pass | |
| not_null | stg_topic_words.word | pass | |
| unique_combination_of_columns | stg_topic_words | pass | |
| not_null | stg_weather_daily.date_day | pass | |
| unique | stg_weather_daily.date_day | pass | |
| not_null | stg_zone_demographics.population | pass | |
| not_null | stg_zone_demographics.zone_id | pass | |
| unique | stg_zone_demographics.zone_id | pass |