Учебник. Прогнозирование спроса для службы проката велосипедов с помощью анализа временных рядов и ML.NET

Сведения о том, как прогнозировать спрос для службы проката велосипедов с помощью одномерного анализа временных рядов по данным, которые хранятся в базе данных SQL Server, с применением ML.NET.

В этом руководстве вы узнаете, как:

  • Определение проблемы
  • загружать данные из базы данных;
  • создавать модели прогнозирования;
  • выполнять оценку модели прогнозирования;
  • сохранять модели прогнозирования;
  • применять модели прогнозирования.

Предварительные требования

  • Visual Studio 2022 с установленной рабочей нагрузкой "Разработка классических приложений .NET".

Общие сведения о примере прогнозирования по временным рядам

Этот пример содержит консольное приложение .NET Core на языке C# , которое прогнозирует спрос для службы проката велосипедов с помощью алгоритма одномерного анализа временных рядов, называемого анализом сингулярного спектра. Код для этого примера можно найти в репозитории dotnet/machinelearning-samples на сайте GitHub.

Определение проблемы

Для эффективной работы крайне важную роль играет правильное управление запасами. Слишком большой запас товаров означает, что значительная его часть хранится на полках и не приносит дохода. Слишком малый запас приводит к потере продаж, а клиенты уходят к конкурентам. Это означает, что постоянно актуален вопрос об оптимальном уровне складских запасов. Анализ временных рядов помогает ответить на этот вопрос путем изучения исторических данных, выявления закономерностей и прогнозирования на их основе значений на некоторый период вперед.

Для анализа данных в этом руководстве используется метод одномерного анализа временных рядов. В ходе одномерного анализа временных рядов просматриваются значения одного численного параметра, измеряемого через определенные интервалы на протяжении некоторого периода времени, например ежемесячные данные о продажах.

В этом руководстве применяется алгоритм Анализ сингулярного спектра (SSA). SSA разбивает временной ряд на набор основных компонентов. Эти компоненты можно интерпретировать как части одного сигнала: тенденции, шумы, сезонные колебания и многие другие факторы. Полученные компоненты реконструируются и применяются для прогнозирования значений на будущие периоды.

Создание консольного приложения

  1. Создайте консольное приложение C# с именем BikeDemandForecasting. Нажмите кнопку Далее.

  2. Выберите .NET 6 в качестве используемой платформы. Нажмите кнопку Создать .

  3. Установите пакет NuGet Microsoft.ML.

    Примечание

    В этом примере используется последняя стабильная версия пакетов NuGet, упомянутых выше, если не указано иное.

    1. В обозревателе решений щелкните проект правой кнопкой мыши и выберите Управление пакетами NuGet.
    2. Выберите nuget.org в качестве источника пакета, щелкните вкладку Обзор и найдите Microsoft.ML.
    3. Установите флажок Включить предварительные версии.
    4. Нажмите кнопку Установить.
    5. Нажмите кнопку ОК в диалоговом окне Предварительный просмотр изменений. Затем нажмите кнопку Принимаю в диалоговом окне "Принятие условий лицензионного соглашения", если вы согласны с условиями лицензионного соглашения для выбранных пакетов.
    6. Повторите эти действия для System.Data.SqlClient и Microsoft.ML.TimeSeries.

Подготовка и анализ данных

  1. Создайте каталог с именем Data.
  2. Скачайте файл базы данных DailyDemand.mdf и сохраните его в каталоге Data.

Примечание

Данные, используемые в этом руководстве, получены из набора данных UCI по аренде велосипедов. Хади Фанаит (Hadi Fanaee-T) и Жуан Гама (Joao Gama), 'Event labeling combining ensemble detectors and background knowledge' (Маркировка событий по сочетанию множества датчиков и предварительных знаний), Progress in Artificial Intelligence (2013): стр. 1–15, Springer Berlin Heidelberg, ссылка на веб-страницу.

Исходный набор данных содержит несколько столбцов, соответствующих сезонности и погоде. Для сокращения объема, поскольку для алгоритма в этом руководстве нужны значения только из одного числового столбца, мы уплотнили исходный набор данных, сохранив только следующие столбцы:

  • dteday: дата наблюдения.
  • year: закодированный год наблюдения (0 = 2011, 1 = 2012).
  • cnt: общее число арендованных велосипедов за этот день.

Исходный набор данных сопоставляется с таблицей базы данных в базе данных SQL Server, которая имеет следующую схему.

CREATE TABLE [Rentals] (
    [RentalDate] DATE NOT NULL,
    [Year] INT NOT NULL,
    [TotalRentals] INT NOT NULL
);

Ниже приводится пример этих данных.

RentalDate Год TotalRentals
1/1/2011 0 985
1/2/2011 0 801
1/3/2011 0 1349

Создание классов входных и выходных данных

  1. Откройте файл Program.cs и замените существующие инструкции using следующим кодом.

    using Microsoft.ML;
    using Microsoft.ML.Data;
    using Microsoft.ML.Transforms.TimeSeries;
    using System.Data.SqlClient;
    
  2. Создайте класс ModelInput. Добавьте приведенный ниже код в класс Program.

    public class ModelInput
    {
        public DateTime RentalDate { get; set; }
    
        public float Year { get; set; }
    
        public float TotalRentals { get; set; }
    }
    

    Класс ModelInput содержит следующие столбцы:

    • RentalDate: дата наблюдения.
    • Year: закодированный год наблюдения (0 = 2011, 1 = 2012).
    • TotalRentals: общее число арендованных велосипедов за этот день.
  3. Создайте класс ModelOutput под только что созданным классом ModelInput.

    public class ModelOutput
    {
        public float[] ForecastedRentals { get; set; }
    
        public float[] LowerBoundRentals { get; set; }
    
        public float[] UpperBoundRentals { get; set; }
    }
    

    Класс ModelOutput содержит следующие столбцы:

    • ForecastedRentals: прогнозируемые значения для прогнозируемого периода.
    • LowerBoundRentals: минимальные прогнозируемые значения для прогнозируемого периода.
    • UpperBoundRentals: максимальные прогнозируемые значения для прогнозируемого периода.

Определение путей и инициализация переменных

  1. Под операторами using определите переменные, которые будут хранить расположение данных, строку подключения и расположение для обученной модели.

    string rootDir = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../../"));
    string dbFilePath = Path.Combine(rootDir, "Data", "DailyDemand.mdf");
    string modelPath = Path.Combine(rootDir, "MLModel.zip");
    var connectionString = $"Data Source=(LocalDB)\\MSSQLLocalDB;AttachDbFilename={dbFilePath};Integrated Security=True;Connect Timeout=30;";
    
  2. Инициализируйте переменную mlContext, используя новый экземпляр MLContext, добавив следующую строку после определения путей.

    MLContext mlContext = new MLContext();
    

    Класс MLContext будет стартовой точкой для любых операций ML.NET. При инициализации класса mlContext создается новая среда ML.NET, которая может использоваться всеми объектами в рабочем процессе создания модели. По существу он аналогичен классу DBContext в Entity Framework.

Загрузка данных

  1. Создайте DatabaseLoader, который загружает записи с типом ModelInput.

    DatabaseLoader loader = mlContext.Data.CreateDatabaseLoader<ModelInput>();
    
  2. Определите запрос для загрузки данных из базы данных.

    string query = "SELECT RentalDate, CAST(Year as REAL) as Year, CAST(TotalRentals as REAL) as TotalRentals FROM Rentals";
    

    Алгоритмы ML.NET предполагают, что данные имеют тип Single. Следовательно, полученные из базы данных числовые значения, тип которых отличается от Real (значение одиночной точности с плавающей запятой), необходимо преобразовать в Real.

    Столбцы Year и TotalRental в базе данных имеют целочисленные типы. С помощью встроенной функции CAST они приводятся к типу Real.

  3. Создайте DatabaseSource для подключения к базе данных и выполните запрос.

    DatabaseSource dbSource = new DatabaseSource(SqlClientFactory.Instance,
                                    connectionString,
                                    query);
    
  4. Загрузите данные в IDataView.

    IDataView dataView = loader.Load(dbSource);
    
  5. Набор данных содержит данные за два года. Для обучения используются только данные за первый год, а данные за второй год откладываются для сравнения фактических значений с прогнозом, созданным моделью. Отфильтруйте данные с помощью преобразования FilterRowsByColumn.

    IDataView firstYearData = mlContext.Data.FilterRowsByColumn(dataView, "Year", upperBound: 1);
    IDataView secondYearData = mlContext.Data.FilterRowsByColumn(dataView, "Year", lowerBound: 1);
    

    Для отбора данных за первый год извлекаются те строки, у которых значение в столбце Year меньше 1. Для этого параметру upperBound присваивается значение 1. И наоборот, для данных за второй год извлекаются те строки, у которых это значение равно 1. Для этого параметру lowerBound присваивается значение 1.

Определение конвейера анализа временных рядов

  1. Определите конвейер, который использует SsaForecastingEstimator для прогнозирования значений в наборе данных временных рядов.

    var forecastingPipeline = mlContext.Forecasting.ForecastBySsa(
        outputColumnName: "ForecastedRentals",
        inputColumnName: "TotalRentals",
        windowSize: 7,
        seriesLength: 30,
        trainSize: 365,
        horizon: 7,
        confidenceLevel: 0.95f,
        confidenceLowerBoundColumn: "LowerBoundRentals",
        confidenceUpperBoundColumn: "UpperBoundRentals");
    

    forecastingPipeline принимает 365 точек данных за первый год и разделяет набор данных временных рядов на выборки за 30-дневные (помесячные) интервалы, как указано в параметре seriesLength. Каждая из этих выборок анализируется по еженедельным (7-дневным) периодам. При определении прогнозируемого значения для предстоящих периодов используются значения за предыдущие семь дней. Модель настроена на прогнозирование семи предстоящих периодов, как определено параметром horizon. Прогноз является лишь предположением, то есть не всегда будет на 100 % точным. Поэтому нам лучше получить диапазон значений, который определяется верхней и нижней границами для наилучшего и наихудшего сценариев. В нашем примере для определения нижней и верхней границ устанавливается уровень достоверности 95 %. Вы можете увеличить или уменьшить этот уровень достоверности. Чем выше значение уровня достоверности, тем шире диапазон между верхней и нижней границами.

  2. Используйте метод Fit, чтобы обучить модель и подогнать данные по ранее определенным forecastingPipeline.

    SsaForecastingTransformer forecaster = forecastingPipeline.Fit(firstYearData);
    

Оценка модели

Оцените точность модели, создав прогноз данных на следующий год и сравнив их с фактическими значениями.

  1. Создайте новый служебный метод с именем Evaluate в конце файла Program.cs.

    Evaluate(IDataView testData, ITransformer model, MLContext mlContext)
    {
    
    }
    
  2. В методе Evaluate примените метод Transform с обученной моделью, чтобы спрогнозировать данные на следующий год.

    IDataView predictions = model.Transform(testData);
    
  3. Чтобы получить фактические значения из примера данных, используйте метод CreateEnumerable.

    IEnumerable<float> actual =
        mlContext.Data.CreateEnumerable<ModelInput>(testData, true)
            .Select(observed => observed.TotalRentals);
    
  4. Чтобы получить прогнозируемые значения, используйте метод CreateEnumerable.

    IEnumerable<float> forecast =
        mlContext.Data.CreateEnumerable<ModelOutput>(predictions, true)
            .Select(prediction => prediction.ForecastedRentals[0]);
    
  5. Вычислите разницу между реальными и прогнозируемыми значениями, которая обычно называется ошибкой.

    var metrics = actual.Zip(forecast, (actualValue, forecastValue) => actualValue - forecastValue);
    
  6. Для оценки точности модели вычислите среднюю абсолютную погрешность и среднеквадратическую погрешность.

    var MAE = metrics.Average(error => Math.Abs(error)); // Mean Absolute Error
    var RMSE = Math.Sqrt(metrics.Average(error => Math.Pow(error, 2))); // Root Mean Squared Error
    

    Для оценки точности используются следующие метрики.

    • Средняя абсолютная погрешность. Оценивает, насколько близки прогнозы к фактическому значению. Принимает значения в диапазоне от 0 до бесконечности. Чем ближе это значение к 0, тем лучше качество модели.
    • Среднеквадратическая погрешность. Суммирует ошибки в модели. Принимает значения в диапазоне от 0 до бесконечности. Чем ближе это значение к 0, тем лучше качество модели.
  7. Выведите значения метрик на консоль.

    Console.WriteLine("Evaluation Metrics");
    Console.WriteLine("---------------------");
    Console.WriteLine($"Mean Absolute Error: {MAE:F3}");
    Console.WriteLine($"Root Mean Squared Error: {RMSE:F3}\n");
    
  8. Вызовите метод Evaluate под вызовом метода Fit().

    Evaluate(secondYearData, forecaster, mlContext);
    

Сохранение модели

Если вы довольны поведением модели, сохраните ее для использования в других приложениях.

  1. Под методом Evaluate() создайте TimeSeriesPredictionEngine. TimeSeriesPredictionEngine — это удобный метод для создания единичных прогнозов.

    var forecastEngine = forecaster.CreateTimeSeriesEngine<ModelInput, ModelOutput>(mlContext);
    
  2. Сохраните модель в файл с именем MLModel.zip, которое задано ранее определенной переменной modelPath. Выполните метод Checkpoint, чтобы сохранить модель.

    forecastEngine.CheckPoint(mlContext, modelPath);
    

Применение модели для прогнозирования спроса

  1. Добавьте новый служебный метод Forecast под методом Evaluate.

    void Forecast(IDataView testData, int horizon, TimeSeriesPredictionEngine<ModelInput, ModelOutput> forecaster, MLContext mlContext)
    {
    
    }
    
  2. В методе Forecast вызовите метод Predict, чтобы получить прогноз по прокату на следующие семь дней.

    ModelOutput forecast = forecaster.Predict();
    
  3. Сравните фактические и прогнозируемые значения по семи периодам.

    IEnumerable<string> forecastOutput =
        mlContext.Data.CreateEnumerable<ModelInput>(testData, reuseRowObject: false)
            .Take(horizon)
            .Select((ModelInput rental, int index) =>
            {
                string rentalDate = rental.RentalDate.ToShortDateString();
                float actualRentals = rental.TotalRentals;
                float lowerEstimate = Math.Max(0, forecast.LowerBoundRentals[index]);
                float estimate = forecast.ForecastedRentals[index];
                float upperEstimate = forecast.UpperBoundRentals[index];
                return $"Date: {rentalDate}\n" +
                $"Actual Rentals: {actualRentals}\n" +
                $"Lower Estimate: {lowerEstimate}\n" +
                $"Forecast: {estimate}\n" +
                $"Upper Estimate: {upperEstimate}\n";
            });
    
  4. В цикле извлекайте выходные данные прогноза и выводите их на консоль.

    Console.WriteLine("Rental Forecast");
    Console.WriteLine("---------------------");
    foreach (var prediction in forecastOutput)
    {
        Console.WriteLine(prediction);
    }
    

Запуск приложения

  1. Вызовите метод Forecast под вызовом метода Checkpoint().

    Forecast(secondYearData, 7, forecastEngine, mlContext);
    
  2. Запустите приложение. Должен отобразиться результат, аналогичный приведенному ниже. Для краткости выходные данные были сжаты.

    Evaluation Metrics
    ---------------------
    Mean Absolute Error: 726.416
    Root Mean Squared Error: 987.658
    
    Rental Forecast
    ---------------------
    Date: 1/1/2012
    Actual Rentals: 2294
    Lower Estimate: 1197.842
    Forecast: 2334.443
    Upper Estimate: 3471.044
    
    Date: 1/2/2012
    Actual Rentals: 1951
    Lower Estimate: 1148.412
    Forecast: 2360.861
    Upper Estimate: 3573.309
    

Сверка фактических и прогнозируемых значений показывает следующие связи:

Actual vs Forecast Comparison

Хотя прогнозируемые значения и не дают точного совпадения с реальными данными о прокате, они предоставляют достаточно узкий диапазон значений, который позволяет оптимизировать использование ресурсов организации.

Поздравляем! Вы успешно создали модель машинного обучения для прогнозирования спроса на прокат велосипедов.

Исходный код для этого руководства можно найти в репозитории dotnet/machinelearning-samples.

Следующие шаги