OTT watch time: completion rate, binge sessions and double counting
In this article (8 sections)
- Commissioning decisions rest on these three numbers
- What the player sends
- Three synthetic viewers, one weekend
- Where the 318 minutes came from
- Two completion rates from the same plays
- Binge counts shrink once attendance is checked
- Checks before the numbers leave the team
- What the programming head should receive
Related programme: the Data Science course (6 months; ₹1,30,000 (inclusive of GST) · EMI from ₹5,000/month).
OTT watch time should be computed from playback segments as the unique seconds of each episode a user covered, not the sum of every segment played, because rewinds, a second device and autoplay with nobody watching all add seconds nobody saw. Completion needs two rules side by side, reaching the credits and covering most of the runtime, and binge sessions should count only episodes the viewer was plausibly present for.
Commissioning decisions rest on these three numbers
A programming or content-strategy head deciding whether to renew a series, or how prominently to place it, asks whether people who started it actually watched it and kept going. Watch minutes, completion rate and binge share are the usual evidence, and all three inflate easily from the same raw logs. Here the data scientist's work is mostly measurement design: getting the unit of counting right before anyone builds a model on it.
What the player sends
Players emit heartbeats every few seconds, which the pipeline stitches into segments.
| Column | Meaning | Where it goes wrong |
|---|---|---|
| user, profile | account and the profile within it | a shared account is several people |
| device | tv, phone, web | two devices on one account at once |
| episode | content ID | trailers and recaps with their own IDs |
| wall_start | clock time the segment began | device clocks drift; prefer server time |
| from_pos, to_pos | position in the episode, seconds | seeks create jumps; rewinds create overlaps |
| input events | remote, touch or key presses | usually a separate stream; needed to detect unattended autoplay |
Three synthetic viewers, one weekend
Invented data for a three-episode series. U1 watches all three, rewinding five minutes in the third. U2 watches 15 minutes on a phone, skips to the end, and that evening plays the middle on a TV while the phone, left open, plays the same stretch. U3 watches two episodes and falls asleep as autoplay runs the third.
import pandas as pd
# Synthetic playback segments from a streaming app. Positions in seconds of the episode.
seg = pd.DataFrame([
# user, device, episode, wall-clock start, from_pos, to_pos
("U1", "tv", "S1E1", "2026-09-19 21:00:00", 0, 2640),
("U1", "tv", "S1E2", "2026-09-19 21:45:00", 0, 2700),
("U1", "tv", "S1E3", "2026-09-19 22:32:00", 0, 1200),
("U1", "tv", "S1E3", "2026-09-19 22:52:00", 900, 2580), # rewound 5 minutes
("U2", "phone", "S1E1", "2026-09-20 08:10:00", 0, 900),
("U2", "phone", "S1E1", "2026-09-20 08:25:00", 2400, 2640), # skipped to the end
("U2", "tv", "S1E1", "2026-09-20 20:00:00", 600, 1500), # same user, second device
("U2", "phone", "S1E1", "2026-09-20 20:00:00", 600, 1500), # phone left playing too
("U3", "tv", "S1E1", "2026-09-21 23:00:00", 0, 2640),
("U3", "tv", "S1E2", "2026-09-21 23:45:00", 0, 2700), # autoplay
("U3", "tv", "S1E3", "2026-09-22 00:31:00", 0, 2580), # autoplay, no input since 23:00
], columns=["user", "device", "episode", "wall_start", "from_pos", "to_pos"])
seg["wall_start"] = pd.to_datetime(seg["wall_start"])
duration = {"S1E1": 2640, "S1E2": 2700, "S1E3": 2580} # end credits start at 95%
last_input = {"U1": "2026-09-19 22:52:00", "U2": "2026-09-20 20:00:00", "U3": "2026-09-21 23:00:00"}
def unique_seconds(g):
"""Length of the union of [from_pos, to_pos) intervals."""
total, cur_s, cur_e = 0, None, None
for s, e in sorted(zip(g.from_pos, g.to_pos)):
if cur_e is None or s > cur_e:
total += 0 if cur_e is None else cur_e - cur_s
cur_s, cur_e = s, e
else:
cur_e = max(cur_e, e)
return total + (cur_e - cur_s)
seg["played"] = seg["to_pos"] - seg["from_pos"]
per = seg.groupby(["user", "episode"]).apply(
lambda g: pd.Series({
"raw_played_min": g.played.sum() / 60,
"unique_min": unique_seconds(g) / 60,
"max_pos_pct": 100 * g.to_pos.max() / duration[g.name[1]],
"coverage_pct": 100 * unique_seconds(g) / duration[g.name[1]],
}), include_groups=False).round(1)
per["complete_by_position"] = per.max_pos_pct >= 95
per["complete_by_coverage"] = per.coverage_pct >= 90
print(per.to_string())
print("watch time, raw (min):", round(per.raw_played_min.sum(), 1))
print("watch time, unique per user-episode (min):", round(per.unique_min.sum(), 1))
print("completion rate by position (%):", round(100 * per.complete_by_position.mean(), 1))
print("completion rate by coverage (%):", round(100 * per.complete_by_coverage.mean(), 1))
# Binge sessions: 3+ episodes with under 30 minutes between one episode's end and the next start
first = (seg.groupby(["user", "episode"]).agg(start=("wall_start", "min")).reset_index()
.merge(per.reset_index()[["user", "episode", "unique_min"]], on=["user", "episode"]))
first["end"] = first["start"] + pd.to_timedelta(first["unique_min"], unit="min")
first = first.sort_values(["user", "start"])
gap = first["start"] - first.groupby("user")["end"].shift()
first["session"] = (gap.isna() | (gap > pd.Timedelta(minutes=30))).groupby(first["user"]).cumsum()
first["idle_hours"] = (first["start"] - first["user"].map(lambda u: pd.Timestamp(last_input[u]))
).dt.total_seconds().clip(lower=0) / 3600
first["unattended"] = first["idle_hours"] > 1 # started over an hour after the last input
sessions = first.groupby(["user", "session"]).agg(
episodes=("episode", "count"), attended=("unattended", lambda x: int((~x).sum())))
sessions["binge_raw"] = sessions.episodes >= 3
sessions["binge_attended"] = sessions.attended >= 3
print(sessions.to_string())
print("unattended minutes:", round(first.loc[first.unattended, "unique_min"].sum(), 1)) raw_played_min unique_min max_pos_pct coverage_pct complete_by_position complete_by_coverage
user episode
U1 S1E1 44.0 44.0 100.0 100.0 True True
S1E2 45.0 45.0 100.0 100.0 True True
S1E3 48.0 43.0 100.0 100.0 True True
U2 S1E1 49.0 29.0 100.0 65.9 True False
U3 S1E1 44.0 44.0 100.0 100.0 True True
S1E2 45.0 45.0 100.0 100.0 True True
S1E3 43.0 43.0 100.0 100.0 True True
watch time, raw (min): 318.0
watch time, unique per user-episode (min): 293.0
completion rate by position (%): 100.0
completion rate by coverage (%): 85.7
episodes attended binge_raw binge_attended
user session
U1 1 3 3 True True
U2 1 1 1 False False
U3 1 3 2 True False
unattended minutes: 43.0Where the 318 minutes came from
The raw sum is 318 minutes. Merging overlapping positions per user and episode brings it to 293: five minutes from U1's rewind, and 20 from U2, whose middle stretch was played on two devices at once and counted twice. A further 43 minutes are U3's third episode, which autoplay started more than an hour after the last remote press. Take that out and attended watch time is 250 minutes, about a fifth below the raw figure. Which figure goes into a renewal deck matters.
The unattended flag here uses a crude rule, a play starting over an hour after the last input. Many apps show a "still watching?" prompt; if yours logs the prompt and the response, use that instead of a fixed threshold.
Two completion rates from the same plays
| Rule | Complete | Rate |
|---|---|---|
| Reached the credits (position at least 95% of runtime) | 7 of 7 | 100.0% |
| Covered at least 90% of runtime | 6 of 7 | 85.7% |
U2 skipped from minute 25 to minute 40 and then the end, so the position rule marks the episode complete; the coverage rule sees 65.9% and does not. Skipping is itself a signal worth reporting; a series where many viewers jump forward may have pacing problems, and the position rule hides it.
Binge counts shrink once attendance is checked
On episode count and gaps alone, U1 and U3 both binged. With unattended plays removed, only U1 did: U3 attended two episodes. At platform scale, late-night autoplay can make a slow series look like a binge hit, which is the opposite of the conclusion the content team wants to draw.
Checks before the numbers leave the team
- Profiles, not accounts. A family account with three profiles is three viewers; an account without profiles may still be several. Count at profile level where profiles exist, and say so.
- Credits position. "95% of runtime" is a stand-in. If the content metadata has a credits timestamp per episode, use it, since credits length varies a lot.
- Downloads. Offline viewing uploads its events later. A daily report run before sync undercounts mobile viewing; wait for the late-arrival window to close or mark the day provisional.
- Trailers and recaps. Auto-playing previews on the home screen generate playback events. Exclude non-episode content IDs before any series metric.
- Survivorship across episodes. Episode 3 completion among people who started episode 3 is not the same as the share of episode 1 starters who reached episode 3. Report the second as the series retention curve.
What the programming head should receive
A per-series card: attended watch minutes, coverage-based completion for episode one, the share of episode-one starters who started the final episode, and attended binge share. Put the definitions on the back of the card, one line each. When the raw and attended figures differ a lot, show both once, so nobody is surprised when the number falls after the method change.
Measurement choices like these are also what product-metrics interviews probe; see the DAU drop case. Event-level data work in pandas and the modelling that follows are core to the Data Science programme.
Frequently asked questions
How should completion rate be defined for a streaming episode?
Two common rules are reaching a position near the end, such as the start of the credits, and actually covering most of the runtime. The position rule counts a viewer who skipped straight to the end as complete. Report the coverage rule alongside it and state both thresholds.
What causes double counting in OTT watch time?
Rewinds, the same account playing on two devices at once, and autoplay continuing after the viewer has left. Summing raw playback segments counts all three. Merge overlapping position intervals per user and episode, and flag plays that began long after the last user input.
What counts as a binge-watching session?
There is no single standard. A workable rule is three or more episodes of the same series with short gaps between them, for example under 30 minutes. Whatever rule you choose, exclude episodes started by autoplay with nobody interacting, or binge rates will be inflated by viewers who have fallen asleep.
Pankit Kumar has 10 years in Data Science & AI, building and shipping production systems in regulated pharma and clinical environments. He is a freelance trainer at Boston Institute of Analytics, AnalytixLabs and Scaler, and has taught this material to thousands of working professionals.
This article is part of our Data Science programme — 6 months. From data foundations to machine learning, deep learning and deployment.
Explore Data Science