mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 11:55:22 +02:00
fix: implement product line seasonality for forecast fallback
This commit is contained in:
@@ -1768,14 +1768,49 @@ export const calculateForecastViewData = (
|
||||
monthMap.set(m, (monthMap.get(m) || 0) + r.units);
|
||||
});
|
||||
|
||||
// 1b. Determine Line-Level Weights (NEW STRATEGY)
|
||||
const lineWeightsMap = new Map<string, number[]>();
|
||||
const linesMap = new Map<string, SalesRecord[]>();
|
||||
|
||||
historicalData.forEach(r => {
|
||||
if (!r.line) return;
|
||||
if (!linesMap.has(r.line)) linesMap.set(r.line, []);
|
||||
linesMap.get(r.line)!.push(r);
|
||||
});
|
||||
|
||||
linesMap.forEach((records, line) => {
|
||||
// We use the same getWeightsInfo logic but for the whole line
|
||||
const info = getWeightsInfo(records);
|
||||
if (info) {
|
||||
lineWeightsMap.set(line, info.weights);
|
||||
} else {
|
||||
// Fallback for line if it has data but odd distribution?
|
||||
// Actually getWeightsInfo returns null only if total=0.
|
||||
// If we have records but 0 units total, we skip map set, so it will fall to global.
|
||||
}
|
||||
});
|
||||
|
||||
return forecastData.map(fc => {
|
||||
const identifier = fc.asin.toUpperCase();
|
||||
const meta = asinMetadata.get(identifier);
|
||||
|
||||
// Resolve Line: Try meta first, then forecast file
|
||||
const resolvedLine = meta?.line || fc.line || "Unassigned";
|
||||
|
||||
const avgWeeklySales = velocityMap?.get(identifier) || 0;
|
||||
|
||||
// 2. Determine weights for this ASIN
|
||||
const productHistoricalRecords = dataByAsinHistorical.get(identifier) || [];
|
||||
let productWeights = globalWeights;
|
||||
|
||||
// LAYERED FALLBACK STRATEGY:
|
||||
// Level 1: Product's own history (Most accurate)
|
||||
// Level 2: Product Line's history (Good for new items in known category e.g. Advent Calendars)
|
||||
// Level 3: Global/Pan-EU history (Generic fallback)
|
||||
|
||||
const lineWeights = lineWeightsMap.get(resolvedLine);
|
||||
const baselineWeights = lineWeights || globalWeights;
|
||||
|
||||
let finalWeights = baselineWeights;
|
||||
|
||||
if (productHistoricalRecords.length > 0) {
|
||||
const historyToUse = isUkOnly
|
||||
@@ -1785,12 +1820,9 @@ export const calculateForecastViewData = (
|
||||
const info = getWeightsInfo(historyToUse);
|
||||
if (info) {
|
||||
// Adaptive Blending (Bayesian Shrinkage):
|
||||
// We blend local seasonality with global seasonality based on how many months of data we have.
|
||||
// 12 months = 85% local, 15% global (safety net)
|
||||
// 6 months = 42.5% local, 57.5% global
|
||||
// 0 months = 0% local, 100% global
|
||||
// We blend local seasonality with baseline (Line or Global) based on data density.
|
||||
const trustFactor = (info.monthsCount / 12) * 0.85;
|
||||
productWeights = info.weights.map((w, i) => (w * trustFactor) + (globalWeights[i] * (1 - trustFactor)));
|
||||
finalWeights = info.weights.map((w, i) => (w * trustFactor) + (baselineWeights[i] * (1 - trustFactor)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1800,7 +1832,7 @@ export const calculateForecastViewData = (
|
||||
let totalForecastUnits = 0;
|
||||
|
||||
MONTH_ORDER.forEach((m, idx) => {
|
||||
const forecastUnits = Math.round(fc.annualForecast * productWeights[idx]);
|
||||
const forecastUnits = Math.round(fc.annualForecast * finalWeights[idx]);
|
||||
const actualUnits = actuals2026.get(identifier)?.get(m) || 0;
|
||||
|
||||
monthlyData[m] = {
|
||||
|
||||
Reference in New Issue
Block a user