From 0ea94aefd110db1a64741b53256b515b3c6904ca Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Wed, 28 Jan 2026 15:17:58 +0100 Subject: [PATCH] Implement Smart Seasonality blending for products with sparse history (fixes 0-forecast issue) --- services/dataProcessor.ts | 45 ++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/services/dataProcessor.ts b/services/dataProcessor.ts index 34e49bb..42d5b43 100644 --- a/services/dataProcessor.ts +++ b/services/dataProcessor.ts @@ -1625,16 +1625,18 @@ export const calculateForecastViewData = ( const data2026 = rawData.filter(r => r.year === 2026); // Calculate Seasonality weights for 2025 - const getWeights = (records: SalesRecord[]): number[] | null => { + const getWeightsInfo = (records: SalesRecord[]): { weights: number[]; monthsCount: number } | null => { const weights = new Array(12).fill(0); let total = 0; + const seenMonths = new Set(); records.forEach(r => { const m = r.month.split('-')[0]; const idx = MONTH_ORDER.indexOf(m); - if (idx !== -1) { + if (idx !== -1 && r.units > 0) { weights[idx] += r.units; total += r.units; + seenMonths.add(m); } }); @@ -1642,7 +1644,10 @@ export const calculateForecastViewData = ( // Return null ONLY if there's no data at all for this ASIN in 2025. if (total === 0) return null; - return weights.map(w => w / total); + return { + weights: weights.map(w => w / total), + monthsCount: seenMonths.size + }; }; // 1. Determine Global/Default Weights @@ -1734,25 +1739,27 @@ export const calculateForecastViewData = ( if (productRecords2025.length > 0) { if (isUkOnly) { - // For UK Only, we try to be strict, but fallback to global UK weights if needed const ukProd = productRecords2025.filter(r => r.customer === 'Amazon UK'); - // We use hybrid approach only if we have PanEU data for this product too - const peProd = productRecords2025.filter(r => PAN_EU_COUNTRIES.includes(r.customer)); - - // If we have solid data for this product, use Hybrid or UK weights - const specificWeights = getHybridWeights(peProd, ukProd); - // Wait, getHybridWeights calls getGlobalWeightsInner which never returns null. - // We need to check if specific product data is sparse. - - // Let's simplify: Check if we have enough UK history - const ukWeights = getWeights(ukProd); - if (ukWeights) { - productWeights = ukWeights; + const info = getWeightsInfo(ukProd); + if (info) { + if (info.monthsCount < 6) { + // Smart Blending: 50% specific, 50% global + productWeights = info.weights.map((w, i) => (w * 0.5) + (globalWeights[i] * 0.5)); + } else { + productWeights = info.weights; + } } } else { - const w = getWeights(productRecords2025); - if (w) productWeights = w; - // If w is null (sparse data), productWeights remains globalWeights (Default) + const info = getWeightsInfo(productRecords2025); + if (info) { + if (info.monthsCount < 6) { + // Smart Blending: 50% specific, 50% global + productWeights = info.weights.map((w, i) => (w * 0.5) + (globalWeights[i] * 0.5)); + } else { + productWeights = info.weights; + } + } + // If info is null (sparse data), productWeights remains globalWeights (Default) } }