ModelMuse02/AI_Sales_Forecasting
0
1/**2 * KPI Calculation Module3 * Computes business metrics and performance indicators4 */5 6import {7 CleanedDataRow,8 MonthlyAggregation,9 ProductAggregation,10 KPIMetrics,11} from '../types';12 13/**14 * Calculate standard deviation15 */16function standardDeviation(values: number[]): number {17 if (values.length === 0) return 0;18 const mean = values.reduce((a, b) => a + b, 0) / values.length;19 const squaredDiffs = values.map(v => Math.pow(v - mean, 2));20 return Math.sqrt(squaredDiffs.reduce((a, b) => a + b, 0) / values.length);21}22 23/**24 * Calculate coefficient of variation (volatility)25 */26function coefficientOfVariation(values: number[]): number {27 if (values.length === 0) return 0;28 const mean = values.reduce((a, b) => a + b, 0) / values.length;29 if (mean === 0) return 0;30 return (standardDeviation(values) / mean) * 100;31}32 33/**34 * Classify volatility level35 */36function classifyVolatility(cv: number): 'Low' | 'Moderate' | 'High' | 'Very High' {37 if (cv < 15) return 'Low';38 if (cv < 30) return 'Moderate';39 if (cv < 50) return 'High';40 return 'Very High';41}42 43/**44 * Calculate Herfindahl-Hirschman Index for concentration45 */46function calculateHHI(products: ProductAggregation[]): number {47 const totalRevenue = products.reduce((sum, p) => sum + p.totalRevenue, 0);48 if (totalRevenue === 0) return 0;49 50 return products.reduce((sum, p) => {51 const share = (p.totalRevenue / totalRevenue) * 100;52 return sum + Math.pow(share, 2);53 }, 0);54}55 56/**57 * Classify concentration risk based on HHI58 */59function classifyConcentration(hhi: number): 'Low' | 'Moderate' | 'High' {60 if (hhi < 1500) return 'Low';61 if (hhi < 2500) return 'Moderate';62 return 'High';63}64 65/**66 * Calculate month-over-month growth rates67 */68function calculateGrowthRates(monthlyData: MonthlyAggregation[]): number[] {69 const growthRates: number[] = [];70 71 for (let i = 1; i < monthlyData.length; i++) {72 const previous = monthlyData[i - 1].totalRevenue;73 const current = monthlyData[i].totalRevenue;74 75 if (previous > 0) {76 const growthRate = ((current - previous) / previous) * 100;77 growthRates.push(growthRate);78 }79 }80 81 return growthRates;82}83 84/**85 * Detect seasonality strength using autocorrelation86 */87function detectSeasonalityStrength(monthlyData: MonthlyAggregation[]): number {88 if (monthlyData.length < 13) return 0;89 90 const revenues = monthlyData.map(m => m.totalRevenue);91 const mean = revenues.reduce((a, b) => a + b, 0) / revenues.length;92 93 // Calculate lag-12 autocorrelation (yearly seasonality)94 let numerator = 0;95 let denominator = 0;96 97 for (let i = 0; i < revenues.length - 12; i++) {98 numerator += (revenues[i] - mean) * (revenues[i + 12] - mean);99 }100 101 for (let i = 0; i < revenues.length; i++) {102 denominator += Math.pow(revenues[i] - mean, 2);103 }104 105 if (denominator === 0) return 0;106 107 const autocorrelation = numerator / denominator;108 return Math.max(0, Math.min(1, autocorrelation));109}110 111/**112 * Find peak and low seasons113 */114function findSeasons(monthlyData: MonthlyAggregation[]): { peak: string | null; low: string | null } {115 if (monthlyData.length === 0) {116 return { peak: null, low: null };117 }118 119 // Group by month of year120 const monthlyAverages = new Map<number, number[]>();121 122 for (const data of monthlyData) {123 const month = data.date.getMonth();124 if (!monthlyAverages.has(month)) {125 monthlyAverages.set(month, []);126 }127 monthlyAverages.get(month)!.push(data.totalRevenue);128 }129 130 const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',131 'July', 'August', 'September', 'October', 'November', 'December'];132 133 let peakMonth = 0;134 let lowMonth = 0;135 let maxAvg = -Infinity;136 let minAvg = Infinity;137 138 for (const [month, values] of monthlyAverages) {139 const avg = values.reduce((a, b) => a + b, 0) / values.length;140 if (avg > maxAvg) {141 maxAvg = avg;142 peakMonth = month;143 }144 if (avg < minAvg) {145 minAvg = avg;146 lowMonth = month;147 }148 }149 150 return {151 peak: monthNames[peakMonth],152 low: monthNames[lowMonth],153 };154}155 156/**157 * Calculate all KPIs from the dataset158 */159export function calculateKPIs(160 cleanedData: CleanedDataRow[],161 monthlyData: MonthlyAggregation[],162 productData: ProductAggregation[]163): KPIMetrics {164 // Basic aggregations165 const totalRevenue = cleanedData.reduce((sum, row) => sum + row.revenue, 0);166 const totalUnits = cleanedData.reduce((sum, row) => sum + row.unitsSold, 0);167 const totalTransactions = cleanedData.length;168 const averageOrderValue = totalTransactions > 0 ? totalRevenue / totalTransactions : 0;169 170 // Profit metrics (if cost data available)171 const hasCost = cleanedData.some(row => row.cost !== null);172 let totalProfit: number | null = null;173 let profitMargin: number | null = null;174 175 if (hasCost) {176 totalProfit = cleanedData.reduce((sum, row) => sum + (row.profit || 0), 0);177 profitMargin = totalRevenue > 0 ? (totalProfit / totalRevenue) * 100 : null;178 }179 180 // Growth metrics181 const growthRates = calculateGrowthRates(monthlyData);182 const averageMonthlyGrowth = growthRates.length > 0183 ? growthRates.reduce((a, b) => a + b, 0) / growthRates.length184 : 0;185 186 // Overall revenue growth (first to last period)187 let revenueGrowthRate = 0;188 if (monthlyData.length >= 2) {189 const first = monthlyData[0].totalRevenue;190 const last = monthlyData[monthlyData.length - 1].totalRevenue;191 revenueGrowthRate = first > 0 ? ((last - first) / first) * 100 : 0;192 }193 194 // Volatility metrics195 const monthlyRevenues = monthlyData.map(m => m.totalRevenue);196 const revenueVolatility = coefficientOfVariation(monthlyRevenues);197 const volatilityClassification = classifyVolatility(revenueVolatility);198 199 // Best and worst months200 let bestMonth: { period: string; revenue: number } | null = null;201 let worstMonth: { period: string; revenue: number } | null = null;202 203 if (monthlyData.length > 0) {204 const sorted = [...monthlyData].sort((a, b) => b.totalRevenue - a.totalRevenue);205 bestMonth = { period: sorted[0].period, revenue: sorted[0].totalRevenue };206 worstMonth = { period: sorted[sorted.length - 1].period, revenue: sorted[sorted.length - 1].totalRevenue };207 }208 209 // Product concentration210 const herfindahlIndex = calculateHHI(productData);211 const concentrationRisk = classifyConcentration(herfindahlIndex);212 const topProduct = productData[0];213 const productConcentration = topProduct ? topProduct.percentage : 0;214 215 // Seasonality216 const seasonalityStrength = detectSeasonalityStrength(monthlyData);217 const seasons = findSeasons(monthlyData);218 219 return {220 totalRevenue,221 totalUnits,222 totalTransactions,223 averageOrderValue,224 totalProfit,225 profitMargin,226 revenueGrowthRate,227 monthlyGrowthRates: growthRates,228 averageMonthlyGrowth,229 revenueVolatility,230 volatilityClassification,231 bestMonth,232 worstMonth,233 productConcentration,234 topProducts: productData.slice(0, 5),235 herfindahlIndex,236 concentrationRisk,237 seasonalityStrength,238 peakSeason: seasons.peak,239 lowSeason: seasons.low,240 };241}242 243/**244 * Format currency value245 */246export function formatCurrency(value: number): string {247 return new Intl.NumberFormat('en-US', {248 style: 'currency',249 currency: 'USD',250 minimumFractionDigits: 0,251 maximumFractionDigits: 0,252 }).format(value);253}254 255/**256 * Format percentage value257 */258export function formatPercentage(value: number): string {259 return `${value >= 0 ? '+' : ''}${value.toFixed(1)}%`;260}261 262/**263 * Format large numbers with abbreviations264 */265export function formatNumber(value: number): string {266 if (value >= 1000000) {267 return `${(value / 1000000).toFixed(1)}M`;268 }269 if (value >= 1000) {270 return `${(value / 1000).toFixed(1)}K`;271 }272 return value.toFixed(0);273}274 