Docs/Skills and code/src/

src/analysis/channel-metric-features.js

Code152 lines7.4 KBsrc/

const finite = value => value !== null && value !== undefined && value !== "" && Number.isFinite(Number(value));
const numberOrNull = value => finite(value) ? Number(value) : null;
const safeRatio = (a, b) => finite(a) && finite(b) && Number(b) > 0 ? Number(a) / Number(b) : null;
const round = (value, digits = 3) => value == null || !Number.isFinite(value) ? null : Number(value.toFixed(digits));

export function median(values) {
  const sorted = values.filter(Number.isFinite).sort((a, b) => a - b);
  if (!sorted.length) return null;
  const middle = Math.floor(sorted.length / 2);
  return sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
}

export function percentile(values, p) {
  const sorted = values.filter(Number.isFinite).sort((a, b) => a - b);
  if (!sorted.length) return null;
  const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(p * sorted.length) - 1));
  return sorted[index];
}

export function subscriberBand(value) {
  if (!finite(value)) return "unknown";
  const n = Number(value);
  if (n < 1_000) return "under_1k";
  if (n < 10_000) return "1k_9k";
  if (n < 100_000) return "10k_99k";
  if (n < 1_000_000) return "100k_999k";
  return "1m_plus";
}

export function catalogueBand(value) {
  if (!finite(value)) return "unknown";
  const n = Number(value);
  if (n < 10) return "under_10";
  if (n < 50) return "10_49";
  if (n < 200) return "50_199";
  if (n < 1_000) return "200_999";
  return "1000_plus";
}

export function ageBand(days) {
  if (!finite(days)) return "unknown";
  const n = Number(days);
  if (n < 365) return "under_1y";
  if (n < 365 * 3) return "1_3y";
  if (n < 365 * 7) return "3_7y";
  return "7y_plus";
}

export function evidenceBand(count) {
  if (!finite(count)) return "unknown";
  const n = Number(count);
  if (n < 5) return "under_5";
  if (n < 10) return "5_9";
  if (n < 20) return "10_19";
  return "20_plus";
}

function validDate(value) {
  if (!value) return null;
  const ms = Date.parse(value);
  return Number.isFinite(ms) ? ms : null;
}

function exactDateVideo(video) {
  const precision = String(video.published_at_precision ?? "").toLowerCase();
  if (!(precision.startsWith("exact_") || precision === "exact" || precision === "date")) return null;
  const date = validDate(video.published_at_normalized) ?? validDate(video.published_at);
  return date;
}

export function calculateChannelMetrics(channel, videos, options = {}) {
  const nowMs = options.nowMs ?? Date.now();
  const linked = [...new Map((videos ?? []).filter(v => v?.video_id).map(v => [v.video_id, v])).values()];
  const withViews = linked.filter(v => finite(v.view_count)).map(v => ({ ...v, numeric_views: Number(v.view_count) }));
  const viewValues = withViews.map(v => v.numeric_views);
  const sampleViewsTotal = viewValues.reduce((sum, value) => sum + value, 0);
  const sortedViews = [...viewValues].sort((a, b) => b - a);
  const topDecileCount = sortedViews.length ? Math.max(1, Math.ceil(sortedViews.length * 0.1)) : 0;
  const topDecileViews = sortedViews.slice(0, topDecileCount).reduce((sum, value) => sum + value, 0);
  const exactAgeAdjusted = withViews.map(video => {
    const publishedMs = exactDateVideo(video);
    if (publishedMs == null) return null;
    const ageDays = Math.max(1, (nowMs - publishedMs) / 86_400_000);
    return video.numeric_views / ageDays;
  }).filter(Number.isFinite);

  const joinedMs = validDate(channel.joined_date);
  const channelAgeDays = joinedMs == null ? null : Math.max(0, (nowMs - joinedMs) / 86_400_000);
  const subscriberCount = numberOrNull(channel.subscriber_count);
  const declaredVideoCount = numberOrNull(channel.video_count);
  const totalViewCount = numberOrNull(channel.total_view_count);
  const subsPerUpload = safeRatio(subscriberCount, declaredVideoCount);
  const viewsPerUpload = safeRatio(totalViewCount, declaredVideoCount);
  const viewsPerSubscriber = safeRatio(totalViewCount, subscriberCount);
  const uploadsPerYear = channelAgeDays != null && channelAgeDays >= 30 ? safeRatio(declaredVideoCount, channelAgeDays / 365.2425) : null;
  const corpusCoverage = safeRatio(linked.length, declaredVideoCount);
  const metadataFields = [channel.channel_id, channel.description, subscriberCount, declaredVideoCount, totalViewCount, channel.joined_date, channel.country, channel.handle, channel.thumbnail_url];
  const metadataCompleteness = metadataFields.filter(value => value !== null && value !== undefined && value !== "").length / metadataFields.length;
  const warnings = [];
  if (channel.subscriber_count_precision === "abbreviated") warnings.push("subscriber_count_is_abbreviated_approximation");
  if (channel.video_count_precision !== "exact_displayed") warnings.push("declared_video_count_not_exact_displayed");
  if (channel.total_view_count_precision !== "exact_displayed") warnings.push("total_view_count_not_exact_displayed");
  if (linked.length < 10) warnings.push("small_linked_video_sample");
  if (declaredVideoCount != null && linked.length > declaredVideoCount) warnings.push("linked_corpus_exceeds_declared_catalogue");
  if (channelAgeDays == null) warnings.push("channel_join_date_missing_or_unparseable");
  if (!withViews.length) warnings.push("no_linked_videos_with_view_counts");
  if (exactAgeAdjusted.length < 5) warnings.push("few_exact_dated_videos_for_age_adjusted_performance");

  const evidenceCount = linked.length;
  return {
    channel_url: channel.channel_url,
    channel_name: channel.name,
    ecosystem_classification: channel.ecosystem_classification ?? "unknown",
    subscriber_count: subscriberCount,
    subscriber_count_precision: channel.subscriber_count_precision ?? "unknown",
    subscriber_band: subscriberBand(subscriberCount),
    declared_video_count: declaredVideoCount,
    declared_video_count_precision: channel.video_count_precision ?? "unknown",
    catalogue_band: catalogueBand(declaredVideoCount),
    total_view_count: totalViewCount,
    total_view_count_precision: channel.total_view_count_precision ?? "unknown",
    joined_date: channel.joined_date ?? null,
    channel_age_days: round(channelAgeDays, 1),
    channel_age_band: ageBand(channelAgeDays),
    subscribers_per_upload: round(subsPerUpload, 2),
    lifetime_views_per_upload: round(viewsPerUpload, 2),
    lifetime_views_per_subscriber: round(viewsPerSubscriber, 3),
    declared_uploads_per_year_since_join: round(uploadsPerYear, 2),
    linked_video_count: linked.length,
    corpus_coverage_ratio: round(corpusCoverage, 4),
    corpus_coverage_percent: round(corpusCoverage == null ? null : corpusCoverage * 100, 1),
    evidence_band: evidenceBand(evidenceCount),
    video_count_with_views: withViews.length,
    performance_sample_size: withViews.length,
    sampled_view_total: sampleViewsTotal,
    video_median_views: round(median(viewValues), 1),
    video_p90_views: round(percentile(viewValues, 0.9), 1),
    top_decile_video_count: topDecileCount,
    top_decile_view_share: round(sampleViewsTotal > 0 ? topDecileViews / sampleViewsTotal : null, 4),
    exact_dated_video_count_for_age_adjusted_views: exactAgeAdjusted.length,
    median_exact_dated_views_per_day: round(median(exactAgeAdjusted), 2),
    metadata_completeness_percent: round(metadataCompleteness * 100, 1),
    warnings,
    metrics_version: "channel-metrics-v1"
  };
}

export function csvCell(value) {
  const text = Array.isArray(value) || (value && typeof value === "object") ? JSON.stringify(value) : value == null ? "" : String(value);
  return /[",\r\n]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
}

Shown as it is in the repository at build time (2026-11-03); the home directory appears as <home>. If this page and the file differ, the file is right. Back to the catalogue.