| | | 1 | | using OzonDomains.Models; |
| | | 2 | | |
| | | 3 | | namespace OzonOrdersWeb.Areas.Studio2.AnalysisServices |
| | | 4 | | { |
| | | 5 | | public class SalesForecastService |
| | | 6 | | { |
| | | 7 | | /// <summary> |
| | | 8 | | /// Прогнозирование продаж на основе исторических данных заказов |
| | | 9 | | /// Использует метод экспоненциального сглаживания с учетом сезонности (день недели) |
| | | 10 | | /// </summary> |
| | | 11 | | public Dictionary<DateTime, decimal> ForecastSales( |
| | | 12 | | List<Order> historicalOrders, |
| | | 13 | | int forecastPeriodDays = 30) |
| | | 14 | | { |
| | | 15 | | // Группируем продажи по дням |
| | 3 | 16 | | var dailySales = historicalOrders |
| | 36 | 17 | | .Where(o => o.ProcessingDate.HasValue) |
| | 35 | 18 | | .GroupBy(o => o.ProcessingDate!.Value.Date) |
| | 35 | 19 | | .Select(g => new |
| | 35 | 20 | | { |
| | 35 | 21 | | Date = g.Key, |
| | 35 | 22 | | TotalSales = g.Sum(o => (o.Price ?? 0) * (o.TrueQuantity ?? o.Quantity ?? 0)), |
| | 35 | 23 | | TotalQuantity = g.Sum(o => o.TrueQuantity ?? o.Quantity ?? 0) |
| | 35 | 24 | | }) |
| | 35 | 25 | | .OrderBy(x => x.Date) |
| | 3 | 26 | | .ToList(); |
| | | 27 | | |
| | 3 | 28 | | var forecast = new Dictionary<DateTime, decimal>(); |
| | | 29 | | |
| | 3 | 30 | | if (!dailySales.Any()) |
| | 2 | 31 | | return forecast; |
| | | 32 | | |
| | 36 | 33 | | var lastDate = dailySales.Max(x => x.Date); |
| | | 34 | | |
| | 12 | 35 | | for (int i = 1; i <= forecastPeriodDays; i++) |
| | | 36 | | { |
| | 5 | 37 | | var forecastDate = lastDate.AddDays(i); |
| | | 38 | | |
| | | 39 | | // Используем среднее значение за последние N дней |
| | 5 | 40 | | int windowSize = 7; // неделя |
| | 5 | 41 | | var recentSales = dailySales |
| | 175 | 42 | | .Where(x => x.Date >= lastDate.AddDays(-windowSize)) |
| | 5 | 43 | | .ToList(); |
| | | 44 | | |
| | 5 | 45 | | if (recentSales.Any()) |
| | | 46 | | { |
| | | 47 | | // Учитываем день недели (сезонность) |
| | 5 | 48 | | var dayOfWeek = forecastDate.DayOfWeek; |
| | 5 | 49 | | var historicalSameDaySales = dailySales |
| | 175 | 50 | | .Where(x => x.Date.DayOfWeek == dayOfWeek) |
| | 5 | 51 | | .TakeLast(4) // Последние 4 недели |
| | 20 | 52 | | .Select(x => x.TotalSales) |
| | 5 | 53 | | .ToList(); |
| | | 54 | | |
| | 5 | 55 | | if (historicalSameDaySales.Any()) |
| | | 56 | | { |
| | | 57 | | // Экспоненциальное сглаживание |
| | 5 | 58 | | double forecastValue = ExponentialSmoothing( |
| | 5 | 59 | | historicalSameDaySales, |
| | 5 | 60 | | alpha: 0.3); |
| | | 61 | | |
| | 5 | 62 | | forecast[forecastDate] = (decimal)forecastValue; |
| | | 63 | | } |
| | | 64 | | } |
| | | 65 | | } |
| | | 66 | | |
| | 1 | 67 | | return forecast; |
| | | 68 | | } |
| | | 69 | | |
| | | 70 | | /// <summary> |
| | | 71 | | /// Экспоненциальное сглаживание временного ряда |
| | | 72 | | /// alpha - параметр сглаживания (0-1), чем больше - тем больше вес последних данных |
| | | 73 | | /// Формула: S_t = α * Y_t + (1-α) * S_{t-1} |
| | | 74 | | /// </summary> |
| | | 75 | | private double ExponentialSmoothing(List<decimal> data, double alpha) |
| | | 76 | | { |
| | 5 | 77 | | if (data.Count == 0) return 0; |
| | | 78 | | |
| | 5 | 79 | | double smoothed = (double)data[0]; |
| | 40 | 80 | | for (int i = 1; i < data.Count; i++) |
| | | 81 | | { |
| | 15 | 82 | | smoothed = alpha * (double)data[i] + (1 - alpha) * smoothed; |
| | | 83 | | } |
| | 5 | 84 | | return smoothed; |
| | | 85 | | } |
| | | 86 | | } |
| | | 87 | | } |
| | | 88 | | |