feat: add Detail Level BSR metric to experiments analysis

This commit is contained in:
Christian Vidal Wolf
2026-03-10 13:58:44 +01:00
parent 2363982144
commit c15642d62d
7 changed files with 70 additions and 24 deletions
+18 -1
View File
@@ -783,7 +783,8 @@ export const mergeSalesAndAdsData = (
adsData: AdsRecord[],
asinMetadataMap?: Map<string, { sku: string; title: string; line: string }>,
trafficData?: TrafficRecord[],
velocityMap?: Map<string, number>
velocityMap?: Map<string, number>,
bsrData?: BSRRecord[]
): CombinedKPIs[] => {
const stringCache: Record<string, string> = {};
const getNorm = (s: string) => {
@@ -808,6 +809,20 @@ export const mergeSalesAndAdsData = (
}
}
// Build BSR lookup map
const bsrMap = new Map<string, number>();
if (bsrData) {
for (let i = 0; i < bsrData.length; i++) {
const b = bsrData[i];
const year = b.date ? parseInt(b.date.split('-')[0]) : new Date().getFullYear();
const country = mapCountryToMarketplace(b.market);
const key = `${getNorm(b.asin)}|${getNorm(country)}|${year}|${b.week}`;
if (b.detailLevelBSR !== null && b.detailLevelBSR !== undefined) {
bsrMap.set(key, b.detailLevelBSR);
}
}
}
// 1. Initialize metadata lookup map with provided global map if available, otherwise build from current sales
const asinMetadata = asinMetadataMap || new Map<string, { sku: string; title: string; line: string }>();
@@ -942,6 +957,7 @@ export const mergeSalesAndAdsData = (
cpc,
cvrUnits,
glanceViews: trafficMap.get(key) || 0,
detailLevelBSR: bsrMap.get(key),
avgWeeklySales
});
});
@@ -983,6 +999,7 @@ export const mergeSalesAndAdsData = (
cpc: ad.clicks > 0 ? ad.cost / ad.clicks : 0,
cvrUnits: ad.clicks > 0 ? (ad.attributedUnits30d / ad.clicks) * 100 : 0,
glanceViews: trafficMap.get(key) || 0,
detailLevelBSR: bsrMap.get(key),
avgWeeklySales
});
}
+19 -2
View File
@@ -35,6 +35,8 @@ interface WeeklyMetrics {
cost: number;
clicks: number;
impressions: number;
detail_bsr: number;
bsrCount: number;
}
interface ComputedWeeklyMetrics extends WeeklyMetrics {
@@ -159,6 +161,8 @@ function aggregateWeeklyMetrics(
cost: 0,
clicks: 0,
impressions: 0,
detail_bsr: 0,
bsrCount: 0,
});
}
@@ -170,6 +174,10 @@ function aggregateWeeklyMetrics(
w.cost += Number(r.cost) || 0;
w.clicks += r.clicks || 0;
w.impressions += r.impressions || 0;
if (r.detailLevelBSR != null) {
w.detail_bsr += r.detailLevelBSR;
w.bsrCount += 1;
}
}
// Debug ad map
@@ -189,6 +197,7 @@ function aggregateWeeklyMetrics(
cvr: w.sessions > 0 ? (w.units / w.sessions) * 100 : 0,
ctr: w.impressions > 0 ? (w.clicks / w.impressions) * 100 : 0,
roas: w.cost > 0 ? w.adRevenue / w.cost : 0,
detail_bsr: w.bsrCount > 0 ? w.detail_bsr / w.bsrCount : 0,
}));
}
@@ -201,6 +210,7 @@ function getMetricValue(w: ComputedWeeklyMetrics, metric: string): number {
case 'roas': return w.roas;
case 'revenue': return w.revenue;
case 'acos': return w.cost > 0 && w.adRevenue > 0 ? (w.cost / w.adRevenue) * 100 : 0;
case 'detail_bsr': return w.detail_bsr;
default: return w.units;
}
}
@@ -265,6 +275,13 @@ function avgMetric(data: ComputedWeeklyMetrics[], metric: string, durationWeeks:
const totalCost = data.reduce((s, w) => s + w.cost, 0);
return totalAdRevenue > 0 && totalCost > 0 ? (totalCost / totalAdRevenue) * 100 : 0;
}
if (metric === 'detail_bsr') {
// For detail_bsr, we want the average of the non-zero weeks
const validWeeks = data.filter(w => w.detail_bsr > 0);
if (validWeeks.length === 0) return 0;
const sum = validWeeks.reduce((s, w) => s + w.detail_bsr, 0);
return sum / validWeeks.length;
}
// For absolute quantities (units, revenue, sessions), we sum them and divide by the number of weeks
// Use actual data.length instead of theoretical durationWeeks for accuracy
@@ -272,8 +289,8 @@ function avgMetric(data: ComputedWeeklyMetrics[], metric: string, durationWeeks:
return sum / data.length;
}
const METRICS = ['units', 'sessions', 'cvr', 'ctr', 'roas', 'revenue', 'acos'];
const LOWER_IS_BETTER = new Set(['acos']);
const METRICS = ['units', 'sessions', 'cvr', 'ctr', 'roas', 'revenue', 'acos', 'detail_bsr'];
const LOWER_IS_BETTER = new Set(['acos', 'detail_bsr']);
function computeMetricDiD(
treatmentBefore: ComputedWeeklyMetrics[],
+3
View File
@@ -278,6 +278,7 @@ export const METRIC_LABELS: Record<string, string> = {
roas: 'ROAS',
revenue: 'Revenue',
acos: 'ACOS',
detail_bsr: 'Detail Level BSR',
};
export const formatMetricValue = (value: number, metric: string): string => {
@@ -290,6 +291,8 @@ export const formatMetricValue = (value: number, metric: string): string => {
return `${value.toFixed(2)}x`;
case 'revenue':
return `${value.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`;
case 'detail_bsr':
return `#${value.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`;
default:
return value.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 2 });
}