feat: 增加不同地区各渠道收入占比图
This commit is contained in:
@@ -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。');
|
||||
|
||||
Reference in New Issue
Block a user