< Summary

Information
Class: OzonOrdersWeb.Areas.Studio2.AnalysisServices.TrendAnalysisService
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/AnalysisServices/TrendAnalysisService.cs
Line coverage
93%
Covered lines: 123
Uncovered lines: 8
Coverable lines: 131
Total lines: 270
Line coverage: 93.8%
Branch coverage
61%
Covered branches: 38
Total branches: 62
Branch coverage: 61.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_EntityName()100%11100%
get_EntityType()100%11100%
get_Slope()100%11100%
get_Direction()100%11100%
get_R2()100%11100%
get_GrowthPercentage()100%11100%
get_DataPointsCount()100%11100%
AnalyzeProductTrends(...)66.66%1818100%
AnalyzeWarehouseTrends(...)50%44100%
AnalyzeRegionTrends(...)50%4487.5%
AnalyzeTrends(...)87.5%88100%
CalculateLinearRegression(...)80%101090.9%
CalculateGrowthPercentageFromRegression(...)50%8887.5%
ClassifyTrend(...)30%401033.33%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/AnalysisServices/TrendAnalysisService.cs

#LineLine coverage
 1using OzonDomains.Models;
 2
 3namespace OzonOrdersWeb.Areas.Studio2.AnalysisServices
 4{
 5    public class TrendAnalysisService
 6    {
 7        public class TrendResult
 8        {
 59            public string EntityName { get; set; } = string.Empty;
 610            public string EntityType { get; set; } = string.Empty; // product, warehouse, region
 511            public double Slope { get; set; } // коэффициент наклона
 312            public TrendDirection Direction { get; set; }
 313            public double R2 { get; set; } // коэффициент детерминации
 214            public double GrowthPercentage { get; set; } // процент роста
 315            public int DataPointsCount { get; set; } // количество точек данных
 16        }
 17
 18        public enum TrendDirection
 19        {
 20            StrongGrowth,      // Сильный рост
 21            ModerateGrowth,    // Умеренный рост
 22            Stable,            // Стабильно
 23            ModerateDecline,   // Умеренное снижение
 24            StrongDecline      // Сильное снижение
 25        }
 26
 27        /// <summary>
 28        /// Анализ трендов по продуктам используя линейную регрессию
 29        /// Метод наименьших квадратов: y = a + b*x
 30        /// где y - продажи, x - время
 31        /// </summary>
 32        public List<TrendResult> AnalyzeProductTrends(List<Order> orders, int periodDays = 90)
 33        {
 234            var cutoffDate = DateTime.Now.AddDays(-periodDays);
 235            var recentOrders = orders
 2136                .Where(o => o.ProcessingDate >= cutoffDate)
 237                .ToList();
 38
 39            // Группируем по продуктам
 240            var productGroups = recentOrders
 2141                .Where(o => !string.IsNullOrEmpty(o.Article))
 2142                .GroupBy(o => o.CleanedArticle ?? o.Article)
 243                .ToList();
 44
 245            var trends = new List<TrendResult>();
 46
 847            foreach (var productGroup in productGroups)
 48            {
 249                var dailySales = productGroup
 2150                    .Where(o => o.ProcessingDate.HasValue)
 2151                    .GroupBy(o => o.ProcessingDate!.Value.Date)
 2152                    .Select(g => new
 2153                    {
 2154                        Date = g.Key,
 2155                        TotalQuantity = g.Sum(o => o.TrueQuantity ?? o.Quantity ?? 0),
 2156                        DaysFromStart = (g.Key - cutoffDate).TotalDays
 2157                    })
 2158                    .OrderBy(x => x.Date)
 259                    .ToList();
 60
 261                if (dailySales.Count < 10) continue; // Минимум 10 точек для анализа
 62
 63                // Линейная регрессия: y = a + b*x
 164                var (slope, intercept, r2) = CalculateLinearRegression(
 1265                    dailySales.Select(d => (d.DaysFromStart, (double)d.TotalQuantity)).ToList()
 166                );
 67
 68                // Получаем первый и последний день для расчета процента роста
 169                double firstDay = dailySales.First().DaysFromStart;
 170                double lastDay = dailySales.Last().DaysFromStart;
 71
 72                // Получаем название продукта из первого заказа в группе
 173                string productName = productGroup.FirstOrDefault()?.ProductName ?? productGroup.Key ?? string.Empty;
 74
 175                var trendResult = new TrendResult
 176                {
 177                    EntityName = productName,
 178                    EntityType = "product",
 179                    Slope = slope,
 180                    R2 = r2,
 181                    GrowthPercentage = CalculateGrowthPercentageFromRegression(slope, intercept, firstDay, lastDay),
 182                    Direction = ClassifyTrend(slope, r2),
 183                    DataPointsCount = dailySales.Count
 184                };
 85
 186                trends.Add(trendResult);
 87            }
 88
 389            return trends.OrderByDescending(t => Math.Abs(t.Slope)).ToList();
 90        }
 91
 92        /// <summary>
 93        /// Анализ трендов по складам
 94        /// </summary>
 95        public List<TrendResult> AnalyzeWarehouseTrends(List<Order> orders, int periodDays = 90)
 96        {
 197            var cutoffDate = DateTime.Now.AddDays(-periodDays);
 198            var recentOrders = orders
 1099                .Where(o => o.ProcessingDate >= cutoffDate && o.ShipmentWarehouse != null)
 1100                .ToList();
 101
 1102            var warehouseGroups = recentOrders
 10103                .Where(o => !string.IsNullOrEmpty(o.ShipmentWarehouse!.Name))
 10104                .GroupBy(o => o.ShipmentWarehouse!.Name!)
 1105                .ToList();
 106
 1107            return AnalyzeTrends(warehouseGroups, cutoffDate, "warehouse");
 108        }
 109
 110        /// <summary>
 111        /// Анализ трендов по регионам
 112        /// </summary>
 113        public List<TrendResult> AnalyzeRegionTrends(List<Order> orders, int periodDays = 90)
 114        {
 1115            var cutoffDate = DateTime.Now.AddDays(-periodDays);
 1116            var recentOrders = orders
 10117                .Where(o => o.ProcessingDate >= cutoffDate && !string.IsNullOrEmpty(o.DeliveryCity))
 1118                .ToList();
 119
 1120            var regionGroups = recentOrders
 0121                .GroupBy(o => o.DeliveryCity!)
 1122                .ToList();
 123
 1124            return AnalyzeTrends(regionGroups, cutoffDate, "region");
 125        }
 126
 127        private List<TrendResult> AnalyzeTrends(
 128            List<IGrouping<string, Order>> groups,
 129            DateTime cutoffDate,
 130            string entityType)
 131        {
 2132            var trends = new List<TrendResult>();
 133
 6134            foreach (var group in groups)
 135            {
 1136                var dailySales = group
 10137                    .Where(o => o.ProcessingDate.HasValue)
 10138                    .GroupBy(o => o.ProcessingDate!.Value.Date)
 10139                    .Select(g => new
 10140                    {
 10141                        Date = g.Key,
 10142                        TotalQuantity = g.Sum(o => o.TrueQuantity ?? o.Quantity ?? 0),
 10143                        DaysFromStart = (g.Key - cutoffDate).TotalDays
 10144                    })
 10145                    .OrderBy(x => x.Date)
 1146                    .ToList();
 147
 1148                if (dailySales.Count < 10) continue;
 149
 1150                var (slope, intercept, r2) = CalculateLinearRegression(
 10151                    dailySales.Select(d => (d.DaysFromStart, (double)d.TotalQuantity)).ToList()
 1152                );
 153
 154                // Получаем первый и последний день для расчета процента роста
 1155                double firstDay = dailySales.First().DaysFromStart;
 1156                double lastDay = dailySales.Last().DaysFromStart;
 157
 1158                var trendResult = new TrendResult
 1159                {
 1160                    EntityName = group.Key,
 1161                    EntityType = entityType,
 1162                    Slope = slope,
 1163                    R2 = r2,
 1164                    GrowthPercentage = CalculateGrowthPercentageFromRegression(slope, intercept, firstDay, lastDay),
 1165                    Direction = ClassifyTrend(slope, r2),
 1166                    DataPointsCount = dailySales.Count
 1167                };
 168
 1169                trends.Add(trendResult);
 170            }
 171
 3172            return trends.OrderByDescending(t => Math.Abs(t.Slope)).ToList();
 173        }
 174
 175        /// <summary>
 176        /// Расчет линейной регрессии методом наименьших квадратов
 177        /// Формулы:
 178        /// slope (b) = (n*Σ(xy) - Σx*Σy) / (n*Σ(x²) - (Σx)²)
 179        /// intercept (a) = (Σy - b*Σx) / n
 180        /// R² = 1 - (SS_res / SS_tot)
 181        /// где SS_res = Σ(y - ŷ)², SS_tot = Σ(y - ȳ)²
 182        /// </summary>
 183        private (double slope, double intercept, double r2) CalculateLinearRegression(
 184            List<(double x, double y)> points)
 185        {
 2186            int n = points.Count;
 2187            if (n < 2)
 0188                return (0, 0, 0);
 189
 8190            double sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
 191
 48192            foreach (var (x, y) in points)
 193            {
 22194                sumX += x;
 22195                sumY += y;
 22196                sumXY += x * y;
 22197                sumX2 += x * x;
 198            }
 199
 200            // Коэффициенты линейной регрессии
 2201            double denominator = n * sumX2 - sumX * sumX;
 2202            if (Math.Abs(denominator) < 0.0001)
 0203                return (0, 0, 0);
 204
 2205            double slope = (n * sumXY - sumX * sumY) / denominator;
 2206            double intercept = (sumY - slope * sumX) / n;
 207
 208            // Коэффициент детерминации R²
 2209            double meanY = sumY / n;
 4210            double ssTotal = 0, ssResidual = 0;
 211
 48212            foreach (var (x, y) in points)
 213            {
 22214                double predicted = intercept + slope * x;
 22215                ssTotal += Math.Pow(y - meanY, 2);
 22216                ssResidual += Math.Pow(y - predicted, 2);
 217            }
 218
 2219            double r2 = ssTotal > 0 ? 1 - (ssResidual / ssTotal) : 0;
 220
 2221            return (slope, intercept, r2);
 222        }
 223
 224        /// <summary>
 225        /// Расчет процента роста на основе линейной регрессии
 226        /// Использует прогнозируемые значения на начало и конец периода вместо фактических
 227        /// Формула: ((прогноз_конец - прогноз_начало) / прогноз_начало) * 100
 228        /// </summary>
 229        private double CalculateGrowthPercentageFromRegression(double slope, double intercept, double firstDay, double l
 230        {
 231            // Рассчитываем прогнозируемые значения на основе линейной регрессии
 232            // y = intercept + slope * x
 2233            double predictedFirst = intercept + slope * firstDay;
 2234            double predictedLast = intercept + slope * lastDay;
 235
 236            // Избегаем деления на ноль и отрицательных значений
 2237            if (predictedFirst <= 0)
 238            {
 239                // Если прогноз на начало <= 0, используем абсолютное изменение
 0240                return predictedLast > 0 ? 100 : 0;
 241            }
 242
 243            // Рассчитываем процент роста
 2244            double growthPercentage = ((predictedLast - predictedFirst) / predictedFirst) * 100;
 245
 246            // Ограничиваем экстремальные значения для читаемости
 3247            if (growthPercentage > 1000) growthPercentage = 1000;
 2248            if (growthPercentage < -100) growthPercentage = -100;
 249
 2250            return growthPercentage;
 251        }
 252
 253        /// <summary>
 254        /// Классификация тренда на основе коэффициента наклона и R²
 255        /// R² показывает качество модели (0-1, где 1 - идеальная модель)
 256        /// </summary>
 257        private TrendDirection ClassifyTrend(double slope, double r2)
 258        {
 3259            if (r2 < 0.3) return TrendDirection.Stable; // Низкая корреляция
 260
 2261            if (slope > 0.5) return TrendDirection.StrongGrowth;
 0262            if (slope > 0.1) return TrendDirection.ModerateGrowth;
 0263            if (slope < -0.5) return TrendDirection.StrongDecline;
 0264            if (slope < -0.1) return TrendDirection.ModerateDecline;
 265
 0266            return TrendDirection.Stable;
 267        }
 268    }
 269}
 270