Las series de tiempo (time series) son muy útiles para predecir valores numéricos para uno o varios período de tiempo. En este ejemplo veremos cómo predecir el valor de una acción usando series de tiempo y ML.NET.
El código completo y el dataset que veremos en este ejemplo se puede descargar de GitHub
Problema
El valor de una acción en el mercado de valores se suele observar en gráficos de velas japonesas en sesiones de tiempo de: una hora, cuatro horas o un día. Durante cada sesión se grafica los valores de apertura, el valor más bajo, el valor más alto y el valor de cierre de la sesión. Como ejemplo veamos un gráfico del índice Nasdaq en sesiones de 4 horas (h4). Con los datos de este gráfico intentaremos predecir los valores del Nasdaq para la última sesión del día.

En resumen, intentaremos contestar las siguientes preguntas respecto al valor del Nasdaq: ¿Cuál va a ser el valor más alto que alcance de la acción durante la última sesión? ¿Cuál va a ser el valor más bajo que alcance la acción durante la última sesión? ¿Cuál va a ser el valor de cierre de la sesión?
Datos
Para este ejercicio contamos con un dataset con los valores de apertura, alto, bajo y cierre para cada sesión de cuatro horas del indice Nasdaq durante los últimos 6 meses. Los datos tienen esta apariencia

Modelo
La técnica para analizar los datos que vamos a usar en este ejemplo es el análisis de serie temporal de variante única. El análisis de serie temporal (time series) de variante única examina una única observación numérica durante un período de tiempo a intervalos específicos, como los valores de una acción cada 4 horas.
El algoritmo que se usa en este tutorial es el análisis de un solo espectro (SSA). SSA sirve para descomponer una serie temporal en un conjunto de componentes principales. Estos componentes se pueden interpretar como partes de una señal que corresponde a tendencias, ruido, estacionalidad y muchos otros factores. Después, estos componentes se reconstruyen y se usan para pronosticar valores en el futuro.
Para la creación del modelo creamos usaremos .NET Core 3.1 y ML.NET , para lo cual iniciaremos creando un proyecto de librería de clases que llamaremos MLStockFunctionValuesML.Model. Al proyecto agregaremos los paquetes NuGet de Microsoft.ML y Microsoft.ML.TimeSeries.
Ahora crearemos las estructura necesaria para cargar el dataset de entrenamiento. Agregamos una clase ModelInput con el siguiente código
public class ModelInput
{
[ColumnName("OpenTime"), LoadColumn(0)]
public DateTime OpenTime { get; set; }
[ColumnName("Open"), LoadColumn(1)]
public float Open { get; set; }
[ColumnName("High"), LoadColumn(2)]
public float High { get; set; }
[ColumnName("Low"), LoadColumn(3)]
public float Low { get; set; }
[ColumnName("Close"), LoadColumn(4)]
public float Close { get; set; }
[ColumnName("TickVolume"), LoadColumn(5)]
public float TickVolume { get; set; }
}
A diferencia de un modelo de clasificación o de regresión, en un modelo de series de tiempo no realizamos la predicción en base a nuevos datos de entrada, la predicción se realiza en base a los datos de entrenamiento y para un período de tiempo -horizonte- determinado. Para la creación del modelo y predicción crearemos una clase ModelBuilder a nuestro proyecto.
En la clase agregamos un método llamado CreateModel y recibirá como parámetro el valor de la sesión a predecir: cierre, alto o bajo
/// <summary>
///
/// </summary>
/// <param name="value">Close,High,Low,Open</param>
public static ModelOutput CreateModel(string value)
{
}
Ahora seguiremos los pasos tradicionales en la creación de un modelo de Machine Learning. Primero cargamos los datos de entrenamiento, en este caso el dataset está en un archivo de texto csv. Adicionalmente separamos los últimos 60 registros (las últimas dos semanas) para evaluar el modelo.
// Load Data
IDataView trainingDataView = mlContext.Data.LoadFromTextFile<ModelInput>(
path: TRAIN_DATA_FILEPATH,
hasHeader: true,
separatorChar: ',',
allowQuoting: true,
allowSparse: false);
var trainList = mlContext.Data.CreateEnumerable<ModelInput>(trainingDataView, true);
int trainSize = trainList.Count();
var testList = trainList.TakeLast(60);
IDataView testDataView = mlContext.Data.LoadFromEnumerable(testList);
La creación del pipeline es muy simple para las series de tiempo ya que no necesitamos de transformaciones de datos, solo debemos agregar el algoritmo ForecastBySsa y especificar los parámetros deseados.
var forecastingPipeline = mlContext.Forecasting.ForecastBySsa(
outputColumnName: "ForecastedValue",
inputColumnName: value,
windowSize: 5,
seriesLength: 60,
trainSize: trainSize,
horizon: 1,
confidenceLevel: 0.95f,
confidenceLowerBoundColumn: "LowerBoundValue",
confidenceUpperBoundColumn: "UpperBoundValue");
El parámetro outputColumnName corresponde al nombre de la columna en el esquema en la que se colocará el valor de la predicción. inputColumnName es la columna en el esquema en los datos de entrenamiento con el valor a predecir, en nuestro caso es el parámetro que recibe el método CreateModel. En el parámetro trainSize le pasamos el total de los registros del dataset de entrenamiento, no queremos excluir ningún registro. seriesLength es el intervalo de la serie temporal en el que se dividen los datos para ser analizados en las ventanas de tiempo especificadas en windowSize. En nuestro caso queremos que los datos sean analizados en períodos de 60 registros (dos semanas de sesiones de 4 horas cada una) y que se tome en cuenta los últimos 5 registros (las primeras 5 sesiones del día) para predecir el siguiente valor de la serie (la última sesión del día). Si quisiera predecir más series en adelante lo debo especificar en el parámetro horizon.
Una vez creado el pipeline el siguiente paso es entrenar el modelo con el dataset de entrenamiento
SsaForecastingTransformer forecaster = forecastingPipeline.Fit(trainingDataView);
Para la evaluación del modelo creamos un método Evaluate
static void Evaluate(IDataView testData, ITransformer model, MLContext mlContext, string value)
{
}
Primero usamos el método Transform para evaluar los resultados con el dataset de pruebas.
IDataView predictions = model.Transform(testData);
Ahora debemos comparar el resultado de las predicciones con los resultados reales, para esto necesitamos primero crear una clase ModelOutput para poder instanciarla con el resultado de las predicciones.
public class ModelOutput
{
public float[] ForecastedValue { get; set; }
public float[] LowerBoundValue { get; set; }
public float[] UpperBoundValue { get; set; }
}
El nombre de las propiedades tiene que coincidir con el nombre de las columnas de salida que especificamos en el pipeline anteriormente. Y deben ser de tipo float[] ya que según el parámetro Horizon se pueden realizar predicciones para una o más períodos hacia adelante.
De vuelta en el método Evaluate transformaremos las predicciones y los datos de prueba a un IEnumerable<> para poder compararlos entre si.
IEnumerable<float> actual =
mlContext.Data.CreateEnumerable<ModelInput>(testData, true)
.Select(observed =>
{
switch (value)
{
case "Close":
return observed.Close;
case "High":
return observed.High;
case "Low":
return observed.Low;
default:
return observed.Close;
}
});
IEnumerable<float> forecast =
mlContext.Data.CreateEnumerable<ModelOutput>(predictions, true)
.Select(prediction => prediction.ForecastedValue[0]);
El error de cada predicción lo calculamos restando el valor real y el valor de la predicción. Y evaluaremos el error con las siguientes métricas.
- Error medio absoluto (Mean Absolute Error): mide la cercanía de las predicciones respecto del valor real. Este valor va de 0 a infinito. Cuanto más se acerque a 0, mejor es la calidad del modelo.
- Error cuadrático medio (Root Mean Squared Error): resume el error del modelo. Este valor va de 0 a infinito.Cuanto más se acerque a 0, mejor es la calidad del modelo.
var metrics = actual.Zip(forecast, (actualValue, forecastValue) => actualValue - forecastValue);
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
Console.WriteLine("Evaluation Metrics");
Console.WriteLine("---------------------");
Console.WriteLine($"Mean Absolute Error: {MAE:F3}");
Console.WriteLine($"Root Mean Squared Error: {RMSE:F3}\n");
De vuelta en le método CreateModel agregamos la llamada al método Evaluate, con eso el modelo está completo y lo podemos grabar con el siguiente código
Evaluate(testDataView, forecaster, mlContext, value);
var forecastEngine = forecaster.CreateTimeSeriesEngine<ModelInput, ModelOutput>(mlContext);
forecastEngine.CheckPoint(mlContext, MODEL_FILE);
El último paso es la creación de un método para realizar la predicción del valor de la acción para el siguiente período de 4 horas. Para esto agregamos en método llamado Forecast.
static ModelOutput Forecast(IEnumerable<ModelInput> testData, int horizon, TimeSeriesPredictionEngine<ModelInput, ModelOutput> forecaster, MLContext mlContext, string value)
{
}
La predicción la hacemos con el método Predict. A diferencia de un modelo de clasificación o regresión, en las series de tiempo no es necesario pasar como parámetro una nueva observación al método Predict ya que la predicción se realiza en base a los datos de entrenamiento.
ModelOutput forecast = forecaster.Predict();
Solo a manera de ayuda agregamos una comparación de la predicción con el último registro de los datos de prueba con el siguiente código.
IEnumerable<string> forecastOutput =
testData.TakeLast(horizon)
.Select((ModelInput values, int index) =>
{
string rentalDate = values.OpenTime.ToString();
float actualValue;
switch (value)
{
case "Close":
actualValue = values.Close;
break;
case "High":
actualValue = values.High;
break;
case "Low":
actualValue = values.Low;
break;
default:
actualValue = values.Close;
break;
}
float lowerEstimate = forecast.LowerBoundValue[index];
float estimate = forecast.ForecastedValue[index];
float upperEstimate = forecast.UpperBoundValue[index];
return $"Date: {rentalDate}\n" +
$"Actual {value}: {actualValue}\n" +
$"Forecast: {estimate}\n";
});
Console.WriteLine("Stock Forecast");
Console.WriteLine("---------------------");
foreach (var prediction in forecastOutput)
{
Console.WriteLine(prediction);
}
return forecast;
De vuelta en el método CreateModel agregamos la última linea para realizar y retornar la predicción.
return Forecast(testList, 1, forecastEngine, mlContext, value);
Ahora solo nos queda probar el modelo, para esto creamos una aplicación de consola que llamaremos MLStockFunctionValues.ConsoleApp y agregamos la referencia al proyecto MLStockFunctionValuesML.Model.
En el Main de la aplicación bastará con agregar tres llamadas al método CreateModel, uno para predecir el valor del cierre de la siguiente sesión (siguiente período), otro para predecir el valor más bajo de la sesión y la última para predecir el valor más alto que alcance el precio durante la sesión.
var predictClose = ModelBuilder.CreateModel("Close");
var predictHigh = ModelBuilder.CreateModel("High");
var predictLow = ModelBuilder.CreateModel("Low");
La salida de la aplicación se verá de esta forma

El código completo y el dataset que veremos en este ejemplo se puede descargar de GitHub
Artículos relacionados
Cómo hacer predicciones en batch usando ML.NET
Trabajar con múltiples fuentes de datos usando ML.NET