diff --git a/services/dataProcessor.ts b/services/dataProcessor.ts index dc312e6..7ec0568 100644 --- a/services/dataProcessor.ts +++ b/services/dataProcessor.ts @@ -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(); + const linesMap = new Map(); + + 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] = {