feat: enable custom baseline periods for Experiments to solve seasonality

This commit is contained in:
Christian Vidal Wolf
2026-02-23 18:34:04 +01:00
parent 69f4456942
commit 4935112a55
14 changed files with 428 additions and 1144 deletions
+42 -11
View File
@@ -118,13 +118,14 @@ function splitPeriods(
data: ComputedWeeklyMetrics[],
startTs: number,
endTs: number,
beforeStartTs: number
beforeStartTs: number,
beforeEndTs: number // Added to support separated custom periods
): { before: ComputedWeeklyMetrics[]; after: ComputedWeeklyMetrics[] } {
const before: ComputedWeeklyMetrics[] = [];
const after: ComputedWeeklyMetrics[] = [];
for (const w of data) {
if (w.timestamp >= beforeStartTs && w.timestamp < startTs) {
if (w.timestamp >= beforeStartTs && w.timestamp < beforeEndTs) {
before.push(w);
} else if (w.timestamp >= startTs && w.timestamp <= endTs) {
after.push(w);
@@ -216,8 +217,21 @@ export function computeDiD(
): DifferenceInDifferencesResult {
const startDate = parseLocalDate(experiment.start_date);
const endDate = experiment.end_date ? parseLocalDate(experiment.end_date) : new Date();
const durationMs = endDate.getTime() - startDate.getTime();
const beforeStart = new Date(startDate.getTime() - durationMs);
// Seasonal Baseline override logic
let beforeStart: Date;
let beforeEnd = startDate; // By default, before period goes up to the exact start date
if (experiment.baseline_start_date && experiment.baseline_end_date) {
beforeStart = parseLocalDate(experiment.baseline_start_date);
beforeEnd = parseLocalDate(experiment.baseline_end_date);
// Explicit end + 1 day so `< beforeEnd` logic includes the last day
beforeEnd = new Date(beforeEnd.getTime() + 86400000);
} else {
// Legacy auto-deduction (shifts the exact timeframe to the left)
const durationMs = endDate.getTime() - startDate.getTime();
beforeStart = new Date(startDate.getTime() - durationMs);
}
const treatmentAsinSet = getExperimentAsins(experiment.asins || [], salesData);
const treatmentWeekly = aggregateWeeklyMetrics(treatmentAsinSet, salesData, experiment.marketplace);
@@ -232,10 +246,11 @@ export function computeDiD(
const startTs = startDate.getTime();
const endTs = endDate.getTime();
const beforeStartTs = beforeStart.getTime();
const beforeEndTs = beforeEnd.getTime();
const tSplit = splitPeriods(treatmentWeekly, startTs, endTs, beforeStartTs);
const tSplit = splitPeriods(treatmentWeekly, startTs, endTs, beforeStartTs, beforeEndTs);
const cSplit = hasControlGroup
? splitPeriods(controlWeekly, startTs, endTs, beforeStartTs)
? splitPeriods(controlWeekly, startTs, endTs, beforeStartTs, beforeEndTs)
: { before: [] as ComputedWeeklyMetrics[], after: [] as ComputedWeeklyMetrics[] };
const metrics: Record<string, DiDMetricResult> = {};
@@ -285,8 +300,19 @@ export function buildCounterfactualSeries(
): TrendDataPoint[] {
const startDate = parseLocalDate(experiment.start_date);
const endDate = experiment.end_date ? parseLocalDate(experiment.end_date) : new Date();
const durationMs = endDate.getTime() - startDate.getTime();
const beforeStart = new Date(startDate.getTime() - durationMs);
// Seasonal Baseline override logic
let beforeStart: Date;
let beforeEnd = startDate;
if (experiment.baseline_start_date && experiment.baseline_end_date) {
beforeStart = parseLocalDate(experiment.baseline_start_date);
beforeEnd = parseLocalDate(experiment.baseline_end_date);
beforeEnd = new Date(beforeEnd.getTime() + 86400000);
} else {
const durationMs = endDate.getTime() - startDate.getTime();
beforeStart = new Date(startDate.getTime() - durationMs);
}
const treatmentAsinSet = getExperimentAsins(experiment.asins || [], salesData);
const treatmentWeekly = aggregateWeeklyMetrics(treatmentAsinSet, salesData, experiment.marketplace);
@@ -297,7 +323,11 @@ export function buildCounterfactualSeries(
// Without control group, counterfactual = flat line at pre-treatment average
const startTs = startDate.getTime();
const beforeStartTs = beforeStart.getTime();
const beforeData = treatmentWeekly.filter(w => w.timestamp >= beforeStartTs && w.timestamp < startTs);
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 preAvg = avgMetric(beforeData, metric);
return treatmentWeekly.map(w => ({
@@ -313,9 +343,10 @@ export function buildCounterfactualSeries(
const startTs = startDate.getTime();
const beforeStartTs = beforeStart.getTime();
const beforeEndTs = beforeEnd.getTime();
const tBeforeData = treatmentWeekly.filter(w => w.timestamp >= beforeStartTs && w.timestamp < startTs);
const cBeforeData = controlWeekly.filter(w => w.timestamp >= beforeStartTs && w.timestamp < startTs);
const tBeforeData = treatmentWeekly.filter(w => w.timestamp >= beforeStartTs && w.timestamp < beforeEndTs);
const cBeforeData = controlWeekly.filter(w => w.timestamp >= beforeStartTs && w.timestamp < beforeEndTs);
const tPreAvg = avgMetric(tBeforeData, metric);
const cPreAvg = avgMetric(cBeforeData, metric);