feat: 增加不同地区各渠道收入占比图

This commit is contained in:
R524809
2026-09-08 16:53:44 +08:00
parent 44c6437b64
commit bab02e1efd
25 changed files with 888 additions and 29 deletions
+78 -1
View File
@@ -714,10 +714,24 @@ const CHANNEL_NAME_TO_KEY = {
线上渠道: 'online',
批发及其他: 'wholesale',
};
const ONLINE_SUB_NAME_TO_KEY = {
泡泡玛特抽盒机: 'roboshop',
天猫旗舰店: 'tmall',
抖音平台: 'douyin',
自研APP及官方网站: 'own_app',
Shopee: 'shopee',
TikTok: 'tiktok',
亚马逊: 'amazon',
其他线上渠道: 'online_other',
};
const channelRevenueByPeriod = new Map();
readCsvRows(RAWDATA_DIR, '地区渠道收入.csv').forEach(({ region, channel, period, revenue }) => {
const key = CHANNEL_NAME_TO_KEY[channel];
if (!key) throw new Error(`地区渠道收入底表存在未知渠道:${channel}`);
if (!key) {
// 线上细分行只进分地区图;未知渠道仍视为底表错误。
if (!ONLINE_SUB_NAME_TO_KEY[channel]) throw new Error(`地区渠道收入底表存在未知渠道:${channel}`);
return;
}
if (!period || !Number.isFinite(revenue)) throw new Error(`地区渠道收入底表行不完整:${region} ${channel} ${period} ${revenue}`);
if (!channelRevenueByPeriod.has(period)) {
channelRevenueByPeriod.set(period, { retail: 0, online: 0, robot_store: 0, wholesale: 0 });
@@ -745,3 +759,66 @@ writeRuntimeViews('ppmt_channel_share', ['period', 'retail', 'online', 'robot_st
});
console.log('已生成渠道收入占比的全部、年度、半年 CSV。');
// 分地区渠道收入占比:线上渠道按披露细分拆开,各地区细分不同。
// 底表中「线上渠道」合计行仅作核对值;细分空缺期已按 0 录入并核对
// (美洲区 2026H1 起 TikTok 未再产生收入、亚马逊 2026 年起开售,与线上合计勾稽一致)。
const REGION_KEY_BY_NAME = { 中国: 'china', 亚太: 'apac', 美洲: 'americas', 欧洲及其他: 'europe' };
const REGION_SERIES = {
china: ['retail', 'robot_store', 'roboshop', 'tmall', 'douyin', 'online_other', 'wholesale'],
apac: ['retail', 'robot_store', 'own_app', 'shopee', 'online_other', 'wholesale'],
americas: ['retail', 'robot_store', 'own_app', 'tiktok', 'amazon', 'online_other', 'wholesale'],
europe: ['retail', 'robot_store', 'own_app', 'online_other', 'wholesale'],
};
const TOP_AND_SUB_NAME_TO_KEY = { ...CHANNEL_NAME_TO_KEY, ...ONLINE_SUB_NAME_TO_KEY };
const regionAmounts = new Map();
const regionOnlineDisclosed = new Map();
readCsvRows(RAWDATA_DIR, '地区渠道收入.csv').forEach(({ region, channel, period, revenue }) => {
const regionKey = REGION_KEY_BY_NAME[region];
if (!regionKey) throw new Error(`地区渠道收入底表存在未知地区:${region}`);
if (channel === '线上渠道') {
regionOnlineDisclosed.set(`${regionKey}:${period}`, revenue);
return;
}
const key = TOP_AND_SUB_NAME_TO_KEY[channel];
if (!key) throw new Error(`地区渠道收入底表存在未知渠道:${channel}`);
if (!period || !Number.isFinite(revenue)) throw new Error(`地区渠道收入底表行不完整:${region} ${channel} ${period} ${revenue}`);
const bucketKey = `${regionKey}:${period}`;
if (!regionAmounts.has(bucketKey)) regionAmounts.set(bucketKey, {});
const bucket = regionAmounts.get(bucketKey);
bucket[key] = (bucket[key] ?? 0) + revenue;
});
regionOnlineDisclosed.forEach((disclosed, bucketKey) => {
const bucket = regionAmounts.get(bucketKey);
if (!bucket) throw new Error(`地区渠道收入底表缺少线上合计对应的期间:${bucketKey}`);
const subSum = Object.values(ONLINE_SUB_NAME_TO_KEY).reduce((sum, key) => sum + (bucket[key] ?? 0), 0);
if (Math.abs(subSum - disclosed) > 0.05) {
throw new Error(`${bucketKey} 线上细分合计 ${subSum.toFixed(2)} ≠ 披露线上渠道 ${disclosed.toFixed(2)}`);
}
});
Object.entries(REGION_SERIES).forEach(([regionKey, seriesKeys]) => {
const amountViews = buildViews((year, type) => {
const bucket = regionAmounts.get(`${regionKey}:${type === 'H1' ? `${year}H1` : `${year}FY`}`);
if (!bucket) return null;
const values = Object.fromEntries(seriesKeys.map((key) => [key, bucket[key] ?? 0]));
return isComplete(values) ? values : null;
});
const shareViews = Object.fromEntries(Object.entries(amountViews).map(([view, rows]) => [
view,
rows.map(({ period, ...amounts }) => {
const total = Object.values(amounts).reduce((sum, value) => sum + value, 0);
return {
period,
...Object.fromEntries(Object.entries(amounts).map(([key, value]) => [key, value / total * 100])),
total_revenue: total,
};
}),
]));
writeRuntimeViews(`ppmt_channel_share_${regionKey}`, ['period', ...seriesKeys, 'total_revenue'], shareViews, {
...Object.fromEntries(seriesKeys.map((key) => [key, 1])),
total_revenue: 2,
});
});
console.log('已生成四地区渠道收入占比(线上细分)的全部、年度、半年 CSV。');