Tutorial: prever a demanda de um serviço de aluguel de bicicletas com análises de série temporal e o ML.NET

Saiba como prever a demanda por um serviço de aluguel de bicicletas usando análises univariadas de série temporal nos dados armazenados em um banco de dados SQL Server com o ML.NET.

Neste tutorial, você aprenderá a:

  • Compreender o problema
  • Carregar dados de um banco de dados
  • Criar um modelo de previsão
  • Avaliar um modelo de previsão
  • Salvar um modelo de previsão
  • Usar um modelo de previsão

Pré-requisitos

  • Visual Studio 2022 com a carga de trabalho "Desenvolvimento de área de trabalho do .NET" instalada.

Visão geral do exemplo de previsão de série temporal

Esse exemplo é um aplicativo de console do .NET Core em C# que prevê a demanda do aluguel de bicicletas usando um algoritmo de análise univariada de série temporal conhecido como análise de espectro singular. O código para este exemplo pode ser encontrado no repositório dotnet/machinelearning-samples no GitHub.

Compreender o problema

Para executar uma operação eficiente, o gerenciamento de estoque desempenha um papel fundamental. Ter muito de um produto em estoque significa que os produtos não vendidos nas prateleiras não geram receita. Ter pouco de um produto em estoque resulta em vendas perdidas e clientes que compram em concorrentes. Portanto, a pergunta constante é: qual é a quantidade ideal de estoque para manter disponível? A análise de série temporal ajuda a fornecer uma resposta para essas perguntas examinando dados históricos, identificando padrões e usando essas informações para prever valores em algum momento no futuro.

A técnica de análise de dados usada neste tutorial é a análise univariada de série temporal. A análise univariada de série temporal examina uma única observação numérica durante um período de tempo em intervalos específicos, como vendas mensais.

O algoritmo usado neste tutorial é SSA (análise de espectro singular). O algoritmo SSA funciona decompondo uma série temporal em um conjunto de componentes principais. Esses componentes podem ser interpretados como as partes de um sinal que correspondem a tendências, ruído, sazonalidade e muitos outros fatores. Em seguida, esses componentes são reconstruídos e usados para prever valores em algum momento no futuro.

Criar um aplicativo de console

  1. Crie um aplicativo de console em C# chamado "BikeDemandForecasting". Clique no botão Avançar.

  2. Escolha o .NET 6 como a estrutura a ser usada. Selecione o botão Criar.

  3. Instale o pacote NuGet da versão Microsoft.ML

    Observação

    Este exemplo usa a versão estável mais recente dos pacotes NuGet mencionados, salvo indicação em contrário.

    1. No Gerenciador de Soluções, clique com o botão direito do mouse no seu projeto e selecione Gerenciar Pacotes NuGet.
    2. Escolha "nuget.org" como a origem do pacote, clique na guia Procurar e pesquise Microsoft.ML.
    3. Marque a caixa de seleção Incluir pré-lançamento.
    4. Selecione o botão Instalar.
    5. Selecione o botão OK na caixa de diálogo Visualizar Alterações e selecione o botão Aceito na caixa de diálogo Aceitação da Licença, se concordar com o termos de licença para os pacotes listados.
    6. Repita as etapas para System.Data.SqlClient e Microsoft.ML.TimeSeries.

Preparar e compreender os dados

  1. Crie um diretório chamado Data.
  2. Baixe o arquivo de banco de dados DailyDemand.mdf e salve-o no diretório Data.

Observação

Os dados usados neste tutorial são provenientes do conjunto de dados UCI Bike Sharing. Link da Web para "Event labeling combining ensemble detectors and background knowledge", de "Progress in Artificial Intelligence" (2013): páginas 1 a 15, Springer Berlin Heidelberg, por Hadi Fanaee-T e João Gama.

O conjunto de dados original contém diversas colunas correspondentes à sazonalidade e ao clima. Por questões de brevidade e porque o algoritmo usado neste tutorial requer somente os valores de uma única coluna numérica, o conjunto de dados original foi condensado para incluir somente as seguintes colunas:

  • dteday: a data da observação.
  • year: o ano codificado da observação (0=2011, 1=2012).
  • cnt: o número total de aluguéis de bicicletas no dia.

O conjunto de dados original é mapeado para uma tabela de banco de dados com o esquema a seguir em um banco de dados SQL Server.

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

Veja o seguinte exemplo dos dados:

RentalDate Year TotalRentals
1/1/2011 0 985
2/1/2011 0 801
3/1/2011 0 1349

Criar classes de entrada e saída

  1. Abra o arquivo Program.cs e substitua as instruções using existentes pelo seguinte:

    using Microsoft.ML;
    using Microsoft.ML.Data;
    using Microsoft.ML.Transforms.TimeSeries;
    using System.Data.SqlClient;
    
  2. Crie a classe ModelInput. Abaixo da classe Program, adicione o código a seguir.

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

    A classe ModelInput contém as seguintes colunas:

    • RentalDate: a data da observação.
    • Year: o ano codificado da observação (0=2011, 1=2012).
    • TotalRentals: o número total de aluguéis de bicicletas no dia.
  3. Crie uma classe ModelOutput abaixo da classe ModelInput recém-criada.

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

    A classe ModelOutput contém as seguintes colunas:

    • ForecastedRentals: os valores previstos para o período previsto.
    • LowerBoundRentals: os valores mínimos previstos para o período previsto.
    • UpperBoundRentals: os valores máximos previstos para o período previsto.

Definir caminhos e inicializar variáveis

  1. Abaixo das instruções de uso, defina variáveis para armazenar a localização de seus dados, a cadeia de conexão e onde salvar o modelo treinado.

    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. Inicialize a variável mlContext com uma nova instância de MLContext adicionando a linha a seguir depois de definir os caminhos.

    MLContext mlContext = new MLContext();
    

    A classe MLContext é um ponto de partida para todas as operações do ML.NET, e a inicialização de mlContext cria um novo ambiente do ML.NET que pode ser compartilhado entre os objetos do fluxo de trabalho de criação de modelo. Ele é semelhante, conceitualmente, a DBContext no Entity Framework.

Carregar os dados

  1. Crie um DatabaseLoader que carregue os registros do tipo ModelInput.

    DatabaseLoader loader = mlContext.Data.CreateDatabaseLoader<ModelInput>();
    
  2. Defina a consulta para carregar os dados do banco de dados.

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

    Os algoritmos do ML.NET esperam que os dados sejam do tipo Single. Portanto, valores numéricos provenientes do banco de dados que não são do tipo Real, um valor de ponto flutuante de precisão simples, devem ser convertidos em Real.

    As colunas Year e TotalRental são tipos inteiros no banco de dados. Usando a função interna CAST, ambas são convertidas em Real.

  3. Crie um DatabaseSource para se conectar ao banco de dados e executar a consulta.

    DatabaseSource dbSource = new DatabaseSource(SqlClientFactory.Instance,
                                    connectionString,
                                    query);
    
  4. Carregar os dados em um IDataView.

    IDataView dataView = loader.Load(dbSource);
    
  5. O conjunto de dados contém dois anos de dados. Somente os dados do primeiro ano são usados para o treinamento. O segundo ano é usado para comparar os valores reais com a previsão produzida pelo modelo. Filtre os dados usando a transformação FilterRowsByColumn.

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

    Para o primeiro ano, somente os valores na coluna Year menores que 1 são selecionados definindo o parâmetro upperBound como 1. Por outro lado, para o segundo ano, os valores maiores ou iguais a 1 são selecionados definindo o parâmetro lowerBound como 1.

Definir o pipeline da análise de série temporal

  1. Defina um pipeline que use o SsaForecastingEstimator para prever valores em um conjunto de dados de série temporal.

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

    O forecastingPipeline utiliza 365 pontos de dados para o primeiro ano e faz a amostragem ou divisão do conjunto de dados de série temporal em intervalos de 30 dias (mensais), conforme especificado pelo parâmetro seriesLength. Cada uma dessas amostras é analisada em uma janela semanal ou de sete dias. Ao determinar qual é o valor previsto para os próximos períodos, os valores dos sete dias anteriores são usados para uma previsão. O modelo é definido para prever sete períodos no futuro, conforme definido pelo parâmetro horizon. Como uma previsão é um palpite informado, nem sempre ela é 100% precisa. Portanto, é bom conhecer o intervalo de valores nos cenários de melhor e pior caso, conforme definido pelos limites superior e inferior. Nesse caso, o nível de confiança para os limites inferior e superior é definido como 95%. O nível de confiança pode ser aumentado ou diminuído de acordo com o que é mais adequado. Quanto maior o valor, mais amplo é o intervalo entre os limites superior e inferior para atingir o nível de confiança desejado.

  2. Use o método Fit para treinar o modelo e ajustar os dados ao forecastingPipeline previamente definido.

    SsaForecastingTransformer forecaster = forecastingPipeline.Fit(firstYearData);
    

Avaliar o modelo

Avalie o desempenho do modelo prevendo os dados do próximo ano e comparando-os com os valores reais.

  1. Crie um novo método de utilitário chamado Evaluate na parte inferior do arquivo Program.cs.

    Evaluate(IDataView testData, ITransformer model, MLContext mlContext)
    {
    
    }
    
  2. Dentro do método Evaluate, preveja os dados do segundo ano usando o método Transform com o modelo treinado.

    IDataView predictions = model.Transform(testData);
    
  3. Obtenha os valores reais dos dados usando o método CreateEnumerable.

    IEnumerable<float> actual =
        mlContext.Data.CreateEnumerable<ModelInput>(testData, true)
            .Select(observed => observed.TotalRentals);
    
  4. Obtenha os valores de previsão usando o método CreateEnumerable.

    IEnumerable<float> forecast =
        mlContext.Data.CreateEnumerable<ModelOutput>(predictions, true)
            .Select(prediction => prediction.ForecastedRentals[0]);
    
  5. Calcule a diferença entre os valores reais e os previstos, comumente referida como o erro.

    var metrics = actual.Zip(forecast, (actualValue, forecastValue) => actualValue - forecastValue);
    
  6. Meça o desempenho calculando os valores de erro médio absoluto e raiz do erro quadrático médio.

    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
    

    Para avaliar o desempenho, as seguintes métricas são usadas:

    • Erro médio absoluto: mede o quanto as previsões estão próximas do valor real. Este valor varia de 0 a infinito. Quanto mais próximo de 0, melhor a qualidade do modelo.
    • Raiz do erro quadrático médio: resume o erro no modelo. Este valor varia de 0 a infinito. Quanto mais próximo de 0, melhor a qualidade do modelo.
  7. Gere as métricas no console.

    Console.WriteLine("Evaluation Metrics");
    Console.WriteLine("---------------------");
    Console.WriteLine($"Mean Absolute Error: {MAE:F3}");
    Console.WriteLine($"Root Mean Squared Error: {RMSE:F3}\n");
    
  8. Chame o método Evaluate abaixo chamando o método Fit().

    Evaluate(secondYearData, forecaster, mlContext);
    

Salvar o modelo

Se estiver satisfeito com o modelo, salve-o para uso posterior em outros aplicativos.

  1. Abaixo do método Evaluate(), crie um TimeSeriesPredictionEngine. O TimeSeriesPredictionEngine é um método de conveniência para fazer previsões individuais.

    var forecastEngine = forecaster.CreateTimeSeriesEngine<ModelInput, ModelOutput>(mlContext);
    
  2. Salve o modelo em um arquivo chamado MLModel.zip, conforme especificado pela variável definida anteriormente, modelPath. Use o método Checkpoint para salvar o modelo.

    forecastEngine.CheckPoint(mlContext, modelPath);
    

Usar o modelo para prever a demanda

  1. Abaixo do método Evaluate, crie um método de utilitário chamado Forecast.

    void Forecast(IDataView testData, int horizon, TimeSeriesPredictionEngine<ModelInput, ModelOutput> forecaster, MLContext mlContext)
    {
    
    }
    
  2. Dentro do método Forecast, use o método Predict para prever aluguéis para os próximos sete dias.

    ModelOutput forecast = forecaster.Predict();
    
  3. Alinhe os valores reais e previstos para sete períodos.

    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. Faça a iteração na saída da previsão para exibi-la no console.

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

Executar o aplicativo

  1. Depois de chamar o método Checkpoint(), chame o método Forecast.

    Forecast(secondYearData, 7, forecastEngine, mlContext);
    
  2. Executar o aplicativo. Uma saída semelhante à abaixo deve aparecer no console. Para fins de brevidade, a saída foi condensada.

    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
    

A inspeção dos valores reais e previstos mostra as seguintes relações:

Comparação de valores reais vs. previsão

Embora os valores previstos não prevejam o número exato de aluguéis, eles fornecem uma faixa de valores mais estreita que permite que uma operação otimize o uso de recursos.

Parabéns! Você criou com sucesso um modelo de machine learning de série temporal para prever a demanda de aluguéis de bicicletas.

É possível encontrar o código-fonte deste tutorial no repositório dotnet/machinelearning-samples.

Próximas etapas