Buffer ยท Compute Stats ยท Flag Anomalies ยท Route to MongoDB
In Lecture 8 we saved every tick to MongoDB stock_prices. Today we make the consumer smarter โ it now computes statistics on batches of prices and automatically flags significant moves.
sum / mean / min / max when buffer is fullmin or max in this batch deviates >5% from the previous closestock_prices ยท Alerts only โ stock_alerts| Time | Activity | Type |
|---|---|---|
| 0โ15 | Recap L8 + why aggregate in the consumer | Discussion |
| 15โ45 | Step 1 โ buffer logic & stats code walkthrough | Theory + Code |
| 45โ75 | Steps 2 & 3 โ alert rule + MongoDB routing | Theory + Code |
| 75โ90 | Hands-on: run consumer_alerts.py, check Atlas | Lab |
Raw tick data is noisy. A single bad tick โ a data quality issue, a momentary spike โ can trigger false alerts. Aggregating over a small window gives you a more stable signal.
Decision Rule: If min or max moves more than 5% away from where prices were before this batch (prev_close), something significant has happened โ raise an alert.
The mean of each completed batch becomes the new prev_close for the next batch. This rolling reference prevents alerts from chasing price after a large move.
We accumulate incoming prices in a per-symbol list. When the buffer reaches BUFFER_SIZE, we compute stats and reset.
from collections import defaultdict
import statistics
BUFFER_SIZE = 5 # compute stats after every 5 ticks per symbol
# State: one buffer and one prev_close entry per symbol
price_buffer = defaultdict(list) # {"INFY": [1410, 1415, 1408, 1420, 1412]}
prev_close = {} # {"INFY": 1413.0} โ updated each batch
def compute_stats(prices):
return {
"count": len(prices),
"sum": round(sum(prices), 2),
"mean": round(statistics.mean(prices), 2),
"min": round(min(prices), 2),
"max": round(max(prices), 2),
}
# Inside the consumer loop:
price_buffer[symbol].append(price)
if len(price_buffer[symbol]) >= BUFFER_SIZE:
stats = compute_stats(price_buffer[symbol])
print(f"sum={stats['sum']} mean={stats['mean']} min={stats['min']} max={stats['max']}")
price_buffer[symbol] = [] # reset buffer
Teaching point: The consumer now "thinks" in batches, not individual ticks. This is the foundation of all stream processing systems โ micro-batching reduces noise before decision logic runs.
After every completed batch, compare the batch's min and max against prev_close โ the mean from the previous batch.
Alert condition: |min โ prev_close| / prev_close > 5% OR |max โ prev_close| / prev_close > 5%
ALERT_PCT = 5.0 # alert threshold in percent
def check_alert(symbol, stats):
if symbol not in prev_close:
return [] # first batch โ no reference yet
pc = prev_close[symbol]
reasons = []
min_dev = abs(stats["min"] - pc) / pc * 100
max_dev = abs(stats["max"] - pc) / pc * 100
if min_dev > ALERT_PCT:
reasons.append(f"MIN_DEV {stats['min']:.2f} ({-min_dev:.1f}% vs close {pc:.2f})")
if max_dev > ALERT_PCT:
reasons.append(f"MAX_DEV {stats['max']:.2f} (+{max_dev:.1f}% vs close {pc:.2f})")
return reasons # empty list = no alert
# After batch is full:
reasons = check_alert(symbol, stats)
prev_close[symbol] = stats["mean"] # update reference for next batch
| Scenario | prev_close | Batch min | Deviation | Result |
|---|---|---|---|---|
| Normal market | โน1,413 | โน1,405 | โ0.6% | โ No alert |
| Modest drop | โน1,413 | โน1,375 | โ2.7% | โ No alert |
| Sharp fall | โน1,413 | โน1,330 | โ5.9% | ๐จ ALERT |
| Spike up | โน3,220 | max โน3,420 | +6.2% | ๐จ ALERT |
The routing logic is a single if. Same consumer, same Kafka topic โ two very different use cases served.
# Every message โ stock_prices (unchanged from L8)
prices_col.insert_one({**data, "_saved_at": datetime.utcnow().isoformat()})
# When buffer full โ route alert to stock_alerts if triggered
if reasons:
alerts_col.insert_one({
"symbol": symbol,
"stats": stats, # full sum/mean/min/max of the batch
"prev_close": prev_close.get(symbol),
"alert_reasons": reasons,
"alert_at": datetime.utcnow(),
"status": "open", # open | reviewed | resolved
})
| Collection | Contents | Who Uses It |
|---|---|---|
stock_prices | Every raw tick from Kafka โ one document per message | All users, Atlas Charts dashboards |
stock_alerts | Only batches where min/max broke 5% โ with full stats + reason | Risk team, portfolio managers, A2 dashboard |
Key insight: stock_alerts never needs querying for "did anything happen?" โ existence in the collection is the signal. No filter required. Perfect for Atlas Charts "total open alerts" widgets.
from kafka import KafkaConsumer
from pymongo import MongoClient
from collections import defaultdict
from datetime import datetime
import json, statistics
BUFFER_SIZE = 5 # ticks per batch
ALERT_PCT = 5.0 # deviation threshold %
prices_col = db["stock_prices"] # every message
alerts_col = db["stock_alerts"] # flagged batches
price_buffer = defaultdict(list)
prev_close = {}
for msg in consumer:
symbol, price = data["symbol"], float(data["price"])
# Step 1: save every tick
prices_col.insert_one({**data, "_saved_at": datetime.utcnow().isoformat()})
# Step 2: buffer
price_buffer[symbol].append(price)
if len(price_buffer[symbol]) >= BUFFER_SIZE:
stats = compute_stats(price_buffer[symbol])
reasons = check_alert(symbol, stats)
if reasons:
alerts_col.insert_one({
"symbol": symbol, "stats": stats,
"prev_close": prev_close.get(symbol),
"alert_reasons": reasons,
"alert_at": datetime.utcnow(), "status": "open"
})
print(f"๐จ ALERT [{symbol}] โ {', '.join(reasons)}")
prev_close[symbol] = stats["mean"]
price_buffer[symbol] = []
| Terminal | Command | Status |
|---|---|---|
| 1 | docker compose up -d | Zookeeper + Kafka |
| 2 | python producer.py | Streaming stock-topic |
| 3 | python consumer_alerts.py | โ Today's file |
| 4 (optional) | python check_alerts.py | Query both collections |
stats (full batch), prev_close, alert_reasons, status: "open"Based on your A1 industry/data source selection, create sample data and configure it to send to Kafka. The two-collection pattern from today (raw + alerts) is a good architecture reference for your submission.
| Deliverable | Description |
|---|---|
| Producer script | Generates and streams your domain data to a Kafka topic |
| Consumer script | Reads from the topic, saves to MongoDB (minimum one collection) |
| Atlas screenshot | Show at least one collection with 5+ documents saved |
Tag your GitHub submission: [A2][SDA-1], [A2][SDA-2], or [A2][SDA-G] based on your section.
| Lecture | Topic | Builds On |
|---|---|---|
| L10 | Real Dataset โ Banking Transactions | Fresh transactions-topic, fraud detection producer |
| L11 | Windowing & Aggregations | Tumbling / sliding / session windows on transactions-topic |
| L12โL13 | Spark & Flink Overview | Where today's consumer logic fits at scale |