<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Diamond]]></title><description><![CDATA[Diamond]]></description><link>https://tanishqbuilds.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 04 Nov 2026 23:02:43 GMT</lastBuildDate><atom:link href="https://tanishqbuilds.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Real-Time OTT Content Analytics: From Clickstream Telemetry to Intelligent Recommendations]]></title><description><![CDATA[Building a streaming platform's analytics brain with HDFS, NoSQL, MapReduce, Spark, Bloom filters, DGIM, Flajolet-Martin, similarity metrics and CURE]]></description><link>https://tanishqbuilds.hashnode.dev/real-time-ott-content-analytics-from-clickstream-telemetry-to-intelligent-recommendations</link><guid isPermaLink="true">https://tanishqbuilds.hashnode.dev/real-time-ott-content-analytics-from-clickstream-telemetry-to-intelligent-recommendations</guid><dc:creator><![CDATA[Tanishq Kulkarni]]></dc:creator><pubDate>Thu, 22 Oct 2026 22:01:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/604f2bef37d84907f132a2ab/bfd24422-d0e8-4f5e-9ecb-9fb1443ae554.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You press play on a show at 11:40 pm. Before the first frame loads, your phone has already sent a handful of events to a server somewhere: the app opened, you scrolled a row, you hovered over a thumbnail, you tapped it, the player requested a manifest, the first video segment arrived 1.8 seconds later. While you watch, the player keeps talking: a heartbeat every few seconds with your position, bitrate, buffer health and the CDN node serving you. When you quit 14 minutes in, that's an event too.</p>
<p>Multiply that by a few million people on a Saturday night and you have one of the cleanest real-world examples of a Big Data problem I know of. The data is huge, it arrives continuously, it is messy, and the business wants answers from it in seconds, not tomorrow morning.</p>
<hr />
<h2>1. What the clickstream actually looks like</h2>
<p>Before talking about tools, it helps to look at a single event. ReelStream's player emits JSON like this:</p>
<pre><code class="language-json">{
  "event_id": "9f1c2a7e-6b1d-4e0a-9c55-0d7e3b1f8a21",
  "user_id": "u_48213907",
  "session_id": "s_77a91c",
  "content_id": "t_10442",
  "event_type": "heartbeat",
  "position_sec": 1834,
  "bitrate_kbps": 2800,
  "buffer_ms": 6200,
  "rebuffer": false,
  "cdn_edge": "blr-edge-07",
  "device": "android_tv",
  "app_version": "7.3.1",
  "event_time": "2026-10-22T18:10:42.311Z"
}
</code></pre>
<p>There are roughly four families of events:</p>
<table>
<thead>
<tr>
<th>Family</th>
<th>Examples</th>
<th>Why we care</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Discovery</strong></td>
<td><code>row_impression</code>, <code>tile_hover</code>, <code>search_query</code></td>
<td>What people see vs. what they click</td>
</tr>
<tr>
<td><strong>Playback lifecycle</strong></td>
<td><code>play_start</code>, <code>pause</code>, <code>seek</code>, <code>play_end</code></td>
<td>Views, completion rate, drop-off points</td>
</tr>
<tr>
<td><strong>Quality of Experience (QoE)</strong></td>
<td><code>heartbeat</code>, <code>rebuffer</code>, <code>bitrate_switch</code>, <code>error</code></td>
<td>Is the video actually working?</td>
</tr>
<tr>
<td><strong>Engagement</strong></td>
<td><code>add_to_watchlist</code>, <code>rating</code>, <code>share</code></td>
<td>Explicit signals of taste</td>
</tr>
</tbody></table>
<blockquote>
<img src="https://cdn.hashnode.com/uploads/covers/604f2bef37d84907f132a2ab/7c8d2d45-c675-46e9-8b4b-c68688270ccb.png" alt="" style="display:block;margin:0 auto" /></blockquote>
<hr />
<h2>2. Why this is a Big Data problem: the 5 Vs, with numbers</h2>
<p>"Big Data" gets thrown around loosely, so let's check ReelStream against the 5 Vs with actual arithmetic.</p>
<p><strong>Volume.</strong> Say peak concurrency is 2 million viewers and the player sends a heartbeat every 10 seconds. That's 200,000 heartbeats per second at peak, before counting discovery and lifecycle events. At about 400 bytes per event:</p>
<pre><code class="language-plaintext">200,000 events/s × 400 B = 80 MB/s ≈ 288 GB per peak hour
</code></pre>
<p>Averaged over a day (traffic is far from flat), ReelStream lands somewhere around <strong>2–3 TB of raw JSON per day</strong>. Stored with HDFS's default 3x replication, that's 6–9 TB of disk per day. This alone rules out "just put it in MySQL".</p>
<p><strong>Velocity.</strong> Some questions have a shelf life of seconds. If a CDN edge in Bengaluru starts dropping segments during a cricket final, ops needs to know within a minute, not in tomorrow's report.</p>
<p><strong>Variety.</strong> JSON events from apps, CSV dumps from the billing system, a relational content catalog, subtitle files, thumbnails, and free-text search queries in Hinglish. Structured, semi-structured and unstructured all at once.</p>
<p><strong>Veracity.</strong> Clients go offline and flush events hours later. Clocks on cheap Android TVs drift. Message queues deliver at-least-once, so duplicates are normal. Bots scrape the catalog. Any metric you compute has to survive this.</p>
<p><strong>Value.</strong> The payoff is concrete: better recommendations raise watch time, early QoE alerts cut churn, and knowing which titles drive sign-ups shapes content buying decisions worth crores.</p>
<blockquote>
<img src="https://cdn.hashnode.com/uploads/covers/604f2bef37d84907f132a2ab/a5afe212-49d7-49b4-a412-a521a41c0251.png" alt="" style="display:block;margin:0 auto" /></blockquote>
<hr />
<h2>3. The architecture at a glance</h2>
<p>Here's the overall design. Every box in it maps to something in the course syllabus, and the rest of the post goes through them one by one.</p>
<pre><code class="language-plaintext"> Player / App SDKs
        │  (HTTPS, batched JSON)
        ▼
 ┌──────────────┐
 │ Ingest API   │──► Kafka topic: player-events (partitioned by user_id)
 └──────────────┘          │
                           ├──────────────► SPEED LAYER (Spark Structured Streaming)
                           │                 • Bloom filter: dedupe / "already watched"
                           │                 • DGIM: rebuffer counts per CDN edge
                           │                 • Flajolet-Martin / HLL: unique viewers per title
                           │                 • Trending titles (sliding windows)
                           │                       │
                           │                       ▼
                           │                 Redis (live counters)  +  Cassandra (watch history)
                           │
                           └──────────────► BATCH LAYER
                                             • Raw events land in HDFS (Parquet, partitioned by date)
                                             • MapReduce / Spark batch: daily views, genre joins
                                             • Similarity (Jaccard / cosine), CURE user segments
                                                   │
                                                   ▼
                                             Recommendation tables ──► Serving API ──► "Because you watched…"
</code></pre>
<p>This is essentially a <strong>Lambda architecture</strong>: a speed layer that gives fast approximate answers and a batch layer that recomputes exact answers from the full history. I'll come back to whether that's the right choice in the trade-offs section.</p>
<blockquote>
<img src="https://cdn.hashnode.com/uploads/covers/604f2bef37d84907f132a2ab/937b009c-0b9a-4c87-af21-fb96291cd69f.png" alt="" style="display:block;margin:0 auto" /></blockquote>
<hr />
<h2>4. Storage layer, part 1: HDFS for the raw event lake</h2>
<p>Every raw event ReelStream receives is kept. Storage is cheap; being unable to recompute a metric after you discover a bug is expensive. HDFS is the long-term home for this data.</p>
<h3>How HDFS is organised</h3>
<ul>
<li><strong>NameNode</strong>: the master. It holds the filesystem namespace (directories, file names, permissions) and the mapping of each file to its blocks and of each block to the DataNodes holding it. It does not store the data itself. All of this metadata sits in memory, which is why HDFS hates millions of tiny files.</li>
<li><strong>DataNodes</strong>: the workers that store the actual blocks on local disks. They send heartbeats and block reports to the NameNode every few seconds.</li>
<li><strong>Secondary NameNode</strong>: despite the name, it is <em>not</em> a hot standby. It periodically merges the NameNode's edit log into the fsimage (a checkpoint) so restarts are fast. For real failover you run an HA pair with a Standby NameNode and JournalNodes.</li>
<li><strong>Blocks</strong>: files are split into 128 MB blocks (default), and each block is replicated 3 times.</li>
<li><strong>Rack awareness</strong>: with default placement, the first replica goes on the writer's node, the second on a node in a different rack, the third on another node in that same remote rack. You survive a whole rack dying without paying cross-rack bandwidth for every replica.</li>
</ul>
<h3>How ReelStream lays out data in HDFS</h3>
<p>Two decisions here matter more than they look:</p>
<ol>
<li><strong>Partition by date and hour.</strong> A query about yesterday's Hindi originals reads one day's folders, not three years of data.</li>
<li><strong>Compact into large Parquet files.</strong> The streaming job writes small files every minute. A nightly compaction job rewrites them into files close to the 128 MB block size. Parquet's columnar compression typically shrinks this kind of JSON by several times, so the 2–3 TB/day raw estimate becomes a few hundred GB on disk per replica.</li>
</ol>
<h3>The write path, step by step</h3>
<p>When the compaction job writes a file:</p>
<ol>
<li>The client asks the NameNode to create the file. The NameNode checks permissions and records it.</li>
<li>For each block, the NameNode returns a list of three DataNodes (rack-aware).</li>
<li>The client streams data to the first DataNode, which forwards to the second, which forwards to the third. This is the <strong>replication pipeline</strong>.</li>
<li>Acknowledgements flow back along the pipeline. Once all blocks are acknowledged, the client closes the file and the NameNode commits it.</li>
</ol>
<blockquote>
<img src="https://cdn.hashnode.com/uploads/covers/604f2bef37d84907f132a2ab/fc31a2fe-6968-4bbf-a9ee-264355288edb.png" alt="" style="display:block;margin:0 auto" /></blockquote>
<hr />
<h2>5. Storage layer, part 2: NoSQL for things that must be fast</h2>
<p>HDFS is great for scanning terabytes. It is terrible for "what's the resume position for user u_48213907 on title t_10442?" — a question the app asks every time someone opens it. Different access patterns get different stores. The four NoSQL families each earn a spot:</p>
<table>
<thead>
<tr>
<th>NoSQL type</th>
<th>Store (example)</th>
<th>What ReelStream keeps there</th>
<th>Why this type</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Key-value</strong></td>
<td>Redis</td>
<td>Resume positions, live trending counters, session state</td>
<td>Sub-millisecond <code>GET</code>/<code>SET</code> by a single key</td>
</tr>
<tr>
<td><strong>Column-family</strong></td>
<td>Cassandra</td>
<td>Full watch history per user, QoE time series per CDN edge</td>
<td>Huge write throughput, data naturally sorted by time within a partition</td>
</tr>
<tr>
<td><strong>Document</strong></td>
<td>MongoDB</td>
<td>Content catalog: titles, cast, languages, nested season/episode data</td>
<td>Schema varies a lot between a film, a series and a live event</td>
</tr>
<tr>
<td><strong>Graph</strong></td>
<td>Neo4j</td>
<td>Actor ↔ title ↔ director ↔ genre relationships</td>
<td>"Titles sharing two cast members with what you just watched" is a graph traversal</td>
</tr>
</tbody></table>
<h3>CAP theorem: choosing per feature, not per company</h3>
<p>The CAP theorem says that when a network partition happens, a distributed store must choose between <strong>Consistency</strong> (every read sees the latest write) and <strong>Availability</strong> (every request gets a non-error response). Since partitions <em>will</em> happen, the real choice is C or A during one.</p>
<p>The interesting part for OTT is that different features want different answers:</p>
<ul>
<li><strong>"Continue watching" row → AP.</strong> If a partition means you see a resume position that's 30 seconds stale, nobody cares. If the row fails to load, people do. Cassandra with <code>consistency_level = ONE</code> fits.</li>
<li><strong>Subscription and entitlement checks → CP.</strong> If someone just cancelled, or just paid, you'd rather return an error for a second than stream premium content to a non-paying account or block a paying one. This lives in a strongly consistent store (an RDBMS or HBase).</li>
<li><strong>Live view counters → AP, approximately.</strong> A "1.2M watching now" badge that's off by 0.5% is fine.</li>
</ul>
<p>Cassandra also lets you tune this per query. With replication factor 3, reading and writing at <code>QUORUM</code> (2 of 3) guarantees <code>R + W &gt; N</code>, so reads see the latest write. You pay for that with higher latency and lower availability, so ReelStream only uses <code>QUORUM</code> where it matters.</p>
<blockquote>
<img src="https://cdn.hashnode.com/uploads/covers/604f2bef37d84907f132a2ab/d630841c-509b-4cf1-8ac5-ff227aa93d3f.png" alt="" style="display:block;margin:0 auto" /></blockquote>
<hr />
<h2>6. Batch processing with MapReduce</h2>
<p>Every morning, the content team wants yesterday's plays per title and per genre. MapReduce is Word Count with a costume on.</p>
<p><strong>The three phases:</strong> Map emits <code>(content_id, 1)</code> for every <code>play_start</code>. Shuffle groups by key. Reduce sums up.</p>
<p>Pass <code>reducer.py</code> as the <strong>combiner</strong> too — each mapper pre-aggregates locally before the shuffle, cutting network traffic dramatically. Addition is commutative and associative, so this is safe.</p>
<p><strong>Genre join</strong> uses a reduce-side join: tag catalog rows with <code>C</code> and play-count rows with <code>P</code>, key both by <code>content_id</code>, and the reducer reassembles them. If the catalog fits in memory (~30K titles, a few MB), a <strong>map-side broadcast join</strong> is faster — no shuffle needed.</p>
<blockquote>
<img src="https://cdn.hashnode.com/uploads/covers/604f2bef37d84907f132a2ab/0dedfce8-105c-40bc-9bfd-8ad0c777ed99.png" alt="" style="display:block;margin:0 auto" /></blockquote>
<hr />
<h2>7. Spark: the same jobs, faster, plus streaming</h2>
<p>MapReduce writes every intermediate result to HDFS. Spark keeps data in memory across stages — our three-job genre pipeline collapses into one program.</p>
<p><strong>Key concepts.</strong> The <strong>driver</strong> builds a DAG; the <strong>cluster manager</strong> hands it <strong>executors</strong> on worker nodes. Nothing runs until you call an <strong>action</strong>. The DAG splits into <strong>stages</strong> at shuffle boundaries. <strong>Narrow</strong> transformations (map, filter) pipeline with no network I/O; <strong>wide</strong> ones (reduceByKey, join) trigger a shuffle and open a new stage.</p>
<hr />
<h2>8. Stream mining: approximate answers at scale</h2>
<p>Some questions can't be answered exactly in a stream without storing everything. Three algorithms handle the common cases.</p>
<h3>8.1 Bloom filter — "already watched?"</h3>
<p>A bit array of <code>m</code> bits + <code>k</code> hash functions. <strong>Insert:</strong> set <code>k</code> bit positions. <strong>Check:</strong> any bit = 0 → definitely not seen; all 1s → probably seen. No false negatives; false-positive rate is tunable.</p>
<p>For 10M pairs at 1% FPR → <code>m ≈ 12 MB</code>, <code>k = 7</code>. An exact Python set would be ~1 GB.</p>
<p><strong>Context matters:</strong> a false positive in recommendation filtering skips one valid title — harmless. In royalty-per-view deduplication it silently drops a real event — use exact dedupe in the batch layer instead.</p>
<h3>8.2 DGIM — rebuffer count in a sliding window</h3>
<p>Summarises a bit stream with <strong>buckets</strong>: each has a timestamp (of its latest 1) and a size (power of 2). Rules: ≤2 buckets per size; sizes non-decreasing toward the past; merge two oldest when a third of the same size appears. <strong>Estimate</strong> = sum of all sizes − half the oldest bucket.</p>
<p>Memory: <code>O(log² N)</code> vs. <code>N</code> bits exact. Worst-case error ≤ 50% (allow <code>r</code> buckets per size for ~`1/(r−1)` error).</p>
<h3>8.3 Flajolet-Martin — distinct viewers per title</h3>
<p><strong>Intuition:</strong> hash each user ID and count trailing zeros. Half of hashes end in ≥1 zero, a quarter in ≥2, so the maximum trailing-zero count <code>R</code> implies ~`2^R<code>distinct items. Duplicates hash identically — they don't move</code>R`.</p>
<p>Use 64 hash functions; average <code>R</code>; estimate <code>2^(mean R) / φ</code> (φ ≈ 0.7735). Memory: ~256 bytes per title, regardless of audience size.</p>
<p><strong>In production</strong> use HyperLogLog (<code>approx_count_distinct</code> in Spark): one hash, <code>m</code> registers, ~1–2% error. Sketches across partitions merge by element-wise max — no user IDs travel over the network.</p>
<h3>8.4 Exact vs. approximate at a glance</h3>
<table>
<thead>
<tr>
<th>Question</th>
<th>Memory (exact)</th>
<th>Algorithm</th>
<th>Memory (approx)</th>
<th>Error</th>
</tr>
</thead>
<tbody><tr>
<td>Seen this title? (10M pairs)</td>
<td>~1 GB</td>
<td>Bloom filter</td>
<td>~12 MB</td>
<td>1% FP</td>
</tr>
<tr>
<td>1s in last N bits</td>
<td>N bits</td>
<td>DGIM</td>
<td>O(log² N)</td>
<td>≤50%</td>
</tr>
<tr>
<td>Distinct users per title</td>
<td>O(distinct)</td>
<td>FM / HLL</td>
<td>~256 B – few KB</td>
<td>~1–2%</td>
</tr>
</tbody></table>
<hr />
<h2>9. From telemetry to taste: similarity metrics</h2>
<p>Two titles are similar if the same people watch them. Pick the metric that fits your data shape.</p>
<p><strong>Jaccard (sets)</strong> — did someone watch it or not?</p>
<pre><code class="language-plaintext">J(A,B) = |A ∩ B| / |A ∪ B|
</code></pre>
<p>Asha = {Monsoon Files, Hostel Diaries, Chai Pe Charcha, Ward No. 9}, Rahul = {Hostel Diaries, Chai Pe Charcha, Night Shift} → J = 2/5 = <strong>0.4</strong>.</p>
<p><strong>Cosine (vectors)</strong> — how many minutes per genre?</p>
<pre><code class="language-plaintext">cos(A,B) = (A·B) / (‖A‖ ‖B‖)
</code></pre>
<p>Asha = (120,30,0,60), Rahul = (90,0,10,45) → cos ≈ <strong>0.97</strong>. Cosine compares direction, not volume — heavy and light viewers with the same taste still match.</p>
<p><strong>At Spark scale</strong> — generate pairs only from co-watched titles; <code>|A∪B| = |A|+|B|−|A∩B|</code> avoids building unions; filter below 20 co-viewers to suppress noisy niche pairs:</p>
<blockquote>
<p><strong>Heads-up:</strong> users with 500+ titles explode the self-join. Cap at 100 recent titles or use <strong>MinHash + LSH</strong> (<code>spark.ml.feature.MinHashLSH</code>).</p>
</blockquote>
<hr />
<h2>10. Understanding the audience: CURE clustering</h2>
<p>Recommendations work per user; the business thinks in <strong>segments</strong> (night-owl bingers, live-sports loyalists, commute snackers). Clustering gives them that.</p>
<p><strong>Why not k-means?</strong> Viewing clusters are rarely spherical, and k-means is wrecked by outliers (a test account at 22 hours/day).</p>
<p><strong>CURE</strong> (Clustering Using REpresentatives) handles non-spherical clusters and outliers in five steps:</p>
<ol>
<li><strong>Sample</strong> ~200K users (fits in memory).</li>
<li><strong>Hierarchically cluster</strong> the sample, merging by closest representative points.</li>
<li><strong>Pick</strong> <code>c</code> <strong>scattered reps</strong> per cluster — they capture shape, not just the centroid.</li>
<li><strong>Shrink reps</strong> toward the centroid: <code>rep += α·(centroid−rep)</code>. Dampens edge outliers.</li>
<li><strong>Assign all users</strong> to the nearest rep — embarrassingly parallel Spark <code>map</code>.</li>
</ol>
<p><strong>Illustrative segments:</strong></p>
<table>
<thead>
<tr>
<th>Segment</th>
<th>Traits</th>
<th>Business action</th>
</tr>
</thead>
<tbody><tr>
<td>Night-owl bingers</td>
<td>70%+ watch after 10 pm</td>
<td>Drop full seasons; autoplay on</td>
</tr>
<tr>
<td>Live sports loyalists</td>
<td>Spikes on match days only</td>
<td>Retention offers between tournaments</td>
</tr>
<tr>
<td>Commute snackers</td>
<td>Mobile, short sessions, commute hours</td>
<td>Push downloads, short-form</td>
</tr>
<tr>
<td>Dormant</td>
<td>&lt;30 days activity</td>
<td>Win-back campaigns</td>
</tr>
</tbody></table>
<hr />
<h2>11. Putting it together: one viewer, end to end</h2>
<p>Asha opens the app at 11:40 pm:</p>
<ol>
<li><code>app_open</code> → Ingest API → Kafka (partitioned by <code>user_id</code>).</li>
<li><strong>Speed layer</strong> (seconds): Spark updates trending windows, HLL sketch, DGIM rebuffer bucket for her CDN edge. Redis counters tick.</li>
<li><strong>Home screen</strong> (milliseconds): Serving reads resume positions (Redis) + recent history (Cassandra), fetches candidates from the nightly Jaccard similarity table, re-ranks by her CURE segment ("night-owl binger" → full seasons up after 10 pm), filters finished titles via Bloom filter.</li>
<li><strong>During playback</strong>: heartbeats every 10 s. DGIM spike → ops alert → traffic steered to another edge.</li>
<li><strong>Overnight batch</strong>: all events in HDFS as Parquet. Spark recomputes exact plays, genre joins, similarity tables, CURE segments — correcting anything the speed layer approximated or dropped as late.</li>
</ol>
<blockquote>
<img src="https://cdn.hashnode.com/uploads/covers/604f2bef37d84907f132a2ab/717f509e-0ddf-4ffd-9e7d-18774bf620ad.png" alt="" style="display:block;margin:0 auto" /></blockquote>
<hr />
<h2>12. Trade-offs worth arguing about</h2>
<table>
<thead>
<tr>
<th>Dimension</th>
<th>RDBMS / warehouse</th>
<th>ReelStream Big Data stack</th>
</tr>
</thead>
<tbody><tr>
<td>Scaling</td>
<td>Vertical</td>
<td>Horizontal (commodity nodes)</td>
</tr>
<tr>
<td>Schema</td>
<td>Strict, write-time</td>
<td>Flexible raw; strict where needed</td>
</tr>
<tr>
<td>Latency</td>
<td>Hours</td>
<td>Seconds (speed) + hours (batch)</td>
</tr>
<tr>
<td>Consistency</td>
<td>Strong ACID</td>
<td>Per-feature (AP history, CP billing)</td>
</tr>
<tr>
<td>Unstructured data</td>
<td>Poor fit</td>
<td>Native (HDFS, document stores)</td>
</tr>
<tr>
<td>Operational complexity</td>
<td>Low</td>
<td><strong>High</strong></td>
</tr>
</tbody></table>
<blockquote>
<p>A PostgreSQL + nightly job beats this architecture below ~50K users. Big Data tooling pays off only when volume and velocity demand it.</p>
</blockquote>
<p><strong>Lambda vs. Kappa:</strong> Lambda duplicates logic in streaming and batch. Kappa streams everything and replays Kafka for reprocessing. Spark makes the same DataFrame code work in both modes, so Kappa is attractive — but ReelStream keeps batch for exact royalty numbers and heavy iterative jobs (similarity, CURE).</p>
<p><strong>MapReduce vs. Spark:</strong> MapReduce is predictable on huge one-pass jobs; Spark wins on anything iterative or multi-stage. New systems: write in Spark, but understand MapReduce first — it's what makes Spark's stages and shuffles make sense.</p>
<hr />
<h2>13. Closing thoughts</h2>
<p>None of these techniques are exotic alone — a Bloom filter is a bit array and a few hashes; DGIM is a bookkeeping rule; Jaccard is a fraction. What makes an OTT analytics system work is picking the right tool for each question and knowing exactly how wrong each approximation is allowed to be.</p>
<p><strong>Try it:</strong> generate a synthetic event stream in Python, push to local Kafka, run the Structured Streaming job from §7, and compare DGIM and FM estimates against exact counts as the stream grows. Watching approximation error stay flat while memory stays tiny is the moment these algorithms stop feeling like exam topics.</p>
<hr />
<h2>References</h2>
<ol>
<li>Leskovec, Rajaraman, Ullman — <em>Mining of Massive Datasets</em>, 3rd ed.</li>
<li>Dean &amp; Ghemawat — "MapReduce," OSDI 2004</li>
<li>Shvachko et al. — "The Hadoop Distributed File System," MSST 2010</li>
<li>Zaharia et al. — "RDDs," NSDI 2012</li>
<li>Bloom — "Space/Time Trade-offs," CACM 1970</li>
<li>Datar, Gionis, Indyk, Motwani — "DGIM," SIAM 2002</li>
<li>Flajolet &amp; Martin — "Probabilistic Counting," JCSS 1985</li>
<li>Guha, Rastogi, Shim — "CURE," ACM SIGMOD 1998</li>
<li>Gilbert &amp; Lynch — "Brewer's Conjecture," ACM SIGACT 2002</li>
<li>Apache Spark — Structured Streaming Programming Guide</li>
</ol>
<hr />
<p><em>Written as part of the Big Data Analytics course. ReelStream is a fictional platform; all traffic figures are illustrative. Spot an error? Let me know in the comments.</em></p>
]]></content:encoded></item></channel></rss>