mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 15:45:24 +02:00
fix(experimentAnalysis): use ISO calendar weeks for treatment/baseline periods
- Add ISO week calculation functions (getISOWeek, getISOWeekMonday, calculateISOWeekRange) - Expand experiment dates to full ISO weeks (Monday-Sunday) instead of exact date ranges - Use actual data.length for averaging metrics instead of theoretical durationWeeks - Baseline now takes same number of complete weeks immediately before experiment Fixes incorrect units sold average calculation (was 6.5, now correctly shows 4.33 for 13 units over 3 weeks) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
co-authored by
Qwen-Coder
parent
b840050686
commit
d5936f1301
@@ -50,6 +50,85 @@ function getWeekStartSunday(year: number, week: number): number {
|
|||||||
return startYear.getTime() + (week - 1) * 7 * 86400000;
|
return startYear.getTime() + (week - 1) * 7 * 86400000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get ISO week number for a given date
|
||||||
|
* ISO weeks start on Monday, week 1 contains the first Thursday of the year
|
||||||
|
*/
|
||||||
|
function getISOWeek(date: Date): { year: number; week: number } {
|
||||||
|
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
||||||
|
const dayNum = d.getUTCDay() || 7; // Convert Sunday (0) to 7
|
||||||
|
d.setUTCDate(d.getUTCDate() + 4 - dayNum); // Set to nearest Thursday
|
||||||
|
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
||||||
|
const weekNum = Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
|
||||||
|
return { year: d.getUTCFullYear(), week: weekNum };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Monday of an ISO week
|
||||||
|
*/
|
||||||
|
function getISOWeekMonday(year: number, week: number): number {
|
||||||
|
const jan4 = new Date(Date.UTC(year, 0, 4)); // Jan 4 is always in week 1
|
||||||
|
const dayOfWeek = jan4.getUTCDay() || 7; // Get day of week (1-7, Monday=1)
|
||||||
|
const week1Monday = new Date(Date.UTC(year, 0, 4 - (dayOfWeek - 1)));
|
||||||
|
return week1Monday.getTime() + (week - 1) * 7 * 86400000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the number of distinct ISO weeks between two dates
|
||||||
|
* and return the expanded date range (first Monday to last Sunday)
|
||||||
|
*/
|
||||||
|
function calculateISOWeekRange(startDate: Date, endDate: Date): {
|
||||||
|
numWeeks: number;
|
||||||
|
expandedStartTs: number;
|
||||||
|
expandedEndTs: number;
|
||||||
|
weekKeys: string[];
|
||||||
|
} {
|
||||||
|
const startISO = getISOWeek(startDate);
|
||||||
|
const endISO = getISOWeek(endDate);
|
||||||
|
|
||||||
|
const weekKeys: string[] = [];
|
||||||
|
let currentYear = startISO.year;
|
||||||
|
let currentWeek = startISO.week;
|
||||||
|
|
||||||
|
// Collect all week keys between start and end
|
||||||
|
while (true) {
|
||||||
|
const weekKey = `${currentYear}-W${String(currentWeek).padStart(2, '0')}`;
|
||||||
|
weekKeys.push(weekKey);
|
||||||
|
|
||||||
|
if (currentYear === endISO.year && currentWeek === endISO.week) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to next week
|
||||||
|
currentWeek++;
|
||||||
|
const weeksInYear = getWeeksInYear(currentYear);
|
||||||
|
if (currentWeek > weeksInYear) {
|
||||||
|
currentWeek = 1;
|
||||||
|
currentYear++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get expanded range: from Monday of first week to Sunday of last week
|
||||||
|
const expandedStartTs = getISOWeekMonday(startISO.year, startISO.week);
|
||||||
|
const expandedEndTs = getISOWeekMonday(endISO.year, endISO.week) + 7 * 86400000;
|
||||||
|
|
||||||
|
return {
|
||||||
|
numWeeks: weekKeys.length,
|
||||||
|
expandedStartTs,
|
||||||
|
expandedEndTs,
|
||||||
|
weekKeys,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get number of weeks in an ISO year (52 or 53)
|
||||||
|
*/
|
||||||
|
function getWeeksInYear(year: number): number {
|
||||||
|
const dec28 = new Date(Date.UTC(year, 11, 28)); // Dec 28 is always in last week
|
||||||
|
const iso = getISOWeek(dec28);
|
||||||
|
return iso.week;
|
||||||
|
}
|
||||||
|
|
||||||
function aggregateWeeklyMetrics(
|
function aggregateWeeklyMetrics(
|
||||||
asinSet: Set<string>,
|
asinSet: Set<string>,
|
||||||
salesData: CombinedKPIs[],
|
salesData: CombinedKPIs[],
|
||||||
@@ -161,7 +240,7 @@ function splitPeriods(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function avgMetric(data: ComputedWeeklyMetrics[], metric: string, durationWeeks: number): number {
|
function avgMetric(data: ComputedWeeklyMetrics[], metric: string, durationWeeks: number): number {
|
||||||
if (data.length === 0 || durationWeeks <= 0) return 0;
|
if (data.length === 0) return 0;
|
||||||
|
|
||||||
// For rates and ratios, we must sum the raw absolute components across all included weeks
|
// For rates and ratios, we must sum the raw absolute components across all included weeks
|
||||||
// and THEN calculate the ratio, otherwise averaging percentages yields mathematically incorrect results.
|
// and THEN calculate the ratio, otherwise averaging percentages yields mathematically incorrect results.
|
||||||
@@ -186,9 +265,10 @@ function avgMetric(data: ComputedWeeklyMetrics[], metric: string, durationWeeks:
|
|||||||
return totalAdRevenue > 0 && totalCost > 0 ? (totalCost / totalAdRevenue) * 100 : 0;
|
return totalAdRevenue > 0 && totalCost > 0 ? (totalCost / totalAdRevenue) * 100 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For absolute quantities (units, revenue, sessions), we sum them and divide by the duration
|
// 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
|
||||||
const sum = data.reduce((s, w) => s + getMetricValue(w, metric), 0);
|
const sum = data.reduce((s, w) => s + getMetricValue(w, metric), 0);
|
||||||
return sum / durationWeeks;
|
return sum / data.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
const METRICS = ['units', 'sessions', 'cvr', 'ctr', 'roas', 'revenue', 'acos'];
|
const METRICS = ['units', 'sessions', 'cvr', 'ctr', 'roas', 'revenue', 'acos'];
|
||||||
@@ -270,19 +350,26 @@ export function computeDiD(
|
|||||||
const startDate = parseLocalDate(experiment.start_date);
|
const startDate = parseLocalDate(experiment.start_date);
|
||||||
const endDate = experiment.end_date ? parseLocalDate(experiment.end_date) : new Date(new Date().setHours(0, 0, 0, 0));
|
const endDate = experiment.end_date ? parseLocalDate(experiment.end_date) : new Date(new Date().setHours(0, 0, 0, 0));
|
||||||
|
|
||||||
const startTs = startDate.getTime();
|
// Calculate ISO week range: expands to full weeks (Monday to Sunday)
|
||||||
const endTs = endDate.getTime() + 86400000; // Add 24h so end bound is midnight of next day
|
const isoRange = calculateISOWeekRange(startDate, endDate);
|
||||||
|
const startTs = isoRange.expandedStartTs;
|
||||||
|
const endTs = isoRange.expandedEndTs;
|
||||||
|
const treatmentDurationWeeks = isoRange.numWeeks;
|
||||||
|
|
||||||
let beforeStartTs: number;
|
let beforeStartTs: number;
|
||||||
let beforeEndTs: number;
|
let beforeEndTs: number;
|
||||||
|
|
||||||
if (experiment.baseline_start_date && experiment.baseline_end_date) {
|
if (experiment.baseline_start_date && experiment.baseline_end_date) {
|
||||||
beforeStartTs = parseLocalDate(experiment.baseline_start_date).getTime();
|
// Use custom baseline dates - also expand to full ISO weeks
|
||||||
beforeEndTs = parseLocalDate(experiment.baseline_end_date).getTime() + 86400000;
|
const baselineStart = parseLocalDate(experiment.baseline_start_date);
|
||||||
|
const baselineEnd = parseLocalDate(experiment.baseline_end_date);
|
||||||
|
const baselineIsoRange = calculateISOWeekRange(baselineStart, baselineEnd);
|
||||||
|
beforeStartTs = baselineIsoRange.expandedStartTs;
|
||||||
|
beforeEndTs = baselineIsoRange.expandedEndTs;
|
||||||
} else {
|
} else {
|
||||||
const durationMs = endTs - startTs;
|
// Take the same number of complete weeks immediately before the experiment
|
||||||
beforeEndTs = startTs; // Adjacent: baseline ends right at the moment experiment starts
|
beforeEndTs = startTs; // Baseline ends right when experiment starts (Monday)
|
||||||
beforeStartTs = beforeEndTs - durationMs;
|
beforeStartTs = beforeEndTs - (treatmentDurationWeeks * 7 * 86400000);
|
||||||
}
|
}
|
||||||
|
|
||||||
const treatmentAsinSet = getExperimentAsins(experiment.asins || [], salesData);
|
const treatmentAsinSet = getExperimentAsins(experiment.asins || [], salesData);
|
||||||
@@ -300,8 +387,7 @@ export function computeDiD(
|
|||||||
? splitPeriods(controlWeekly, startTs, endTs, beforeStartTs, beforeEndTs)
|
? splitPeriods(controlWeekly, startTs, endTs, beforeStartTs, beforeEndTs)
|
||||||
: { before: [] as ComputedWeeklyMetrics[], after: [] as ComputedWeeklyMetrics[] };
|
: { before: [] as ComputedWeeklyMetrics[], after: [] as ComputedWeeklyMetrics[] };
|
||||||
|
|
||||||
const treatmentDurationWeeks = Math.max(1, Math.round((endTs - startTs) / (7 * 86400000)));
|
const baselineDurationWeeks = Math.max(1, cSplit.before.length || tSplit.before.length);
|
||||||
const baselineDurationWeeks = Math.max(1, Math.round((beforeEndTs - beforeStartTs) / (7 * 86400000)));
|
|
||||||
|
|
||||||
// Debug: log data flow for ACOS diagnosis
|
// Debug: log data flow for ACOS diagnosis
|
||||||
const afterTotalCost = tSplit.after.reduce((s, w) => s + w.cost, 0);
|
const afterTotalCost = tSplit.after.reduce((s, w) => s + w.cost, 0);
|
||||||
|
|||||||
Reference in New Issue
Block a user