diff --git a/src/app/(main)/ad-revenue-report/page.tsx b/src/app/(main)/ad-revenue-report/page.tsx index e46de3c..e798ad6 100644 --- a/src/app/(main)/ad-revenue-report/page.tsx +++ b/src/app/(main)/ad-revenue-report/page.tsx @@ -30,7 +30,7 @@ import { } from '@ant-design/icons'; import dayjs, { type Dayjs } from 'dayjs'; import { api, errMsg } from '@/lib/api'; -import { formatUtcTime, percentile } from '@/lib/format'; +import { formatUtcTime, nearestRankPercentile } from '@/lib/format'; import type { AdRevenueDaily, AdRevenueHourly, @@ -758,9 +758,9 @@ export default function AdRevenueReportPage() { .filter((item) => item.feed_scene === sceneName) .map((item) => item.sub_count ?? 1); return { - p5: percentile(counts, 0.05, false), - p50: percentile(counts, 0.5, false), - p95: percentile(counts, 0.95, false), + p5: nearestRankPercentile(counts, 0.05), + p50: nearestRankPercentile(counts, 0.5), + p95: nearestRankPercentile(counts, 0.95), }; }; return { coupon: summarize('coupon'), comparison: summarize('comparison') }; @@ -1083,12 +1083,12 @@ export default function AdRevenueReportPage() { - - - - - - + + + + + + )} diff --git a/src/lib/format.ts b/src/lib/format.ts index a1d7af3..15dab59 100644 --- a/src/lib/format.ts +++ b/src/lib/format.ts @@ -37,6 +37,18 @@ export function percentile(values: readonly number[], q: number, round = true): return round ? Math.round(result) : result; } +/** + * 离散计数分位数(nearest-rank)。 + * 广告条数等不可拆分的计数不做线性插值,结果始终取自真实样本。 + */ +export function nearestRankPercentile(values: readonly number[], q: number): number | null { + if (!values.length) return null; + const sorted = [...values].sort((a, b) => a - b); + const quantile = Math.min(1, Math.max(0, q)); + const rank = Math.max(1, Math.ceil(quantile * sorted.length)); + return sorted[rank - 1]; +} + const hasTz = (v: string) => /(?:Z|[+-]\d{2}:?\d{2})$/i.test(v); /** 把 UTC 口径字符串解析为带时区的 dayjs(无时区后缀的补 Z 当 UTC)。 */