Oak Park Crime Dashboard Data Docs

Data quality report

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.

Incidents
23,045
usable rows behind every published count
Models
33
staging, intermediate and marts, all built nightly
Tests passing
99
definitions sit on every one of 546 columns
Tests warning
1
deliberate flags, not regressions
Published figures corroborated
11/16
recounted in SQL against the pandas original
Rows flagged
10%
2,375 distinct rows carry a defect, almost all the upstream date artifact

Do the published numbers hold up?

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.

MeasurePublishedRecounted Verdict
total_rows23,052.023,052.0agree
valid_rows23,045.023,045.0agree
year_1900_sentinel5.05.0agree
future_dates2.02.0agree
zone_assigned_rows21,904.021,904.0agree
zone_unassigned_rows1,141.01,141.0agree
time_darkness_rows_used22,986.022,986.0agree
calendar_days3,835.03,835.0agree
weather_days_used3,810.03,810.0agree
darkness_overall_fraction0.3660.36581462349822436differ
darkness_overall_baseline0.4390.43864870645900467differ
darkness_overall_bend-0.073-0.07283408296077942differ
darkness_by_offense_max_diff0.00.00048277230682014505differ
darkness_by_offense_count_mismatches0.00.0agree
heatmap_max_cell_diff0.00.0049867218915835565differ
heatmap_cells_beyond_tolerance0.00.0agree

When does Oak Park report crime?

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.

Mon 00:00 · 157.9Mon 01:00 · 93.5Mon 02:00 · 90.2Mon 03:00 · 79.5Mon 04:00 · 74.3Mon 05:00 · 71.1Mon 06:00 · 71.3Mon 07:00 · 103.5Mon 08:00 · 143.1Mon 09:00 · 147.8Mon 10:00 · 164.3Mon 11:00 · 153.0Mon 12:00 · 235.0Mon 13:00 · 171.6Mon 14:00 · 177.8Mon 15:00 · 199.4Mon 16:00 · 208.0Mon 17:00 · 183.7Mon 18:00 · 189.0Mon 19:00 · 178.3Mon 20:00 · 135.5Mon 21:00 · 134.0Mon 22:00 · 130.4Mon 23:00 · 111.8Tue 00:00 · 153.2Tue 01:00 · 97.9Tue 02:00 · 84.1Tue 03:00 · 62.1Tue 04:00 · 81.4Tue 05:00 · 68.0Tue 06:00 · 77.9Tue 07:00 · 92.3Tue 08:00 · 141.8Tue 09:00 · 138.3Tue 10:00 · 163.2Tue 11:00 · 202.2Tue 12:00 · 228.6Tue 13:00 · 198.2Tue 14:00 · 215.8Tue 15:00 · 221.4Tue 16:00 · 187.9Tue 17:00 · 182.1Tue 18:00 · 167.2Tue 19:00 · 185.6Tue 20:00 · 153.8Tue 21:00 · 153.2Tue 22:00 · 128.6Tue 23:00 · 104.3Wed 00:00 · 165.8Wed 01:00 · 98.9Wed 02:00 · 94.0Wed 03:00 · 89.9Wed 04:00 · 76.6Wed 05:00 · 68.9Wed 06:00 · 69.7Wed 07:00 · 100.6Wed 08:00 · 127.2Wed 09:00 · 142.9Wed 10:00 · 185.9Wed 11:00 · 160.9Wed 12:00 · 254.5Wed 13:00 · 222.0Wed 14:00 · 209.9Wed 15:00 · 213.0Wed 16:00 · 200.3Wed 17:00 · 188.2Wed 18:00 · 169.3Wed 19:00 · 152.3Wed 20:00 · 156.6Wed 21:00 · 126.9Wed 22:00 · 116.8Wed 23:00 · 99.1Thu 00:00 · 155.8Thu 01:00 · 80.7Thu 02:00 · 83.2Thu 03:00 · 86.7Thu 04:00 · 72.9Thu 05:00 · 77.7Thu 06:00 · 63.9Thu 07:00 · 100.2Thu 08:00 · 110.4Thu 09:00 · 142.6Thu 10:00 · 144.8Thu 11:00 · 226.1Thu 12:00 · 233.1Thu 13:00 · 195.0Thu 14:00 · 207.9Thu 15:00 · 205.6Thu 16:00 · 205.8Thu 17:00 · 167.4Thu 18:00 · 168.8Thu 19:00 · 155.8Thu 20:00 · 168.5Thu 21:00 · 164.6Thu 22:00 · 130.2Thu 23:00 · 105.3Fri 00:00 · 169.6Fri 01:00 · 96.8Fri 02:00 · 80.6Fri 03:00 · 83.9Fri 04:00 · 71.1Fri 05:00 · 61.6Fri 06:00 · 60.0Fri 07:00 · 91.9Fri 08:00 · 107.7Fri 09:00 · 143.7Fri 10:00 · 164.2Fri 11:00 · 165.6Fri 12:00 · 241.5Fri 13:00 · 203.1Fri 14:00 · 170.6Fri 15:00 · 206.2Fri 16:00 · 197.6Fri 17:00 · 181.9Fri 18:00 · 182.2Fri 19:00 · 143.9Fri 20:00 · 159.2Fri 21:00 · 128.9Fri 22:00 · 139.2Fri 23:00 · 122.2Sat 00:00 · 148.5Sat 01:00 · 107.4Sat 02:00 · 89.2Sat 03:00 · 87.4Sat 04:00 · 87.6Sat 05:00 · 65.2Sat 06:00 · 55.6Sat 07:00 · 82.5Sat 08:00 · 101.0Sat 09:00 · 115.4Sat 10:00 · 132.7Sat 11:00 · 129.8Sat 12:00 · 166.1Sat 13:00 · 143.5Sat 14:00 · 130.1Sat 15:00 · 189.8Sat 16:00 · 161.1Sat 17:00 · 152.4Sat 18:00 · 157.5Sat 19:00 · 137.7Sat 20:00 · 151.4Sat 21:00 · 133.0Sat 22:00 · 131.5Sat 23:00 · 123.7Sun 00:00 · 143.0Sun 01:00 · 116.7Sun 02:00 · 92.9Sun 03:00 · 86.0Sun 04:00 · 91.3Sun 05:00 · 88.4Sun 06:00 · 68.3Sun 07:00 · 69.7Sun 08:00 · 78.4Sun 09:00 · 88.7Sun 10:00 · 94.5Sun 11:00 · 93.5Sun 12:00 · 177.2Sun 13:00 · 158.0Sun 14:00 · 152.2Sun 15:00 · 136.3Sun 16:00 · 140.8Sun 17:00 · 148.5Sun 18:00 · 155.8Sun 19:00 · 141.0Sun 20:00 · 131.1Sun 21:00 · 116.3Sun 22:00 · 114.2Sun 23:00 · 114.2MonTueWedThuFriSatSun0003060912151821fewer to more, peak 254 in one hourhour of day

Weather moves reported crime more than anything else here

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.

hothot · +23% · 214+23%rainyrainy · -1% · 767-1%holidayholiday · -9% · 152-9%weekendweekend · -12% · 1089-12%freezingfreezing · -17% · 981-17%snowysnowy · -20% · 340-20%coldcold · -21% · 313-21%baseline

Darkness explains less than people expect

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.

DUI ARRESTDUI ARREST · +28% · 116+28%BURGLARY TO AUTOBURGLARY TO AUTO · +23% · 30+23%DRIVING UNDER THE INFLUENCE A…DRIVING UNDER THE INFLUENCE ARREST · +22% · 25+22%THEFT OF MOTOR VEHICLETHEFT OF MOTOR VEHICLE · +17% · 88+17%ATTEMPT MOTOR VEHICLE THEFTATTEMPT MOTOR VEHICLE THEFT · +17% · 27+17%ARMED ROBBERYARMED ROBBERY · +15% · 116+15%Driving Under the InfluenceDriving Under the Influence · +15% · 69+15%Forcible RapeForcible Rape · +14% · 113+14%False Pretenses/Swindle/Confi…False Pretenses/Swindle/Confidence Game · -21% · 371-21%All Other LarcenyAll Other Larceny · -22% · 2585-22%TRAFFIC ARRESTTRAFFIC ARREST · -24% · 26-24%RETAIL THEFT ARRESTRETAIL THEFT ARREST · -24% · 220-24%BICYCLE THEFTBICYCLE THEFT · -25% · 81-25%ShopliftingShoplifting · -27% · 1476-27%RETAIL THEFTRETAIL THEFT · -27% · 444-27%CRIMINAL TRESPASS TO REAL PRO…CRIMINAL TRESPASS TO REAL PROPERTY ARREST · -33% · 32-33%baseline

An exact timestamp is not an exact time

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.

00:0000:00 · 53553512:0012:00 · 48848818:0018:00 · 28628620:0020:00 · 25925917:0017:00 · 24624619:0019:00 · 24224216:0016:00 · 23023015:0015:00 · 22222222:0022:00 · 21921921:0021:00 · 21121114:0014:00 · 18218208:0008:00 · 180180

Where the two ingest streams came from

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.

pdf · 8,996 (39.0%)39%dashboard · 7,534 (32.7%)33%both · 6,515 (28.3%)28%pdf · 8,996dashboard · 7,534both · 6,515

Defects

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.

pdf_dashboard_date_mismatchThe 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. · 2,3602,360midnight_or_noon_timestampDashboard 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. · 1,0231,023ungeocodedNo coordinates, so the incident cannot be placed on the map or assigned to a zone. · 1717date_sentinelThe date parser could not read a date at all and fell back to 1900-01-01. · 55date_futureReported date is later than today, which means a typo at the source. · 22
DefectCountGrain ShareWhat it means
pdf_dashboard_date_mismatch2,360incident10.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_timestamp1,023incident4.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_days25dayCalendar days in range with no station observation. Any per-day weather cohort silently excludes these.
ungeocoded17incident0.07%No coordinates, so the incident cannot be placed on the map or assigned to a zone.
published_count_disagreement5measureMeasures where this project recounted the published coverage summary and got a different answer.
date_sentinel5incident0.02%The date parser could not read a date at all and fell back to 1900-01-01.
dead_gazetteer_flag3flagGazetteer flags that never fire on any narrative. Either the terms never appear in this corpus or the pattern is wrong.
date_future2incident0.01%Reported date is later than today, which means a typo at the source.

Keyword flag prevalence

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.

outcome_flags_arrestoutcome_flags_arrest · 3,5493,549location_types_commerciallocation_types_commercial · 2,0012,001vehicle_mo_unlocked_entryvehicle_mo_unlocked_entry · 1,7201,720items_stolen_bicycleitems_stolen_bicycle · 1,4711,471location_types_garagelocation_types_garage · 1,0001,000outcome_flags_recoveredoutcome_flags_recovered · 826826outcome_flags_warrantoutcome_flags_warrant · 770770items_stolen_packageitems_stolen_package · 768768location_types_porchlocation_types_porch · 666666firearm_terms_firearmfirearm_terms_firearm · 641641vehicle_mo_stolen_vehiclevehicle_mo_stolen_vehicle · 579579vehicle_mo_catalytic_convertervehicle_mo_catalytic_converter · 564564outcome_flags_report_onlyoutcome_flags_report_only · 536536items_stolen_identityitems_stolen_identity · 530530

Characteristics

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.

CharacteristicRowsShare What it means
usable_rows23,045100.00%Rows that survive the date filters and back every published count.
zone_assigned21,90495.05%Incidents inside one of the eight policing zone polygons.
post_regime_change16,09169.82%On or after 2022-01-01, when open-data ingestion began. Before that, coverage is PDF bulletins only.
has_narrative15,50867.29%Rows with narrative text. Everything downstream of language, topics and flags alike, can only see these.
topic_assigned15,50867.29%Narratives BERTopic placed in a theme, noise bucket included.
source_pdf_only8,99639.04%Only the PDF activity bulletin reported this incident.
time_window_is_range8,13635.30%Reported as a span rather than a moment. The heatmap spreads these across the hours they cover.
no_narrative7,53732.71%Rows with no narrative. Dashboard-only incidents arrive as structured NIBRS charges with no prose.
topic_unassigned7,53732.71%No topic row at all, because there was no narrative to embed.
source_dashboard_only7,53432.69%Only the open-data dashboard reported it. Mostly post-2022, and the reason both streams exist.
source_both6,51528.27%Both streams reported it and upstream merged them on the complaint number.
time_overnight_window3,08413.38%Time window crosses midnight, so the hour arithmetic has to wrap.
topic_noise2,90912.62%Narratives BERTopic declined to cluster. A real answer about the text, not a failure.
zone_unassigned1,1414.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_incidents3981.73%NIBRS incidents carrying more than one charge, collapsed upstream into one row with arrays.
time_unknown_sentinel260.11%Time recorded as unknown. Excluded from every hour-of-day and darkness measure.

Test outcomes

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.

TestTargetStatusFailures
singularassert_no_dead_gazetteer_flagswarn3
singularassert_hour_dow_weight_conservedpass
singularassert_topic_shares_sum_to_onepass
singularassert_zone_population_rollup_consistentpass
singularassert_coverage_paritypass
singularassert_trend_marts_conserve_totalspass
not_nullagg_darkness_by_offense.offensepass
uniqueagg_darkness_by_offense.offensepass
accepted_rangeagg_flag_prevalence.prevalencepass
accepted_valuesagg_flag_prevalence.flag_familypass
not_nullagg_flag_prevalence.flag_namepass
uniqueagg_flag_prevalence.flag_namepass
accepted_rangeagg_incidents_by_hour_dow.day_of_weekpass
accepted_rangeagg_incidents_by_hour_dow.hour_of_daypass
not_nullagg_incidents_by_hour_dow.day_of_weekpass
not_nullagg_incidents_by_hour_dow.hour_of_daypass
unique_combination_of_columnsagg_incidents_by_hour_dowpass
unique_combination_of_columnsagg_incidents_by_zone_monthpass
unique_combination_of_columnsagg_offense_trendspass
accepted_rangeagg_time_precision.hour_of_daypass
not_nullagg_time_precision.incidentspass
not_nullagg_time_precision.time_of_daypass
uniqueagg_time_precision.time_of_daypass
unique_combination_of_columnsagg_topic_trendspass
accepted_valuesagg_weather_cohorts.cohort_kindpass
not_nullagg_weather_cohorts.cohortpass
uniqueagg_weather_cohorts.cohortpass
accepted_valuesdata_parity_summary.check_kindpass
not_nulldata_parity_summary.dbt_valuepass
not_nulldata_parity_summary.measurepass
not_nulldata_parity_summary.published_valuepass
uniquedata_parity_summary.measurepass
not_nulldata_profile_summary.characteristicpass
not_nulldata_profile_summary.row_countpass
uniquedata_profile_summary.characteristicpass
accepted_valuesdata_quality_summary.grainpass
not_nulldata_quality_summary.defect_classpass
uniquedata_quality_summary.defect_classpass
not_nulldim_date.date_daypass
not_nulldim_date.incidentspass
uniquedim_date.date_daypass
not_nulldim_offense.incidentspass
not_nulldim_offense.offensepass
uniquedim_offense.offensepass
not_nulldim_topic.incidentspass
not_nulldim_topic.topic_idpass
uniquedim_topic.topic_idpass
not_nulldim_zone.populationpass
not_nulldim_zone.zone_idpass
not_nulldim_zone.zone_namepass
uniquedim_zone.zone_idpass
accepted_valuesfct_incidents.date_classpass
accepted_valuesfct_incidents.sourcepass
not_nullfct_incidents.date_daypass
not_nullfct_incidents.record_idpass
uniquefct_incidents.record_idpass
accepted_rangeint_incident_darkness.dark_fractionpass
not_nullint_incident_darkness.dark_fractionpass
not_nullint_incident_darkness.record_idpass
uniqueint_incident_darkness.record_idpass
accepted_valuesint_incidents_dated.date_classpass
not_nullint_incidents_dated.record_idpass
uniqueint_incidents_dated.record_idpass
not_nullint_incidents_enriched.record_idpass
uniqueint_incidents_enriched.record_idpass
not_nullint_month_spine.month_startpass
uniqueint_month_spine.month_startpass
not_nullint_zone_reference.zone_idpass
uniqueint_zone_reference.zone_idpass
not_nullstg_calendar.date_daypass
uniquestg_calendar.date_daypass
not_nullstg_coverage_summary.total_rowspass
not_nullstg_incident_details.record_idpass
relationshipsstg_incident_details.record_idpass
uniquestg_incident_details.record_idpass
accepted_valuesstg_incidents.sourcepass
not_nullstg_incidents.date_daypass
not_nullstg_incidents.record_idpass
uniquestg_incidents.record_idpass
not_nullstg_narrative_flags.record_idpass
relationshipsstg_narrative_flags.record_idpass
uniquestg_narrative_flags.record_idpass
accepted_rangestg_narrative_topics.topic_probpass
not_nullstg_narrative_topics.record_idpass
relationshipsstg_narrative_topics.record_idpass
uniquestg_narrative_topics.record_idpass
not_nullstg_policing_zones.zone_idpass
not_nullstg_policing_zones.zone_namepass
uniquestg_policing_zones.zone_idpass
not_nullstg_published_darkness.offensepass
uniquestg_published_darkness.offensepass
not_nullstg_published_heatmap.published_weightpass
not_nullstg_topic_words.topic_idpass
not_nullstg_topic_words.wordpass
unique_combination_of_columnsstg_topic_wordspass
not_nullstg_weather_daily.date_daypass
uniquestg_weather_daily.date_daypass
not_nullstg_zone_demographics.populationpass
not_nullstg_zone_demographics.zone_idpass
uniquestg_zone_demographics.zone_idpass