Course: CS4109 Fundamentals of Big Data Analytics
Goal: An end-to-end big-data system that supports batch analytics on historical Chicago crime data, real-time anomaly detection and alerts on a Kafka stream, and a single Streamlit dashboard backed by PostgreSQL and MongoDB.
This report describes a course-scale Lambda Architecture built on Chicago open data: a batch layer for trustworthy historical analytics and machine learning, a speed layer for near-real-time anomaly alerts, and a serving + dashboard layer that makes both paths visible in one place.
Motivation. Crime data is a canonical big-data use case: high row counts, multiple related tables, messy fields, and a need for both aggregate insight (policy, planning) and timely signals (operational awareness). The design intentionally mirrors how production systems separate offline recomputation from online stream processing, then merge results at read time in the UI.
Lambda Architecture (one sentence). Spark recomputes batch views and writes them to PostgreSQL; Kafka buffers live events consumed by Storm, which emits alerts into PostgreSQL (tabular) and MongoDB (documents); Streamlit reads both stores for demonstration.
System at a glance (Lambda). The high-level architecture below is the “map” for the rest of the report.
graph TD
subgraph Data Source
CSV["Chicago Crime CSV (Crimes_-_2001_to_Present)"]
end
subgraph Speed Layer
Producer["Python Kafka Producer"]
Kafka["Apache Kafka"]
Storm["Apache Storm"]
end
subgraph Batch Layer
Spark["Apache Spark"]
end
subgraph Serving Layer
Postgres["PostgreSQL"]
Mongo["MongoDB"]
end
subgraph Presentation
UI["Streamlit Dashboard"]
end
CSV -->|Replay Data| Producer
Producer -->|Publish| Kafka
Kafka -->|Consume| Storm
CSV -->|Batch Load| Spark
Storm -->|Real-Time Alerts| Postgres
Storm -->|Alert Documents| Mongo
Spark -->|Historical Analytics| Postgres
Postgres -->|Query| UI
Mongo -->|Query| UI
City-scale crime data is large, continuously updated, and used for both strategic (long-horizon trends, hotspots) and tactical (unusual bursts of incidents) questions.
This project demonstrates a Lambda Architecture: Spark materializes authoritative batch views into PostgreSQL while Storm consumes Kafka for low-latency district-level windows and writes alerts to PostgreSQL (structured) and MongoDB (document audit trail). The dashboard reads both paths so evaluators see one cohesive system rather than disconnected demos.
The batch job pulls five Chicago-style datasets (paths are globs in config/config.yaml):
| Theme | Typical filename pattern (glob) | Role in analytics |
|---|---|---|
| Crimes (2001–Present) | Crimes*2001*Present*.csv |
Core fact table: timestamps, district, geo, crime type, arrest flag; drives trends, hotspots, Kafka replay |
| Arrests | Arrests*.csv |
Join on case number to compute arrest rates by primary type, district, race |
| Police stations | Police_Stations*.csv |
District metadata + coordinates for labeling; used when building deterministic offender–district attribution |
| Violence reduction / shootings | Violence_Reduction*.csv |
Gunshot injury flags + incident classifications; aggregated into violence / gunshot statistics and correlation inputs |
| Sex offenders / registry-style export | Sex_Offenders*.csv |
Block-level registry rows; aggregated to offender-density-style summaries (see Methodology for join limitations) |
Schema and cleaning choices (why they matter)
- Reads use explicit
StructTypeschemas inspark/batch_job.pyso malformed rows land in_corrupt_recordrather than silently shifting columns. - CSV ingest uses
PERMISSIVEmode for resilience; downstream steps filter critical nulls (e.g.,case_numbernot null). Districtis normalized withtrim, regex extraction, andlpadto three digits so string variants join consistently across tables.- Timestamps are parsed from the Chicago
MM/dd/yyyy hh:mm:ss astrings into realtimestampcolumns for grouping by year / month / day-of-week / hour. - Types:
Arrestis cast to boolean; lat/lon to double for geo clustering. - Streaming replay:
kafka/producer.pyskips rows missinglatitudeorlongitudeso the live map-centric path stays consistent.
Streaming source note. For the speed layer, the Crimes CSV is replayed by the producer into Kafka topic crime_events (one event per row) and then consumed by the Storm topology.
The speed layer parses each event, aggregates by district over a sliding window, tests an anomaly threshold, then persists alerts.
graph LR
A[Crime Event] --> B(Kafka Producer)
B --> C{Kafka Topic: crime_events}
C --> D[Storm Spout]
D --> E[Parse Bolt]
E -->|Tuple| F[District Bolt]
E -->|Tuple| G[Raw Event Bolt]
F -->|Grouped by District| H[Window Count Bolt]
H -->|Windowed Count| I[Anomaly Bolt]
I -->|Threshold Exceeded| J[Alert Bolt]
J -->|SQL Insert| K[(PostgreSQL)]
J -->|BSON Insert| L[(MongoDB)]
PostgreSQL holds analytics and relational alert rows suited to SQL queries; MongoDB holds raw-style alert documents for flexible inspection.
graph TD
subgraph Databases
PG[(PostgreSQL)]
MG[(MongoDB)]
end
subgraph Batch Writes
Spark[Spark Batch Job] -->|Overwrites Trends/Hotspots| PG
end
subgraph Speed Writes
Storm[Storm Topology] -->|Appends Alerts| PG
Storm -->|Appends Documents| MG
end
subgraph Reads
PG -->|Fetch Trends & Alerts| UI[Streamlit Dashboard]
MG -->|Fetch Raw Alerts| UI
end
graph TD
Python[Python 3.12] -->|Simulates Stream| Kafka[Kafka 2.6]
Kafka -->|Buffers Events| Storm[Storm 2.6.2]
Storm -->|JDBC| Postgres[PostgreSQL 16]
Storm -->|Mongo Driver| Mongo[MongoDB 7.0]
Spark[Spark 3.5.0] -->|Heavy Analytics| Postgres
Streamlit[Streamlit 1.41] -->|Visualizes| Postgres
Streamlit -->|Visualizes| Mongo
Kafka in Docker uses Confluent images (cp-kafka:7.6.1); broker API version strings in client logs may still show values like 2.6 from compatibility negotiation. Storm and Spark versions match the topology and Spark UI you run in Compose.
sequenceDiagram
participant CSV as CSV File
participant Prod as Python Producer
participant Kafka as Kafka Topic
participant Spout as Storm Spout
participant Bolts as Storm Bolts (Parse/Window/Anomaly/Alert)
participant DB as Databases (Postgres/Mongo)
participant UI as Streamlit Dashboard
CSV->>Prod: Read Row
Prod->>Kafka: Publish Event
Kafka->>Spout: Poll Message
Spout->>Bolts: Emit Tuple
Note over Bolts: Parse -> Window -> Detect Anomaly
Bolts->>DB: Write Alert (if count > threshold)
UI->>DB: Query for Alerts
DB-->>UI: Return Data
UI->>UI: Update Display
Aligned with the Lambda architecture decomposition:
- Batch layer (Apache Spark,
spark/batch_job.py) — typed ingest + cleaning, historical analytics tables, geospatial K-Means hotspots, and correlation-style outputs into PostgreSQL. - Speed layer (
kafka/producer.py+ Storm topology) — Kafka replay + sliding-window anomaly detection into PostgreSQL and MongoDB. - Serving layer (PostgreSQL + MongoDB) — SQL-friendly analytics and alerts + flexible alert documents.
- Dashboard layer (
dashboard/app.py) — Streamlit reads both stores for a single integrated demonstration.
- Explicit Spark schemas —
CRIME_SCHEMA,ARRESTS_SCHEMA,POLICE_SCHEMA,VIOLENCE_SCHEMA, andSEX_SCHEMAinspark/batch_job.pypin column order/type for every CSV variant you place under/app/datain Docker ordata/locally. - Null handling and casts —
coalesce(..., "UNKNOWN")for optional dimensions in arrest-rate groups; left join crime⟕arrests keeps all crimes; geo clustering uses only rows with non-null lat/lon;dropDuplicateson arrests bycase_numberavoids double-counting when joining. - Sliding-window anomaly detection (Storm) — Configurable window length, slide interval, and threshold (see
docker/docker-compose.ymlfor the values used in the graded demo;config/config.yamlcarries related defaults for documentation). - K-Means geospatial hotspots —
VectorAssembleron(latitude, longitude);KMeanswith (k =)spark.kmeans_k(default 10 in config) and seed 4109 for reproducibility; cluster sizes are joined back to centroid coordinates for the hotspots table and Streamlit map. - Cross-dataset joins and correlations — Crime ⟕ Arrests on
case_numberpowers multi-dimensional arrest rates. Violence rollups include homicide vs shooting style breakdowns, top community areas, and gunshot injury proportion by district. A correlations table stores aligned ((x, y)) pairs for violence count vs district arrest rate and offender count vs district crime count for secondary analysis (e.g., external plotting or future dashboard panels). - Offender density vs true spatial join (limitation) — Registered sex offender rows in this extract often lack a police district key. The implementation therefore assigns each row to a synthetic district index via
hash(block) mod Nand maps that index to the ordered list of police districts—a deterministic device for cross-district comparison, not a certified geospatial containment join. See Results and Limitations.
This section presents the core outcomes (trends, hotspots, arrest rates, alerts) together with screenshots that validate the end-to-end system.
This section maps outcomes to the evidence above and the tables Spark materializes in PostgreSQL.
- Crime trends — Year / month / hour aggregations in
crime_trends; dashboard tabs and the “Crime Trends by …” figures in Evidence: screenshots. - Arrest rates — Left-joined crime–arrest data produces
arrest_rates(by primary type, district, race); Top Arrest Rates table andTopArrestRates.png. - Violence and gunshot analysis —
violence_statscaptures homicide vs shooting style metrics, community-area concentration, and gunshot injury proportion by district (see batch job). Suitable for extended charts beyond the default Streamlit panels. - Offender density and deterministic district assignment —
offender_densityaggregates registry rows per district using the hash-mod assignment described under Methodology; interpret as a relative cross-district signal, not ground-truth geocoding. - Hotspot centroids —
hotspotsfrom K-Means on lat/lon; map view in the dashboard and Hotspot Centroids figure. - Alert examples — PostgreSQL
alertstable in the dashboard’s Latest Alerts grid; MongoDBalert_logsraw documents in the expander and in MongoDB alert documents screenshot.
- Role: Offline computation over the full historical dataset—time trends, hotspots, arrest-rate style summaries—and write-through into PostgreSQL for fast dashboard reads.
- Why it fits: Dataset scale and aggregation depth suit a batch engine; results are refreshed explicitly when you submit the job (reproducible for grading).
- Role: Continuous consumption of
crime_events, sliding-window counts per district, threshold-based anomalies, JDBC inserts to Postgres and document inserts to MongoDB. - Why it fits: Storm’s spout/bolt model maps directly onto parse → route → window → detect → persist; Kafka decouples the producer from storm processing rates.
- The Python producer skips rows missing required fields (e.g. latitude/longitude) so bad records do not corrupt streaming or mislead hotspots.
Symptom: spark-submit with org.postgresql:postgresql failed when Ivy tried to write under a missing or non-writable cache path.
Fix: Use a writable Ivy root, for example --conf spark.jars.ivy=/tmp/.ivy2, as reflected in the runbook below.
Symptom: Dashboard alert tables stayed empty despite a live Kafka producer.
Cause: Threshold too high for the replay pattern (many districts splitting the traffic).
Fix: Tune Docker env for Storm (see docker/docker-compose.yml): e.g. WINDOW_SIZE_SECONDS=300, SLIDE_INTERVAL_SECONDS=10, ANOMALY_THRESHOLD=5.
Symptom: New topology submits but consumes nothing.
Cause: Older topology instance still attached to the same consumer group; with a single partition, only one consumer receives messages.
Fix: Kill superseded topologies (storm kill <name>) before relying on a new submission, or use distinct consumer identities per graded run.
Challenge: The crimes extract is a multi-million-row file; repeated full scans during iteration are slow on a laptop and stress Docker disk I/O.
Mitigation: Spark’s column pruning and overwrite-once batch job keep recomputation explicit; optional limits in config/config.yaml (crime_sample_limit, other_sample_limit, sample_mode) support faster dev cycles when needed.
Challenge: Chicago exports use human-readable headers (spaces, mixed case) and joins such as crime ↔ arrest depend on case number alignment; violence and sex-offender extracts use different key conventions than the core crime file.
Mitigation: Centralized rename + cast steps, dropDuplicates on arrests, and documented limitations (e.g., offender-to-district hashing) instead of silent wrong joins.
Challenge: Coordinating ZooKeeper, Kafka, Storm, Spark, two databases, and Streamlit with correct ports, dependencies, and env is error-prone on Windows/WSL2.
Mitigation: A single docker/docker-compose.yml encodes service order, env for Storm, and published ports; the runbook below matches what graders can execute end-to-end.
- Docker Desktop on Windows
- Python 3.12 + project venv (for the local Kafka producer)
docker compose -f docker/docker-compose.yml up -ddocker compose -f docker/docker-compose.yml exec spark-master `
/opt/spark/bin/spark-submit `
--master spark://spark-master:7077 `
--conf spark.jars.ivy=/tmp/.ivy2 `
--packages org.postgresql:postgresql:42.7.4 `
/app/spark/batch_job.py `
--config /app/config/config.yamldocker compose -f docker/docker-compose.yml exec storm-nimbus `
storm jar /storm-app/target/crime-storm-topology-1.0.0.jar `
edu.nu.crimeanalytics.topology.CrimeAnalyticsTopology `
crime-analytics-v9python kafka/producer.py --config config/config.yaml| Service | URL |
|---|---|
| Streamlit | http://localhost:8501 |
| Storm UI | http://localhost:8081 |
| Spark Master UI | http://localhost:8080 |
| Where | What |
|---|---|
docker/docker-compose.yml |
WINDOW_SIZE_SECONDS, SLIDE_INTERVAL_SECONDS, ANOMALY_THRESHOLD, JDBC and Mongo URIs for Storm |
config/config.yaml |
Kafka bootstrap, topic, DB endpoints, dashboard wiring |
config/ # runtime YAML (connections, topic, etc.)
dashboard/ # Streamlit app
db/ # Postgres + Mongo init scripts
docker/ # Compose, Spark image, Storm config
kafka/ # CSV replay producer
spark/ # PySpark batch job
storm/ # Java topology (spouts/bolts)
Figs/ # Report screenshots and terminal evidence
- Operational metrics: surface Kafka lag, Storm bolt latency, and end-to-end alert delay for quantitative streaming evaluation.
- Scale-out: increase topic partitions and Storm workers for higher throughput tests.
- Richer anomalies: baseline per district/time-of-week instead of one global threshold; optional seasonal adjustment.
- Geospatial refinement: grid-based hotspots (e.g. H3) and reproducible CRS handling for centroid maps.
Generative AI assistance (e.g. Codex, Antigravity, Cursor) was used to scaffold, debug, and document parts of this project—including README structure, Docker/Spark troubleshooting notes, and code review-style refactors.














