DEV Community

Daniel Pertu
Daniel Pertu

Posted on

Typical views is a median with five filters in front of it, and sometimes the answer is null

Nakodo finds creators on YouTube, Instagram and TikTok for a brand and runs the outreach. Everything about money in the app hangs off one number per creator: the views a post of theirs can expect. That number sets which size bracket they are asked as, and the fee they are offered. Our own guide on how much to pay YouTubers gives brands the manual version of the same advice, median views times a CPM, and the glossary entry for median views is the two-sentence version.

So the function that produces it is thirteen lines and gets more scrutiny than anything else in the codebase. Here it is in full.

const SETTLE_DAYS = 7;   // views on newer posts are still climbing
const MIN_VIEWS = 3;     // fewer posts with views than this say little
const RECENT_DAYS = 90;  // what a post gets now, not what one got two years ago
const SPARSE_DAYS = 365; // a creator who posts rarely: their latest from the past year
const SPARSE_POSTS = 6;
const HIT = 3;           // over this many times the others' median is a hit, not a forecast

export function typicalViews(all: Post[], now: Date): { views: number | null; posts: number } {
  const ageDays = (p: Post) => (now.getTime() - p.publishedAt.getTime()) / DAY_MS;
  const withViews = all.filter((p) => p.viewCount != null).sort((a, b) => b.publishedAt.getTime() - a.publishedAt.getTime());
  const settled = withViews.filter((p) => ageDays(p) >= SETTLE_DAYS);
  const pool = settled.length >= MIN_VIEWS ? settled : withViews;
  const recent = pool.filter((p) => ageDays(p) <= RECENT_DAYS);
  const used = recent.length >= MIN_VIEWS ? recent : pool.filter((p) => ageDays(p) <= SPARSE_DAYS).slice(0, SPARSE_POSTS);
  let views = used.map((p) => p.viewCount!);
  if (views.length > MIN_VIEWS) {
    const m = median(views)!;
    views = views.filter((v) => v <= m * HIT);
  }
  return { views: views.length >= MIN_VIEWS ? Math.round(median(views)!) : null, posts: views.length };
}
Enter fullscreen mode Exit fullscreen mode

Five decisions in there, and each one was a wrong fee first.

A post from Tuesday is not evidence

Views on a two-day-old video are still climbing, so including it drags the median towards zero. settled drops anything younger than a week.

The pool line is the fallback: if dropping them leaves fewer than three posts, we put them back. An account that posted four times this week has no settled posts at all, and a slightly pessimistic number beats no number, because no number means the creator gets no fee offer and has to be asked for their rate instead.

Ninety days, or a year for people who post rarely

A channel that got 400,000 views on a video in 2023 and 2,000 views now is a 2,000-view channel. RECENT_DAYS is the whole of that argument.

The second branch is for the many creators who are a good fit and post once a quarter. Fewer than three recent posts, and we take their latest from the past year, capped at six. Note where the cap sits: .slice(0, SPARSE_POSTS) works because withViews was sorted newest first at the top. That sort is load-bearing and it is twelve lines away from the thing that depends on it, which is the kind of distance that earns a test.

The hit filter takes the median twice

const m = median(views)!;
views = views.filter((v) => v <= m * HIT);
Enter fullscreen mode Exit fullscreen mode

One post doing three times the median is the single most expensive thing that can happen to a fee. The median is already robust against it in principle, but with four or five posts in the sample, one hit at 40 times the rest shifts which pair the median lands between.

So the median is computed, used only as a yardstick, posts above three times it are dropped, and the median is taken again on what is left. A channel whose last five videos got 1,000, 1,200, 900, 1,100 and 40,000 views comes out at 1,050, not at 1,100, and the sample it reports is four posts rather than five.

The guard is views.length > MIN_VIEWS, strictly greater. With exactly three posts there is no sensible yardstick to throw one of them away by, so nothing is dropped.

Null is a result, not a failure

return { views: views.length >= MIN_VIEWS ? Math.round(median(views)!) : null, posts: views.length };
Enter fullscreen mode Exit fullscreen mode

Two posts with views is not a sample. The function returns null, and null travels: the fee code has an explicit "unknown" note for it, and the creator is asked what they charge rather than offered a number we invented. A fee suggestion that is confidently wrong is worse for the brand than no suggestion, because it goes out in an email.

The dormant case is the one that surprised me. A creator whose newest post is 400 days old returns { views: null, posts: 0 }: settled has three posts, so it becomes the pool, nothing is inside 90 days, and the sparse branch filters on the same now, so the year window is empty too. Posts: 0 reads oddly next to a profile with four videos on it, but it is the honest answer. We know nothing about what they get today.

The second return value is the one people forget

posts is not telemetry. Downstream, a sample of fewer than six posts is multiplied by 0.85 before it becomes a fee:

const FEW_POSTS = 6;
const FEW_POSTS_SHARE = 0.85;
Enter fullscreen mode Exit fullscreen mode

Uncertainty that is quantified in one place and then discarded at the call site is a bug waiting to be written, so the only way to get the number is to get the count with it. Three posts and thirty posts do not produce the same offer.

Where you can see it

The same arithmetic, by hand and with no account, is on our engagement rate calculator: paste the views, likes and comments from three recent videos and it gives you each video's rate and the median across them, with the same "use videos that are a few days old" instruction that SETTLE_DAYS enforces in code. The data section of how it works says what we keep and for how long, which is why this function only ever sees recent posts in the first place.

The lesson I keep relearning from this file: a statistic that goes into an email to a stranger needs a way to say "I do not know", and the count that backs it has to travel in the same return value.

Top comments (0)