Docs/Skills and code/src/
src/analysis/within-channel-statistics.js
const DAY_MS = 86_400_000;
export const SAMPLE_BANDS = Object.freeze([
{ min: 0, max: 4, label: "insufficient_evidence" },
{ min: 5, max: 9, label: "preliminary_distribution" },
{ min: 10, max: 19, label: "initial_outlier_analysis" },
{ min: 20, max: 49, label: "distribution_analysis" },
{ min: 50, max: Infinity, label: "subgroup_analysis_possible" }
]);
export function isFiniteNumber(value) {
return value !== null && value !== undefined && value !== "" && Number.isFinite(Number(value));
}
export function numberOrNull(value) {
return isFiniteNumber(value) ? Number(value) : null;
}
export function round(value, digits = 6) {
return 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 quantile(values, p) {
const sorted = values.filter(Number.isFinite).sort((a, b) => a - b);
if (!sorted.length) return null;
const position = Math.max(0, Math.min(1, p)) * (sorted.length - 1);
const lower = Math.floor(position);
const upper = Math.ceil(position);
if (lower === upper) return sorted[lower];
return sorted[lower] + (sorted[upper] - sorted[lower]) * (position - lower);
}
export function medianAbsoluteDeviation(values) {
const center = median(values);
return center == null ? null : median(values.filter(Number.isFinite).map(value => Math.abs(value - center)));
}
export function robustZ(value, values) {
if (!Number.isFinite(value) || values.length < 2) return { score: null, mad: null, center: median(values) };
const center = median(values);
const mad = medianAbsoluteDeviation(values);
if (center == null || mad == null || mad <= 0) return { score: null, mad, center };
return { score: 0.67448975 * (value - center) / mad, mad, center };
}
export function sampleAssessment(n) {
return SAMPLE_BANDS.find(band => n >= band.min && n <= band.max)?.label ?? "insufficient_evidence";
}
export function reliablePublicationDate(video) {
const precision = String(video.published_at_precision ?? "").toLowerCase();
const method = String(video.published_at_normalization_method ?? "").toLowerCase();
if (!(precision === "exact" || precision === "date" || precision.startsWith("exact_"))) return null;
if (method.includes("relative") || method.includes("unrecognized")) return null;
const value = video.published_at_normalized || video.published_at;
if (!value) return null;
const time = Date.parse(value);
return Number.isFinite(time) ? time : null;
}
export function ageCohort(ageDays) {
if (!Number.isFinite(ageDays) || ageDays < 0) return "unknown";
if (ageDays <= 7) return "0_7d";
if (ageDays <= 30) return "8_30d";
if (ageDays <= 90) return "31_90d";
if (ageDays <= 365) return "91_365d";
if (ageDays <= 1095) return "1_3y";
return "3y_plus";
}
export function outlierLabel(z, ratio, sampleSize, mad) {
if (sampleSize < 5) return "insufficient_sample";
if (z != null) {
if (z >= 3.5) return "strong_outlier";
if (z >= 2.5) return "potential_outlier";
return "not_outlier";
}
// A zero-MAD distribution cannot support a robust z-score. Use an explicit,
// conservative ratio fallback and record the fallback method for auditability.
if (mad === 0 && ratio != null) {
if (ratio >= 5) return "strong_outlier_ratio_fallback";
if (ratio >= 3) return "potential_outlier_ratio_fallback";
return "not_outlier_ratio_fallback";
}
return "unavailable";
}
export function viewRatio(value, baseline) {
return baseline != null && baseline > 0 ? value / baseline : null;
}
export function concentrationShare(sortedDesc, topN, total) {
if (!(total > 0) || !sortedDesc.length) return null;
return sortedDesc.slice(0, topN).reduce((sum, video) => sum + video.numeric_views, 0) / total;
}
function getAgeDays(video, nowMs) {
const publishedMs = reliablePublicationDate(video);
if (publishedMs == null) return null;
const observedMs = Date.parse(video.view_observed_at || "");
const referenceMs = Number.isFinite(observedMs) ? observedMs : nowMs;
const age = (referenceMs - publishedMs) / DAY_MS;
if (!Number.isFinite(age) || age < -1) return null;
return Math.max(1, age);
}
export function prepareVideos(videos, nowMs) {
const uniqueVideos = [...new Map(videos.filter(video => video?.video_id).map(video => [video.video_id, video])).values()];
return uniqueVideos.map(video => {
const views = numberOrNull(video.view_count);
const ageDays = getAgeDays(video, nowMs);
const viewsPerDay = views != null && ageDays != null ? views / ageDays : null;
const cohort = ageDays == null ? "unknown" : ageCohort(ageDays);
return {
...video,
numeric_views: views != null && views >= 0 ? views : null,
age_days: ageDays,
age_cohort: cohort,
views_per_day: views != null && views >= 0 && ageDays != null ? viewsPerDay : null,
log_views: views != null && views >= 0 ? Math.log1p(views) : null,
log_views_per_day: views != null && views >= 0 && ageDays != null ? Math.log1p(viewsPerDay) : null,
date_reliable: ageDays != null
};
});
}
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.