"Post on Thursdays." "Keep it under ten minutes." Advice like this rarely comes with numbers from your niche. You can check it with public data, but sorting videos by views and looking at what the top ones share gives the wrong answer. This post shows a fairer comparison: views per day, each video measured against its own channel, medians, and a count next to every number.

Why raw views mislead

Three things drive a video's view count more than its length or weekday:

  • Age. A video from last year has had twelve months to collect views; one from last week has had seven days. Sort by total views and old videos win.
  • Channel size. A channel with a million subscribers gets more views on its weakest upload than a small channel gets on its best. If the big channel happens to make long videos, "long videos win".
  • Outliers. One video that took off can carry a whole bucket's average. Medians are less easily moved.

There is also a quieter problem: you only see what was published and kept. Creators delete flops and re-upload hits. That is survivorship, and no script fixes it, so treat the result as a hint.

The method

  1. Pick channels, not a search. Choose four to eight channels in your niche that publish regularly. Normalising for channel size needs several videos per channel, and a keyword search returns one or two videos from many channels. Search results are also ranked toward videos that already did well, which is survivorship again.
  2. Take a year of uploads. Recent enough to reflect the niche now, long enough for a few dozen videos per channel.
  3. Drop the very young. Videos under 14 days old are still in their first push, so their views per day are inflated and unstable.
  4. Compute views per day since publish, then divide by the channel's median views per day. A value of 2.0 means "twice as many views per day as this channel's typical video". This is the rel column below.
  5. Bucket and take medians. Length into four buckets, weekday in your audience's time zone, and print n and the number of channels next to every median.

Views per day is not perfect. Most videos collect their views early, so a 30-day-old video averages more per day than the same video at 300 days. The script prints the median age of each bucket so you can see when one bucket is simply younger than the rest.

Collect the videos

Best Damn YouTube Scraper is an Apify Actor that reads public data through the official YouTube Data API v3. Given a channel, it reads the uploads playlist newest first. With publishedAfter set, it stops paging once it reaches older videos, so you are not billed for the back catalogue. Replace the handles with channels from your niche:

{ "startUrls": [ "https://www.youtube.com/@ChannelOne", "https://www.youtube.com/@ChannelTwo", "https://www.youtube.com/@ChannelThree", "https://www.youtube.com/@ChannelFour" ], "publishedAfter": "12 months", "maxResults": 0, "maxDescriptionLength": 50 } 

maxResults: 0 means every video inside the date window, and maxDescriptionLength trims descriptions you will not use. In the Console, paste the URLs, fill in Only videos published after and click Start. From the API, with the input saved as input.json:

curl -X POST "https://api.apify.com/v2/acts/josh99smith~youtube-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H "Content-Type: application/json" -d @input.json -o videos.json 

Each record has publishedAt, durationSeconds, viewCount, likeCount, commentCount, liveStatus, channelTitle and scrapedAt. The last one matters: age is measured from publish to the moment the record was read, not to when you run the script.

The script

Save it as buckets.py and run python buckets.py videos.json. Python 3.8 or later, no packages. Set UTC_OFFSET to your audience's time zone: publishedAt is in UTC, so a video posted at 9 pm on Monday in New York is a Tuesday upload in UTC.

import json import sys from collections import defaultdict from datetime import datetime, timedelta from statistics import median MIN_AGE, MAX_AGE = 14, 365 # days since publish to keep UTC_OFFSET = -5 # hours; weekday in your audience's time zone LENGTHS = [(180, "under 3 min"), (600, "3-10 min"), (1200, "10-20 min"), (10**9, "20+ min")] DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] def ts(s): return datetime.fromisoformat(s.replace("Z", "+00:00")) rows, dropped = [], defaultdict(int) for v in json.load(open(sys.argv[1], encoding="utf-8")): if not v["success"] or v.get("type") != "video": continue if v["liveStatus"] != "none" or not v["durationSeconds"] or v["viewCount"] is None: dropped["live, upcoming or hidden views"] += 1 continue published = ts(v["publishedAt"]) age = (ts(v["scrapedAt"]) - published).total_seconds() / 86400 if not MIN_AGE <= age <= MAX_AGE: dropped[f"outside {MIN_AGE}-{MAX_AGE} days old"] += 1 continue rows.append({ "channel": v["channelTitle"], "age": age, "vpd": v["viewCount"] / age, "length": next(name for cap, name in LENGTHS if v["durationSeconds"] <= cap), "day": DAYS[(published + timedelta(hours=UTC_OFFSET)).weekday()], }) # Channel size: compare each video with its own channel's median views per day. by_channel = defaultdict(list) for r in rows: by_channel[r["channel"]].append(r["vpd"]) for r in rows: r["rel"] = r["vpd"] / median(by_channel[r["channel"]]) print(f"{len(rows)} videos kept from {len(by_channel)} channels;", ", ".join(f"{n} dropped ({why})" for why, n in dropped.items()) or "none dropped") for ch, vals in sorted(by_channel.items()): print(f" {ch}: {len(vals)} videos, median {median(vals):,.0f} views/day") def table(key, order): print(f"\nBy {key} n channels median rel median views/day median age") for b in order: g = [r for r in rows if r[key] == b] if not g: continue flag = "" if len(g) >= 10 else " (n < 10, ignore)" print(f" {b:12} {len(g):3} {len({r['channel'] for r in g}):8} {median(r['rel'] for r in g):10.2f}" f" {median(r['vpd'] for r in g):16,.0f} {median(r['age'] for r in g):8.0f}d{flag}") table("length", [name for _, name in LENGTHS]) table("day", DAYS) 

Live streams (liveStatus other than none) are dropped because they collect views differently. The Actor does not label Shorts, so "under 3 min" stands in for them.

How to read the result

Here is the length table from a test run on three home-coffee channels, 80 latest uploads each:

97 videos kept from 2 channels; 138 dropped (outside 14-365 days old), 5 dropped (live, upcoming or hidden views) By length n channels median rel median views/day median age under 3 min 11 2 3.07 1,809 66d 3-10 min 4 2 1.24 4,868 106d (n < 10, ignore) 10-20 min 29 2 0.66 441 221d 20+ min 53 2 1.05 942 172d 

It is tempting to read "short videos get three times the views". Look at the other columns first:

  • Median age. The short videos are 66 days old; the 10-20 minute ones are 221. Part of that 3.07 is youth. Narrow MAX_AGE to 120 and see whether the gap survives.
  • Channels. One of the three channels had not posted inside the window and dropped out, so this is two channels. One contributed 78 of the 97 videos, so the pooled medians mostly describe that channel. Run the script per channel too, and only trust a pattern that shows up in more than one.
  • n. Four videos in a bucket is an anecdote. The script flags anything under 10.

The weekday table from the same run had three of seven days flagged, and the rest sat between 0.86 and 1.34 with 16 to 19 videos each. Differences that small, on samples that small, are noise. That is a real result: for this niche, upload day is not where the views are. Spread across seven days, even a few hundred videos leaves thin buckets.

Whatever you find, it is correlation. Creators put their biggest ideas into their longest videos and post them on the days they think are best, so a length or weekday bucket also carries topic and effort. If a pattern holds across channels and age windows, treat it as something to test on your own uploads.

Cost and limits

  • Price. $0.001 per delivered video. The test run above returned 240 videos for $0.24. Six channels posting twice a week for a year is about 6 x 104 = 624 videos, so $0.62; a weekly channel adds about $0.05. Private, deleted and unavailable videos, and quota errors, are not charged, and a run stops at the maximum cost you set.
  • Quota. Reading uploads costs about 1 quota unit per 50 videos plus the same again for statistics, so the 240-video test used 14 units. The Actor's built-in key has a daily quota shared by every user. You can paste your own free key from the Google Cloud Console as apiKey to get 10,000 units a day to yourself. Search is the expensive part, 100 units per page, which is another reason to start from channels.
  • One snapshot. View counts keep changing, and YouTube can recount views. Note the run date with the result.
  • Public data only. No login, nothing private or members-only.

If this saves you time, a review on the Store page helps other people find it, and a star on GitHub helps too.

Disclosure: I built this Actor. Source: github.com/josh99smith/youtube-scraper. YouTube is a trademark of Google; the Actor is not affiliated with Google.