Implement Smart Seasonality blending for products with sparse history (fixes 0-forecast issue)

This commit is contained in:
Christian Vidal Wolf
2026-01-28 15:17:58 +01:00
parent d40740fce7
commit 0ea94aefd1
+26 -19
View File
@@ -1625,16 +1625,18 @@ export const calculateForecastViewData = (
const data2026 = rawData.filter(r => r.year === 2026); const data2026 = rawData.filter(r => r.year === 2026);
// Calculate Seasonality weights for 2025 // 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); const weights = new Array(12).fill(0);
let total = 0; let total = 0;
const seenMonths = new Set<string>();
records.forEach(r => { records.forEach(r => {
const m = r.month.split('-')[0]; const m = r.month.split('-')[0];
const idx = MONTH_ORDER.indexOf(m); const idx = MONTH_ORDER.indexOf(m);
if (idx !== -1) { if (idx !== -1 && r.units > 0) {
weights[idx] += r.units; weights[idx] += r.units;
total += 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. // Return null ONLY if there's no data at all for this ASIN in 2025.
if (total === 0) return null; 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 // 1. Determine Global/Default Weights
@@ -1734,25 +1739,27 @@ export const calculateForecastViewData = (
if (productRecords2025.length > 0) { if (productRecords2025.length > 0) {
if (isUkOnly) { 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'); const ukProd = productRecords2025.filter(r => r.customer === 'Amazon UK');
// We use hybrid approach only if we have PanEU data for this product too const info = getWeightsInfo(ukProd);
const peProd = productRecords2025.filter(r => PAN_EU_COUNTRIES.includes(r.customer)); if (info) {
if (info.monthsCount < 6) {
// If we have solid data for this product, use Hybrid or UK weights // Smart Blending: 50% specific, 50% global
const specificWeights = getHybridWeights(peProd, ukProd); productWeights = info.weights.map((w, i) => (w * 0.5) + (globalWeights[i] * 0.5));
// Wait, getHybridWeights calls getGlobalWeightsInner which never returns null. } else {
// We need to check if specific product data is sparse. productWeights = info.weights;
}
// Let's simplify: Check if we have enough UK history
const ukWeights = getWeights(ukProd);
if (ukWeights) {
productWeights = ukWeights;
} }
} else { } else {
const w = getWeights(productRecords2025); const info = getWeightsInfo(productRecords2025);
if (w) productWeights = w; if (info) {
// If w is null (sparse data), productWeights remains globalWeights (Default) 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)
} }
} }