From 4de9fb5b89dc4346755a3996e14f4ada5b4fefcd Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Wed, 25 Feb 2026 08:39:13 +0100 Subject: [PATCH] fix(experiments): include overlapping weeks in date range logic and use exact ISO weeks --- services/experimentAnalysis.ts | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/services/experimentAnalysis.ts b/services/experimentAnalysis.ts index e80bd55..651d538 100644 --- a/services/experimentAnalysis.ts +++ b/services/experimentAnalysis.ts @@ -42,6 +42,13 @@ interface ComputedWeeklyMetrics extends WeeklyMetrics { roas: number; } +function getISOWeekStart(year: number, week: number): number { + const jan4 = new Date(year, 0, 4); + const day = jan4.getDay() || 7; + const startYear = new Date(year, 0, 4 - day + 1); + return startYear.getTime() + (week - 1) * 7 * 86400000; +} + function aggregateWeeklyMetrics( asinSet: Set, salesData: CombinedKPIs[], @@ -61,10 +68,9 @@ function aggregateWeeklyMetrics( const key = `${year}-W${String(weekNum).padStart(2, '0')}`; if (!weeklyMap.has(key)) { - const d = new Date(year, 0, 1 + (weekNum - 1) * 7); weeklyMap.set(key, { week: key, - timestamp: d.getTime(), + timestamp: getISOWeekStart(year, weekNum), units: 0, revenue: 0, sessions: 0, @@ -125,10 +131,20 @@ function splitPeriods( const after: ComputedWeeklyMetrics[] = []; for (const w of data) { - if (w.timestamp >= beforeStartTs && w.timestamp < beforeEndTs) { - before.push(w); - } else if (w.timestamp >= startTs && w.timestamp <= endTs) { + const weekStartTs = w.timestamp; + const weekEndTs = w.timestamp + 6 * 86400000 + 86399999; // End of the 7th day + + // A week overlaps the treatment period if it starts before the period ends, + // AND ends after the period starts. + const overlapsAfter = weekEndTs >= startTs && weekStartTs <= endTs; + const overlapsBefore = weekEndTs >= beforeStartTs && weekStartTs < beforeEndTs; + + // We prioritize assigning to 'after' so that we capture all units happening + // closely around the experiment dates for accurate reporting. + if (overlapsAfter) { after.push(w); + } else if (overlapsBefore) { + before.push(w); } } @@ -322,12 +338,13 @@ export function buildCounterfactualSeries( if (!hasControlGroup) { // Without control group, counterfactual = flat line at pre-treatment average const startTs = startDate.getTime(); + const endTs = endDate.getTime(); const beforeStartTs = beforeStart.getTime(); const beforeEndTs = beforeEnd.getTime(); // To calculate counterfactual, we need the "before" average. // Treatment before period: - const beforeData = treatmentWeekly.filter(w => w.timestamp >= beforeStartTs && w.timestamp < beforeEndTs); + const { before: beforeData } = splitPeriods(treatmentWeekly, startTs, endTs, beforeStartTs, beforeEndTs); // Use splitPeriods to match exactly const preAvg = avgMetric(beforeData, metric); return treatmentWeekly.map(w => ({ @@ -342,11 +359,12 @@ export function buildCounterfactualSeries( const controlWeekly = aggregateWeeklyMetrics(controlAsinSet, salesData, experiment.marketplace); const startTs = startDate.getTime(); + const endTs = endDate.getTime(); const beforeStartTs = beforeStart.getTime(); const beforeEndTs = beforeEnd.getTime(); - const tBeforeData = treatmentWeekly.filter(w => w.timestamp >= beforeStartTs && w.timestamp < beforeEndTs); - const cBeforeData = controlWeekly.filter(w => w.timestamp >= beforeStartTs && w.timestamp < beforeEndTs); + const { before: tBeforeData } = splitPeriods(treatmentWeekly, startTs, endTs, beforeStartTs, beforeEndTs); + const { before: cBeforeData } = splitPeriods(controlWeekly, startTs, endTs, beforeStartTs, beforeEndTs); const tPreAvg = avgMetric(tBeforeData, metric); const cPreAvg = avgMetric(cBeforeData, metric);