Fix Seasonality: Use Global Catalog curve as fallback for sparse ASIN history

This commit is contained in:
Christian Vidal Wolf
2026-01-28 13:17:04 +01:00
parent cfce00cd63
commit 894271b549
+45 -13
View File
@@ -1625,7 +1625,7 @@ 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[]) => { const getWeights = (records: SalesRecord[]): 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>(); const seenMonths = new Set<string>();
@@ -1640,26 +1640,44 @@ export const calculateForecastViewData = (
} }
}); });
// Safety Fallback: If we have sparse data (e.g., only Jan loaded), // 1. No data -> Fallback
// using it as 100% seasonality skews the forecast entirely to that month. if (total === 0) return null;
// We require at least 4 months of history to trust the curve; otherwise, we assume flat seasonality (1/12).
if (total === 0 || seenMonths.size < 4) { // 2. Sparse Data Check (< 4 months)
return new Array(12).fill(1 / 12); // If an ASIN has very little history (e.g. only Jan), using its own curve implies 100% seasonality in Jan.
} // The user requested to use the "General Catalog Seasonality" in these cases.
if (seenMonths.size < 4) return null;
return weights.map(w => w / total); return weights.map(w => w / total);
}; };
// 1. Determine Global/Default Weights // 1. Determine Global/Default Weights
const panEuData2025 = data2025.filter(r => PAN_EU_COUNTRIES.includes(r.customer)); const panEuData2025 = data2025.filter(r => PAN_EU_COUNTRIES.includes(r.customer));
const panEuWeights = getWeights(panEuData2025); // For Global Weights, we do NOT return null on sparse data (we accept whatever we have for the whole catalog)
// We recreate a simple version of getWeights that doesn't return null for the global set
const getGlobalWeightsInner = (records: SalesRecord[]) => {
const weights = new Array(12).fill(0);
let total = 0;
records.forEach(r => {
const m = r.month.split('-')[0];
const idx = MONTH_ORDER.indexOf(m);
if (idx !== -1) {
weights[idx] += r.units;
total += r.units;
}
});
return total === 0 ? new Array(12).fill(1 / 12) : weights.map(w => w / total);
};
const panEuWeights = getGlobalWeightsInner(panEuData2025);
// Check if we are in UK-only mode // Check if we are in UK-only mode
const isUkOnly = filters?.customer?.includes('Amazon UK') && filters.customer.length === 1; const isUkOnly = filters?.customer?.includes('Amazon UK') && filters.customer.length === 1;
const getHybridWeights = (paEuRecords: SalesRecord[], ukRecords: SalesRecord[]) => { const getHybridWeights = (paEuRecords: SalesRecord[], ukRecords: SalesRecord[]) => {
const peWeights = getWeights(paEuRecords); // Use Inner helper to ensure we always get weights for global subsets
const ukWeights = getWeights(ukRecords); const peWeights = getGlobalWeightsInner(paEuRecords);
const ukWeights = getGlobalWeightsInner(ukRecords);
// Blend: Jan-Aug from Pan-EU, Sep-Dec from UK // Blend: Jan-Aug from Pan-EU, Sep-Dec from UK
const hybrid = new Array(12).fill(0); const hybrid = new Array(12).fill(0);
@@ -1711,11 +1729,25 @@ export const calculateForecastViewData = (
if (productRecords2025.length > 0) { if (productRecords2025.length > 0) {
if (isUkOnly) { if (isUkOnly) {
const peProd = productRecords2025.filter(r => PAN_EU_COUNTRIES.includes(r.customer)); // 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');
productWeights = getHybridWeights(peProd.length > 0 ? peProd : panEuData2025, ukProd); // 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;
}
} else { } else {
productWeights = getWeights(productRecords2025); const w = getWeights(productRecords2025);
if (w) productWeights = w;
// If w is null (sparse data), productWeights remains globalWeights (Default)
} }
} }