advanced⏱60-90 minutesLesson 10 of 10
Advanced Project: Real-Time Streaming Analytics with ML Inference
Capstone: build a real-time streaming analytics pipeline with ML inference using Structured Streaming, MLlib, and Delta Lake
Advanced Project: Real-Time Streaming Analytics with ML Inference
This capstone project combines structured streaming, ML models, window operations, Delta Lake, and monitoring into a complete real-time analytics pipeline.
Project Overview
You will build a real-time pipeline that:
- Ingests streaming event data from Kafka
- Computes real-time aggregations with tumbling windows
- Runs ML model inference on streaming data
- Stores raw and aggregated data in Delta Lake
- Detects anomalies using statistical methods
- Monitors pipeline health with custom metrics
- Handles late-arriving data with watermarks
Architecture
IoT Devices / Web Apps
|
v
Kafka (events topic)
|
v
Structured Streaming
|-- Windowed Aggregations (5-min tumbling)
|-- ML Inference (fraud detection model)
|-- Anomaly Detection (statistical thresholds)
|-- Raw Event Storage (Delta Lake)
|
v
Delta Lake Tables
|-- raw_events
|-- agg_events
|-- predictions
|-- anomalies
|
v
Consumption (BI, dashboards, alerts)
Setup
python
from pyspark.sql import SparkSession
from pyspark.sql.functions import *
from pyspark.sql.types import *
from pyspark.sql.window import Window
from delta.tables import DeltaTable
import json
spark = SparkSession.builder \
.appName("RealTimeStreamingAnalytics") \
.master("local[*]") \
.config("spark.sql.extensions", "io.delta.sql.DeltaSparkSessionExtension") \
.config("spark.sql.catalog.spark_catalog", "org.apache.spark.sql.delta.catalog.DeltaCatalog") \
.config("spark.sql.streaming.checkpointLocation", "/tmp/checkpoints/analytics") \
.config("spark.sql.adaptive.enabled", "true") \
.config("spark.sql.shuffle.partitions", "10") \
.getOrCreate()
sc = spark.sparkContext
sc.setLogLevel("WARN")Step 1: Generate Synthetic Streaming Data
python
import random
from datetime import datetime, timedelta
import time
def generate_events(num_events=1000):
"""Generate synthetic streaming events for testing."""
users = [f"user_{i}" for i in range(100)]
events = ["purchase", "click", "login", "logout", "view", "add_to_cart"]
categories = ["electronics", "clothing", "home", "sports", "books"]
events_data = []
base_time = datetime.now()
for i in range(num_events):
event_time = base_time - timedelta(
seconds=random.randint(0, 3600),
milliseconds=random.randint(0, 1000)
)
amount = round(random.uniform(5.0, 500.0), 2) if random.random() > 0.3 else 0.0
events_data.append({
"user_id": random.choice(users),
"event_type": random.choice(events),
"amount": amount,
"category": random.choice(categories),
"ip_address": f"{random.randint(1,255)}.{random.randint(0,255)}.{random.randint(0,255)}.{random.randint(1,255)}",
"timestamp": event_time.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3],
"device": random.choice(["web", "mobile", "tablet"]),
"session_id": f"sess_{random.randint(1000, 9999)}"
})
return events_data
# Write test data as JSON files (simulating streaming)
import os
os.makedirs("data/streaming_input", exist_ok=True)
for batch in range(5):
batch_data = generate_events(200)
batch_file = f"data/streaming_input/batch_{batch}.json"
with open(batch_file, "w") as f:
for event in batch_data:
f.write(json.dumps(event) + "\n")
print(f"Wrote {batch_file}: {len(batch_data)} events")
time.sleep(1) # Simulate delay between batchesStep 2: Define Schema and Read Stream
python
from pyspark.sql.types import StructType, StructField, StringType, DoubleType, TimestampType
schema = StructType([
StructField("user_id", StringType()),
StructField("event_type", StringType()),
StructField("amount", DoubleType()),
StructField("category", StringType()),
StructField("ip_address", StringType()),
StructField("timestamp", StringType()),
StructField("device", StringType()),
StructField("session_id", StringType())
])
# Read streaming data from directory
raw_stream = spark.readStream \
.schema(schema) \
.option("maxFilesPerTrigger", 1) \
.json("data/streaming_input/")
# Parse timestamp
parsed_stream = raw_stream \
.withColumn("event_time", to_timestamp(col("timestamp"), "yyyy-MM-dd HH:mm:ss.SSS")) \
.withColumn("processing_time", current_timestamp()) \
.drop("timestamp")
# Cache checkpoint
parsed_stream.printSchema()Success
Using a file source for testing makes development simple. In production, replace with Kafka: spark.readStream.format("kafka").option("subscribe", "events").
Step 3: Train ML Model (Pre-computed)
python
from pyspark.ml.feature import VectorAssembler, StandardScaler
from pyspark.ml.classification import LogisticRegression
from pyspark.ml import Pipeline
# Train a simple fraud detection model
training_data = spark.createDataFrame([
("purchase", 500.0, 0), ("purchase", 25.0, 0), ("purchase", 450.0, 1),
("click", 0.0, 0), ("login", 0.0, 0), ("purchase", 300.0, 0),
("purchase", 475.0, 1), ("add_to_cart", 0.0, 0), ("purchase", 50.0, 0),
("purchase", 490.0, 1), ("purchase", 100.0, 0), ("purchase", 480.0, 1)
], ["event_type", "amount", "is_fraud"])
# Feature engineering
feature_assembler = VectorAssembler(
inputCols=["amount"],
outputCol="features_raw"
)
scaler = StandardScaler(
inputCol="features_raw",
outputCol="features"
)
classifier = LogisticRegression(
featuresCol="features",
labelCol="is_fraud",
maxIter=50
)
ml_pipeline = Pipeline(stages=[feature_assembler, scaler, classifier])
fraud_model = ml_pipeline.fit(training_data)
# Save model
fraud_model.write().overwrite().save("models/fraud_detection")
print("Fraud detection model trained and saved.")Note
In a real deployment, retrain the model periodically with batch jobs and use the streaming pipeline only for inference. Load the latest model version from a model registry.
Step 4: Streaming Inference Pipeline
python
from pyspark.ml import PipelineModel
# Load pre-trained model
loaded_model = PipelineModel.load("models/fraud_detection")
# Apply model to streaming data
features_for_ml = parsed_stream \
.filter(col("event_type") == "purchase") \
.withColumn("amount", col("amount").cast("double"))
predictions = loaded_model.transform(features_for_ml)
# Select relevant columns
ml_results = predictions.select(
col("user_id"),
col("event_time"),
col("amount"),
col("event_type"),
col("category"),
col("device"),
col("prediction").alias("is_fraud_prediction"),
col("probability").getItem(1).alias("fraud_probability")
)Step 5: Real-Time Aggregations with Windows
python
# Windowed aggregations
windowed_aggs = parsed_stream \
.withWatermark("event_time", "10 minutes") \
.groupBy(
window(col("event_time"), "5 minutes"),
col("event_type"),
col("category")
) \
.agg(
count("*").alias("event_count"),
sum("amount").alias("total_amount"),
avg("amount").alias("avg_amount"),
countDistinct("user_id").alias("unique_users"),
countDistinct("ip_address").alias("unique_ips")
)
# Rolling user stats
user_window = Window.partitionBy("user_id") \
.orderBy(col("event_time")) \
.rowsBetween(-100, Window.currentRow)
rolling_stats = parsed_stream \
.withColumn("user_event_count", count("*").over(user_window)) \
.withColumn("user_total_amount", sum("amount").over(user_window))Step 6: Anomaly Detection
python
# Statistical anomaly detection
anomaly_check = parsed_stream \
.withWatermark("event_time", "10 minutes") \
.groupBy(
window(col("event_time"), "5 minutes"),
col("user_id")
) \
.agg(
count("*").alias("event_count"),
sum("amount").alias("total_amount"),
countDistinct("ip_address").alias("ip_count")
) \
.withColumn("is_anomalous_rate",
when(col("event_count") > 100, lit(True))
.otherwise(lit(False))
) \
.withColumn("is_anomalous_amount",
when(col("total_amount") > 10000, lit(True))
.otherwise(lit(False))
) \
.withColumn("is_anomalous_geo",
when(col("ip_count") > 5, lit(True))
.otherwise(lit(False))
) \
.withColumn("anomaly_score",
col("is_anomalous_rate").cast("int") +
col("is_anomalous_amount").cast("int") +
col("is_anomalous_geo").cast("int")
) \
.filter(col("anomaly_score") > 0)Step 7: Write to Delta Lake Sinks
python
# Raw data sink
raw_query = parsed_stream.writeStream \
.format("delta") \
.outputMode("append") \
.option("checkpointLocation", "/tmp/checkpoints/raw_events") \
.table("raw_events")
# Aggregations sink
agg_query = windowed_aggs.writeStream \
.format("delta") \
.outputMode("append") \
.option("checkpointLocation", "/tmp/checkpoints/agg_events") \
.table("agg_events")
# ML predictions sink
ml_query = ml_results.writeStream \
.format("delta") \
.outputMode("append") \
.option("checkpointLocation", "/tmp/checkpoints/predictions") \
.table("predictions")
# Anomaly alerts sink
anomaly_query = anomaly_check.writeStream \
.format("delta") \
.outputMode("append") \
.option("checkpointLocation", "/tmp/checkpoints/anomalies") \
.table("anomalies")
# Console output for monitoring
console_query = ml_results.writeStream \
.format("console") \
.outputMode("append") \
.option("truncate", "false") \
.queryName("fraud_alerts") \
.start()Step 8: Monitoring and Observability
python
# Custom monitoring accumulators
events_processed = sc.accumulator(0)
fraud_detected = sc.accumulator(0)
anomalies_detected = sc.accumulator(0)
def monitor_stream(query, name):
"""Monitor streaming query progress."""
def report():
progress = query.lastProgress
if progress:
print(f"[{name}] Rows: {progress.get('numInputRows', 0)}, "
f"Rate: {progress.get('inputRowsPerSecond', 0):.1f}/s, "
f"Batch duration: {progress.get('durationMs', {}).get('triggerExecution', 0)}ms")
return report
# Register progress monitor
import threading
def monitoring_loop(interval=5):
while True:
for q in spark.streams.active:
if q.lastProgress:
p = q.lastProgress
print(f"[{q.name}] Rows: {p.get('numInputRows', 0)}, "
f"Rate: {p.get('inputRowsPerSecond', 0):.1f}/s")
time.sleep(interval)
monitor_thread = threading.Thread(target=monitoring_loop, daemon=True)
monitor_thread.start()Step 9: Query Results
python
# Query Delta tables (in another session or after streaming)
spark.sql("SELECT * FROM raw_events LIMIT 5").show(truncate=False)
spark.sql("SELECT * FROM agg_events ORDER BY window DESC LIMIT 10").show()
spark.sql("SELECT * FROM predictions WHERE is_fraud_prediction = 1 LIMIT 10").show()
spark.sql("SELECT * FROM anomalies ORDER BY anomaly_score DESC LIMIT 10").show()
# Aggregate fraud metrics
fraud_metrics = spark.sql("""
SELECT category,
COUNT(*) as total_purchases,
SUM(is_fraud_prediction) as fraud_count,
ROUND(AVG(fraud_probability), 4) as avg_fraud_prob
FROM predictions
GROUP BY category
ORDER BY fraud_count DESC
""")
fraud_metrics.show()
# Check latest watermark
for q in spark.streams.active:
status = q.status
print(f"Stream: {q.name}")
print(f" Status: {status['message']}")Step 10: Cleanup and Final Script
python
# Complete pipeline script
class RealTimeAnalyticsPipeline:
def __init__(self, spark):
self.spark = spark
self.queries = []
def build_pipeline(self):
schema = self._define_schema()
stream = self._read_stream(schema)
parsed = self._parse_timestamp(stream)
model = self._load_model()
# Parallel streams
self.queries.append(self._write_raw(parsed))
self.queries.append(self._write_aggregations(parsed))
self.queries.append(self._write_predictions(parsed, model))
self.queries.append(self._write_anomalies(parsed))
return self
def start(self):
for q in self.queries:
q.start()
return self
def await_termination(self):
self.spark.streams.awaitAnyTermination()
def stop(self):
for q in self.queries:
q.stop()
# Run the pipeline
pipeline = RealTimeAnalyticsPipeline(spark)
pipeline.build_pipeline().start()
try:
pipeline.await_termination()
except KeyboardInterrupt:
print("Shutting down...")
pipeline.stop()Business Questions
Answer these using your pipeline results:
- What is the average fraud probability per category?
- Which users show anomalous behavior patterns?
- How do event counts vary across 5-minute windows?
- What is the peak events-per-minute rate?
- Which device type has the highest fraud rate?
- What is the ratio of purchases to other events?
- How many unique anomalies were detected?
- What is the average processing latency?
- Which hours have the highest transaction volumes?
- What is the distribution of anomaly scores?
Practice Questions
- How does the streaming pipeline apply ML inference to each batch?
- How do watermarks and windows work together in this pipeline?
- Why are aggregations written with
appendmode? - How would you add more features to the fraud detection model?
- How do you monitor streaming query health in production?
- What is the purpose of the
foreachBatchsink in this pipeline? - How would you handle model retraining without downtime?
- What Delta Lake features ensure exactly-once semantics?
- How would you scale this pipeline for 10x more data?
- How do you implement alerting when fraud probability exceeds a threshold?
Progress100%