| | | 1 | | using OzonOrdersWeb.Areas.Studio2.DTOs; |
| | | 2 | | |
| | | 3 | | namespace OzonOrdersWeb.Areas.Studio2.AnalysisServices; |
| | | 4 | | |
| | | 5 | | /// <summary>Русские подписи и DTO для вкладки «Прогнозирование и тренды».</summary> |
| | | 6 | | public static class ForecastDtoMapper |
| | | 7 | | { |
| | 0 | 8 | | public static string DayOfWeekName(DayOfWeek day) => day switch |
| | 0 | 9 | | { |
| | 0 | 10 | | DayOfWeek.Monday => "Понедельник", |
| | 0 | 11 | | DayOfWeek.Tuesday => "Вторник", |
| | 0 | 12 | | DayOfWeek.Wednesday => "Среда", |
| | 0 | 13 | | DayOfWeek.Thursday => "Четверг", |
| | 0 | 14 | | DayOfWeek.Friday => "Пятница", |
| | 0 | 15 | | DayOfWeek.Saturday => "Суббота", |
| | 0 | 16 | | DayOfWeek.Sunday => "Воскресенье", |
| | 0 | 17 | | _ => day.ToString() |
| | 0 | 18 | | }; |
| | | 19 | | |
| | 0 | 20 | | public static string DirectionName(TrendAnalysisService.TrendDirection direction) => direction switch |
| | 0 | 21 | | { |
| | 0 | 22 | | TrendAnalysisService.TrendDirection.StrongGrowth => "Сильный рост", |
| | 0 | 23 | | TrendAnalysisService.TrendDirection.ModerateGrowth => "Умеренный рост", |
| | 0 | 24 | | TrendAnalysisService.TrendDirection.Stable => "Стабильно", |
| | 0 | 25 | | TrendAnalysisService.TrendDirection.ModerateDecline => "Умеренное снижение", |
| | 0 | 26 | | TrendAnalysisService.TrendDirection.StrongDecline => "Сильное снижение", |
| | 0 | 27 | | _ => "Неизвестно" |
| | 0 | 28 | | }; |
| | | 29 | | |
| | | 30 | | public static string TrendDescription(TrendAnalysisService.TrendResult trend) |
| | | 31 | | { |
| | 0 | 32 | | var direction = trend.Direction switch |
| | 0 | 33 | | { |
| | 0 | 34 | | TrendAnalysisService.TrendDirection.StrongGrowth => "сильный рост", |
| | 0 | 35 | | TrendAnalysisService.TrendDirection.ModerateGrowth => "умеренный рост", |
| | 0 | 36 | | TrendAnalysisService.TrendDirection.Stable => "стабильность", |
| | 0 | 37 | | TrendAnalysisService.TrendDirection.ModerateDecline => "умеренное снижение", |
| | 0 | 38 | | TrendAnalysisService.TrendDirection.StrongDecline => "сильное снижение", |
| | 0 | 39 | | _ => "неопределенный тренд" |
| | 0 | 40 | | }; |
| | | 41 | | |
| | 0 | 42 | | return $"Наблюдается {direction} ({trend.GrowthPercentage:F1}% за период). " + |
| | 0 | 43 | | $"Коэффициент детерминации R² = {trend.R2:F3} " + |
| | 0 | 44 | | $"(качество модели: {(trend.R2 > 0.7 ? "высокое" : trend.R2 > 0.4 ? "среднее" : "низкое")}). " + |
| | 0 | 45 | | $"Анализ основан на {trend.DataPointsCount} точках данных."; |
| | | 46 | | } |
| | | 47 | | |
| | 0 | 48 | | public static string PriorityName(PurchaseRecommendationService.RecommendationPriority priority) => priority switch |
| | 0 | 49 | | { |
| | 0 | 50 | | PurchaseRecommendationService.RecommendationPriority.Critical => "Критический", |
| | 0 | 51 | | PurchaseRecommendationService.RecommendationPriority.High => "Высокий", |
| | 0 | 52 | | PurchaseRecommendationService.RecommendationPriority.Medium => "Средний", |
| | 0 | 53 | | PurchaseRecommendationService.RecommendationPriority.Low => "Низкий", |
| | 0 | 54 | | _ => "Неизвестно" |
| | 0 | 55 | | }; |
| | | 56 | | |
| | 0 | 57 | | public static string AbcDescription(char abc) => abc switch |
| | 0 | 58 | | { |
| | 0 | 59 | | 'A' => "Класс A: 20% продуктов, обеспечивающих 80% продаж (высокая важность)", |
| | 0 | 60 | | 'B' => "Класс B: 30% продуктов, обеспечивающих 15% продаж (средняя важность)", |
| | 0 | 61 | | 'C' => "Класс C: 50% продуктов, обеспечивающих 5% продаж (низкая важность)", |
| | 0 | 62 | | _ => "Неизвестный класс" |
| | 0 | 63 | | }; |
| | | 64 | | |
| | 0 | 65 | | public static string XyzDescription(char xyz) => xyz switch |
| | 0 | 66 | | { |
| | 0 | 67 | | 'X' => "Класс X: Стабильный спрос (коэффициент вариации < 10%)", |
| | 0 | 68 | | 'Y' => "Класс Y: Умеренно изменчивый спрос (коэффициент вариации 10-25%)", |
| | 0 | 69 | | 'Z' => "Класс Z: Нестабильный спрос (коэффициент вариации > 25%)", |
| | 0 | 70 | | _ => "Неизвестный класс" |
| | 0 | 71 | | }; |
| | | 72 | | |
| | 0 | 73 | | private static TrendAnalysisDto Trend(TrendAnalysisService.TrendResult t) => new() |
| | 0 | 74 | | { |
| | 0 | 75 | | EntityName = t.EntityName, |
| | 0 | 76 | | EntityType = t.EntityType, |
| | 0 | 77 | | Slope = t.Slope, |
| | 0 | 78 | | Direction = DirectionName(t.Direction), |
| | 0 | 79 | | R2 = t.R2, |
| | 0 | 80 | | GrowthPercentage = t.GrowthPercentage, |
| | 0 | 81 | | DataPointsCount = t.DataPointsCount, |
| | 0 | 82 | | TrendDescription = TrendDescription(t) |
| | 0 | 83 | | }; |
| | | 84 | | |
| | | 85 | | /// <summary>Прогноз, тренды и рекомендации по заказам-заглушкам (<see cref="AnalyticsRowProjection.ToOrders"/>).</s |
| | | 86 | | public static ForecastTrendAnalysisDto Build(List<OzonDomains.Models.Order> orders, int forecastDays, |
| | | 87 | | SalesForecastService forecast, TrendAnalysisService trends, PurchaseRecommendationService purchases) |
| | | 88 | | { |
| | | 89 | | // Остатки из системы учёта пока не подключены — как и прежде, рекомендации считаются без остатков. |
| | 0 | 90 | | var recommendations = purchases.GenerateRecommendations(orders, new Dictionary<string, int>(), leadTimeDays: 7, |
| | 0 | 91 | | return new ForecastTrendAnalysisDto |
| | 0 | 92 | | { |
| | 0 | 93 | | SalesForecast = forecast.ForecastSales(orders, forecastDays) |
| | 0 | 94 | | .Select(kvp => new SalesForecastDto { Date = kvp.Key, ForecastedSales = kvp.Value, DayOfWeek = DayOfWeek |
| | 0 | 95 | | ProductTrends = trends.AnalyzeProductTrends(orders).Take(20).Select(Trend).ToList(), |
| | 0 | 96 | | WarehouseTrends = trends.AnalyzeWarehouseTrends(orders).Take(10).Select(Trend).ToList(), |
| | 0 | 97 | | RegionTrends = trends.AnalyzeRegionTrends(orders).Take(20).Select(Trend).ToList(), |
| | 0 | 98 | | PurchaseRecommendations = recommendations.Take(20).Select(r => new PurchaseRecommendationDto |
| | 0 | 99 | | { |
| | 0 | 100 | | Article = r.Article, |
| | 0 | 101 | | ProductName = r.ProductName, |
| | 0 | 102 | | CurrentStock = r.CurrentStock, |
| | 0 | 103 | | RecommendedQuantity = r.RecommendedQuantity, |
| | 0 | 104 | | RecommendedOrderDate = r.RecommendedOrderDate, |
| | 0 | 105 | | ServiceLevel = r.ServiceLevel, |
| | 0 | 106 | | Priority = PriorityName(r.Priority), |
| | 0 | 107 | | Reason = r.Reason, |
| | 0 | 108 | | ABCClass = r.ABCClass, |
| | 0 | 109 | | XYZClass = r.XYZClass, |
| | 0 | 110 | | AvgDailyDemand = r.AvgDailyDemand, |
| | 0 | 111 | | SafetyStock = r.SafetyStock, |
| | 0 | 112 | | ReorderPoint = r.ReorderPoint, |
| | 0 | 113 | | ABCDescription = AbcDescription(r.ABCClass), |
| | 0 | 114 | | XYZDescription = XyzDescription(r.XYZClass) |
| | 0 | 115 | | }).ToList() |
| | 0 | 116 | | }; |
| | | 117 | | } |
| | | 118 | | } |