CoolFace
Apppublic

ModelMuse02/AI_Sales_Forecasting

sourceHugging Faceupdated 7mo agoView on Hugging Face
0likes
forecasting.ts309 linesDownload Raw Back to root
1/**2 * Forecasting Module3 * Implements time-series forecasting with trend and seasonality decomposition4 * Uses a Prophet-like approach adapted for browser execution5 */6 7import { MonthlyAggregation, ForecastPoint, ForecastResult } from '../types';8 9/**10 * Simple linear regression11 */12function linearRegression(x: number[], y: number[]): { slope: number; intercept: number; r2: number } {13  const n = x.length;14  if (n === 0) return { slope: 0, intercept: 0, r2: 0 };15  16  const sumX = x.reduce((a, b) => a + b, 0);17  const sumY = y.reduce((a, b) => a + b, 0);18  const sumXY = x.reduce((acc, xi, i) => acc + xi * y[i], 0);19  const sumXX = x.reduce((acc, xi) => acc + xi * xi, 0);20  21  const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);22  const intercept = (sumY - slope * sumX) / n;23  24  // Calculate R²25  const meanY = sumY / n;26  const ssTotal = y.reduce((acc, yi) => acc + Math.pow(yi - meanY, 2), 0);27  const ssResidual = y.reduce((acc, yi, i) => acc + Math.pow(yi - (slope * x[i] + intercept), 2), 0);28  const r2 = ssTotal > 0 ? 1 - (ssResidual / ssTotal) : 0;29  30  return { slope, intercept, r2 };31}32 33/**34 * Extract seasonality pattern using monthly averages35 */36function extractSeasonality(data: MonthlyAggregation[]): { pattern: { [month: number]: number }; peakMonth: number; troughMonth: number } {37  const monthlyValues = new Map<number, number[]>();38  39  for (const d of data) {40    const month = d.date.getMonth();41    if (!monthlyValues.has(month)) {42      monthlyValues.set(month, []);43    }44    monthlyValues.get(month)!.push(d.totalRevenue);45  }46  47  const pattern: { [month: number]: number } = {};48  let peakMonth = 0;49  let troughMonth = 0;50  let maxFactor = -Infinity;51  let minFactor = Infinity;52  53  // Calculate overall mean54  const allValues = data.map(d => d.totalRevenue);55  const overallMean = allValues.reduce((a, b) => a + b, 0) / allValues.length;56  57  for (let month = 0; month < 12; month++) {58    const values = monthlyValues.get(month);59    if (values && values.length > 0) {60      const monthMean = values.reduce((a, b) => a + b, 0) / values.length;61      pattern[month] = overallMean > 0 ? monthMean / overallMean : 1;62      63      if (pattern[month] > maxFactor) {64        maxFactor = pattern[month];65        peakMonth = month;66      }67      if (pattern[month] < minFactor) {68        minFactor = pattern[month];69        troughMonth = month;70      }71    } else {72      pattern[month] = 1;73    }74  }75  76  return { pattern, peakMonth, troughMonth };77}78 79/**80 * Calculate forecast error metrics81 */82function calculateMetrics(actual: number[], predicted: number[]): { mae: number; rmse: number; mape: number } {83  if (actual.length === 0) return { mae: 0, rmse: 0, mape: 0 };84  85  let sumAbsError = 0;86  let sumSquaredError = 0;87  let sumPercentError = 0;88  let validCount = 0;89  90  for (let i = 0; i < actual.length; i++) {91    const error = Math.abs(actual[i] - predicted[i]);92    sumAbsError += error;93    sumSquaredError += error * error;94    95    if (actual[i] !== 0) {96      sumPercentError += error / actual[i];97      validCount++;98    }99  }100  101  return {102    mae: sumAbsError / actual.length,103    rmse: Math.sqrt(sumSquaredError / actual.length),104    mape: validCount > 0 ? (sumPercentError / validCount) * 100 : 0,105  };106}107 108/**109 * Generate confidence intervals based on prediction error110 */111function generateConfidenceInterval(112  predicted: number,113  errorStd: number,114  stepsAhead: number,115  confidenceLevel: number = 0.95116): { lower: number; upper: number } {117  // Z-score for 95% confidence118  const zScore = confidenceLevel === 0.95 ? 1.96 : 1.645;119  120  // Widen confidence interval as we forecast further121  const widthFactor = 1 + (stepsAhead * 0.1);122  const margin = zScore * errorStd * widthFactor;123  124  return {125    lower: Math.max(0, predicted - margin),126    upper: predicted + margin,127  };128}129 130/**131 * Prophet-like forecasting implementation132 * Decomposes time series into trend + seasonality + residual133 */134export function generateForecast(135  monthlyData: MonthlyAggregation[],136  forecastMonths: number = 12137): ForecastResult {138  if (monthlyData.length < 3) {139    // Not enough data for meaningful forecast140    return {141      historicalData: [],142      forecastData: [],143      combinedData: [],144      metrics: { mae: 0, rmse: 0, mape: 0, r2: 0 },145      trend: { direction: 'Stable', slope: 0, intercept: 0 },146      seasonality: { detected: false, pattern: {}, peakMonth: 0, troughMonth: 0 },147      forecastSummary: {148        nextMonthPrediction: 0,149        sixMonthPrediction: 0,150        yearEndPrediction: 0,151        expectedGrowth: 0,152      },153    };154  }155  156  // Prepare data157  const revenues = monthlyData.map(d => d.totalRevenue);158  159  // Train-test split (80-20)160  const splitIndex = Math.floor(monthlyData.length * 0.8);161  const trainData = monthlyData.slice(0, splitIndex);162  const testData = monthlyData.slice(splitIndex);163  164  // Use full data if test set is too small165  const effectiveTrainData = testData.length < 3 ? monthlyData : trainData;166  const effectiveTestData = testData.length < 3 ? [] : testData;167  168  // Extract trend using linear regression169  const trainRevenues = effectiveTrainData.map(d => d.totalRevenue);170  const trainIndices = effectiveTrainData.map((_, i) => i);171  const regression = linearRegression(trainIndices, trainRevenues);172  173  // Extract seasonality174  const seasonality = extractSeasonality(effectiveTrainData);175  const seasonalityDetected = Object.values(seasonality.pattern).some(176    v => Math.abs(v - 1) > 0.1177  );178  179  // Calculate residuals and error standard deviation180  const trainPredictions = trainIndices.map(i => {181    const trendValue = regression.slope * i + regression.intercept;182    const month = effectiveTrainData[i].date.getMonth();183    return trendValue * (seasonality.pattern[month] || 1);184  });185  186  const residuals = trainRevenues.map((actual, i) => actual - trainPredictions[i]);187  const errorStd = Math.sqrt(188    residuals.reduce((sum, r) => sum + r * r, 0) / residuals.length189  );190  191  // Evaluate on test set192  let testMetrics = { mae: 0, rmse: 0, mape: 0 };193  if (effectiveTestData.length > 0) {194    const testPredictions = effectiveTestData.map((d, i) => {195      const idx = splitIndex + i;196      const trendValue = regression.slope * idx + regression.intercept;197      const month = d.date.getMonth();198      return trendValue * (seasonality.pattern[month] || 1);199    });200    const testActuals = effectiveTestData.map(d => d.totalRevenue);201    testMetrics = calculateMetrics(testActuals, testPredictions);202  }203  204  // Generate historical data points205  const historicalData: ForecastPoint[] = monthlyData.map((d, i) => {206    const trendValue = regression.slope * i + regression.intercept;207    const month = d.date.getMonth();208    const predicted = trendValue * (seasonality.pattern[month] || 1);209    const ci = generateConfidenceInterval(predicted, errorStd, 0);210    211    return {212      date: d.date,213      dateStr: d.period,214      predicted,215      lowerBound: ci.lower,216      upperBound: ci.upper,217      isHistorical: true,218      actual: d.totalRevenue,219    };220  });221  222  // Generate forecast data points223  const forecastData: ForecastPoint[] = [];224  const lastDate = monthlyData[monthlyData.length - 1].date;225  const lastIndex = monthlyData.length - 1;226  227  for (let i = 1; i <= forecastMonths; i++) {228    const futureDate = new Date(lastDate);229    futureDate.setMonth(futureDate.getMonth() + i);230    231    const idx = lastIndex + i;232    const trendValue = regression.slope * idx + regression.intercept;233    const month = futureDate.getMonth();234    const predicted = Math.max(0, trendValue * (seasonality.pattern[month] || 1));235    const ci = generateConfidenceInterval(predicted, errorStd, i);236    237    const period = `${futureDate.getFullYear()}-${String(futureDate.getMonth() + 1).padStart(2, '0')}`;238    239    forecastData.push({240      date: futureDate,241      dateStr: period,242      predicted,243      lowerBound: ci.lower,244      upperBound: ci.upper,245      isHistorical: false,246    });247  }248  249  // Determine trend direction250  let trendDirection: 'Increasing' | 'Decreasing' | 'Stable';251  const monthlyGrowthRate = (regression.slope / (revenues.reduce((a, b) => a + b, 0) / revenues.length)) * 100;252  253  if (monthlyGrowthRate > 2) {254    trendDirection = 'Increasing';255  } else if (monthlyGrowthRate < -2) {256    trendDirection = 'Decreasing';257  } else {258    trendDirection = 'Stable';259  }260  261  // Calculate forecast summary262  const lastActualRevenue = revenues[revenues.length - 1];263  const nextMonthPrediction = forecastData[0]?.predicted || 0;264  const sixMonthPrediction = forecastData[5]?.predicted || forecastData[forecastData.length - 1]?.predicted || 0;265  const yearEndPrediction = forecastData[forecastData.length - 1]?.predicted || 0;266  267  const expectedGrowth = lastActualRevenue > 0268    ? ((yearEndPrediction - lastActualRevenue) / lastActualRevenue) * 100269    : 0;270  271  return {272    historicalData,273    forecastData,274    combinedData: [...historicalData, ...forecastData],275    metrics: {276      mae: testMetrics.mae,277      rmse: testMetrics.rmse,278      mape: testMetrics.mape,279      r2: regression.r2,280    },281    trend: {282      direction: trendDirection,283      slope: regression.slope,284      intercept: regression.intercept,285    },286    seasonality: {287      detected: seasonalityDetected,288      pattern: seasonality.pattern,289      peakMonth: seasonality.peakMonth,290      troughMonth: seasonality.troughMonth,291    },292    forecastSummary: {293      nextMonthPrediction,294      sixMonthPrediction,295      yearEndPrediction,296      expectedGrowth,297    },298  };299}300 301/**302 * Get month name from index303 */304export function getMonthName(monthIndex: number): string {305  const months = ['January', 'February', 'March', 'April', 'May', 'June',306                  'July', 'August', 'September', 'October', 'November', 'December'];307  return months[monthIndex] || '';308}309