修复:统一领券与比价广告数百分位口径 (#92)
## 改动说明 - 将单次领券、比价广告数从线性插值百分位改为 nearest-rank 离散百分位 - 百分位结果始终取自真实观测样本,不再出现 13.5 条这类小数广告数 - 指标展示改为整数 ## 线上数据复算 2026-07-24 正式业务口径: - 领券 P5/P50/P95:`1/7/13.5` 修正为 `1/7/15` - 比价 P5/P50/P95:`1/2/8.3` 修正为 `1/2/9` ## 验证 - 线上数据完整分组复算通过 - 空样本、单样本边界校验通过 - `npm run build` 通过(含 TypeScript 类型检查) --------- Co-authored-by: guke <guke@wonderable.ai> Co-authored-by: linkeyu <798648091@qq.com> Reviewed-on: #92 Co-authored-by: linkeyu <linkeyu@wonderable.ai> Co-committed-by: linkeyu <linkeyu@wonderable.ai>
This commit was merged in pull request #92.
This commit is contained in:
@@ -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() {
|
||||
</Typography.Text>
|
||||
</Divider>
|
||||
<Row gutter={[16, 12]}>
|
||||
<Col span={4}><Statistic title="单次领券广告数 P5" value={sessionAdCounts.coupon.p5 ?? '-'} precision={1} /></Col>
|
||||
<Col span={4}><Statistic title="单次领券广告数 P50" value={sessionAdCounts.coupon.p50 ?? '-'} precision={1} /></Col>
|
||||
<Col span={4}><Statistic title="单次领券广告数 P95" value={sessionAdCounts.coupon.p95 ?? '-'} precision={1} /></Col>
|
||||
<Col span={4}><Statistic title="单次比价广告数 P5" value={sessionAdCounts.comparison.p5 ?? '-'} precision={1} /></Col>
|
||||
<Col span={4}><Statistic title="单次比价广告数 P50" value={sessionAdCounts.comparison.p50 ?? '-'} precision={1} /></Col>
|
||||
<Col span={4}><Statistic title="单次比价广告数 P95" value={sessionAdCounts.comparison.p95 ?? '-'} precision={1} /></Col>
|
||||
<Col span={4}><Statistic title="单次领券广告数 P5" value={sessionAdCounts.coupon.p5 ?? '-'} precision={0} /></Col>
|
||||
<Col span={4}><Statistic title="单次领券广告数 P50" value={sessionAdCounts.coupon.p50 ?? '-'} precision={0} /></Col>
|
||||
<Col span={4}><Statistic title="单次领券广告数 P95" value={sessionAdCounts.coupon.p95 ?? '-'} precision={0} /></Col>
|
||||
<Col span={4}><Statistic title="单次比价广告数 P5" value={sessionAdCounts.comparison.p5 ?? '-'} precision={0} /></Col>
|
||||
<Col span={4}><Statistic title="单次比价广告数 P50" value={sessionAdCounts.comparison.p50 ?? '-'} precision={0} /></Col>
|
||||
<Col span={4}><Statistic title="单次比价广告数 P95" value={sessionAdCounts.comparison.p95 ?? '-'} precision={0} /></Col>
|
||||
</Row>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
@@ -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)。 */
|
||||
|
||||
Reference in New Issue
Block a user