Module 2 ยท Lecture 9

Smarter Consumers:
Aggregations & Alert Routing

Buffer ยท Compute Stats ยท Flag Anomalies ยท Route to MongoDB

โฑ 90 min
๐ŸŽ“ MBA Streaming Data Analytics
๐Ÿ“ Continues from Lecture 8
02 / 09Overview

Where We Are & What We Build

L6โ€“L7
producer โ†’ consumer
โ†’
L8
save to stock_prices
โ†’
L9
aggregate + alert routing

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.

Today's Build โ€” Three Steps

1
Buffer & Aggregate
Collect N ticks per symbol, compute sum / mean / min / max when buffer is full
2
Alert Rule โ€” 5% Deviation
Alert if min or max in this batch deviates >5% from the previous close
3
Route to MongoDB
All messages โ†’ stock_prices ยท Alerts only โ†’ stock_alerts
TimeActivityType
0โ€“15Recap L8 + why aggregate in the consumerDiscussion
15โ€“45Step 1 โ€” buffer logic & stats code walkthroughTheory + Code
45โ€“75Steps 2 & 3 โ€” alert rule + MongoDB routingTheory + Code
75โ€“90Hands-on: run consumer_alerts.py, check AtlasLab
03 / 09Theory

Why Aggregate in the Consumer?

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.

Sum
ฮฃ
Total value traded in the batch โ€” useful for volume-weighted logic
Mean
xฬ„
Average price across the batch โ€” becomes the new "close" reference
Min
โ†“
Lowest price โ€” used to detect downside breaks vs previous close
Max
โ†‘
Highest price โ€” used to detect upside breaks vs previous close

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.

04 / 09Step 1 โ€” Code

Buffer & Compute Stats

We accumulate incoming prices in a per-symbol list. When the buffer reaches BUFFER_SIZE, we compute stats and reset.

python ยท buffer + stats setup
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

Sample Output โ€” Step 1 Only

๐Ÿ“ก Listening on stock-topic...
โ†ณ [INFY] โ‚น1410.20 buffer 1/5
โ†ณ [INFY] โ‚น1415.80 buffer 2/5
โ†ณ [INFY] โ‚น1408.50 buffer 3/5
โ†ณ [INFY] โ‚น1420.00 buffer 4/5
โ†ณ [INFY] โ‚น1412.30 buffer 5/5
๐Ÿ“Š [INFY] Batch stats โ€” sum=7066.80 mean=1413.36 min=1408.50 max=1420.00
โ†ณ [INFY] โ‚น1401.10 buffer 1/5 โ† next batch starts

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.

05 / 09Step 2 โ€” Alert Rule

From Stats to Alerts โ€” the 5% Rule

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%

python ยท check_alert()
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
Scenarioprev_closeBatch minDeviationResult
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,220max โ‚น3,420+6.2%๐Ÿšจ ALERT
06 / 09Step 3 โ€” Routing

Two Collections, One Consumer

The routing logic is a single if. Same consumer, same Kafka topic โ€” two very different use cases served.

python ยท step 3 routing logic
# 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
    })

What Each Collection Contains

CollectionContentsWho Uses It
stock_pricesEvery raw tick from Kafka โ€” one document per messageAll users, Atlas Charts dashboards
stock_alertsOnly batches where min/max broke 5% โ€” with full stats + reasonRisk 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.

stock-topic
Kafka (L6 producer)
โ†’
consumer_alerts.py
buffer โ†’ stats โ†’ route
โ†’
stock_prices
every message
โ†˜
stock_alerts
if >5% deviation
07 / 09Full Code

consumer_alerts.py โ€” Complete File

python ยท consumer_alerts.py (key sections)
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] = []
08 / 09Hands-On Lab

Lab โ€” Run & Observe

What Must Be Running

TerminalCommandStatus
1docker compose up -dZookeeper + Kafka
2python producer.pyStreaming stock-topic
3python consumer_alerts.pyโ† Today's file
4 (optional)python check_alerts.pyQuery both collections

Expected Terminal Output

โœ… Connected to MongoDB Atlas
Prices โ†’ sda_course.stock_prices
Alerts โ†’ sda_course.stock_alerts
๐Ÿ“ก Listening on stock-topic (Ctrl+C to stop)...
 
โ†ณ [INFY] โ‚น1410.20 buffer 1/5
โ†ณ [INFY] โ‚น1415.80 buffer 2/5
โ†ณ [TCS] โ‚น3280.00 buffer 1/5
โ†ณ [INFY] โ‚น1408.50 buffer 3/5
โ†ณ [INFY] โ‚น1420.00 buffer 4/5
โ†ณ [INFY] โ‚น1412.30 buffer 5/5
๐Ÿ“Š [INFY] Batch stats โ€” sum=7066.80 mean=1413.36 min=1408.50 max=1420.00
โœ… [INFY] No alert โ€” within ยฑ5.0% of close N/A โ† first batch has no ref
 
โ†ณ [INFY] โ‚น1340.00 buffer 1/5
... (4 more ticks)
๐Ÿ“Š [INFY] Batch stats โ€” sum=6720.00 mean=1344.00 min=1330.00 max=1360.00
๐Ÿšจ ALERT [INFY] โ†’ MIN_DEV 1330.00 (-5.9% vs close 1413.36)

Check Atlas After 2โ€“3 Minutes

09 / 09Assignment + Next

Assignment 2 & What's Next

Assignment 2 โ€” Due 1 September

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.

DeliverableDescription
Producer scriptGenerates and streams your domain data to a Kafka topic
Consumer scriptReads from the topic, saves to MongoDB (minimum one collection)
Atlas screenshotShow 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.

Coming Up

LectureTopicBuilds On
L10Real Dataset โ€” Banking TransactionsFresh transactions-topic, fraud detection producer
L11Windowing & AggregationsTumbling / sliding / session windows on transactions-topic
L12โ€“L13Spark & Flink OverviewWhere today's consumer logic fits at scale
1 / 9