Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Forecast the Future

forecast-the-future.livemd

Forecast the Future

Mix.install([
  {:explorer, "~> 0.8.1"},
  {:nx, "~> 0.7.1"},
  {:exla, "~> 0.7.1"},
  {:axon, "~> 0.6.1"},
  {:vega_lite, "~> 0.1.8"},
  {:kino, "~> 0.12.3"},
  {:kino_vega_lite, "~> 0.1.11"}
])

alias VegaLite, as: Vl

Application.put_env(
  :exla,
  :clients,
  cuda: [
    platforms: :cuda,
    lazy_transformers: :never
  ]
)

Nx.global_default_backend(EXLA.Backend)
Nx.Defn.default_options(compiler: EXLA)

Exploring the Data

csv_file = "all_stocks_2006-01-01_to_2018-01-01.csv"

df =
  csv_file
  |> Explorer.DataFrame.from_csv!(parse_dates: true)
  |> Explorer.DataFrame.select(["Date", "Close", "Name"])
Vl.new(title: "DIJA Stock Prices", width: 640, height: 480)
|> Vl.data_from_values(Explorer.DataFrame.to_columns(df))
|> Vl.mark(:line)
|> Vl.encode_field(:x, "Date", type: :temporal)
|> Vl.encode_field(:y, "Close", type: :quantitative)
|> Vl.encode_field(:color, "Name", type: :nominal)
|> Kino.VegaLite.new()
aapl_df =
  Explorer.DataFrame.filter_with(df, fn df ->
    Explorer.Series.equal(df["Name"], "AAPL")
  end)
Vl.new(title: "AAPL Stock Prices", width: 640, height: 480)
|> Vl.data_from_values(Explorer.DataFrame.to_columns(aapl_df))
|> Vl.mark(:line)
|> Vl.encode_field(:x, "Date", type: :temporal)
|> Vl.encode_field(:y, "Close", type: :quantitative)
|> Vl.encode_field(:color, "Name", type: :nominal)
|> Kino.VegaLite.new()

Normalize the Data

normalized_aapl_df =
  Explorer.DataFrame.mutate_with(aapl_df, fn df ->
    var = Explorer.Series.variance(df["Close"])
    mean = Explorer.Series.mean(df["Close"])
    centered = Explorer.Series.subtract(df["Close"], mean)
    norm = Explorer.Series.divide(centered, var)
    [Close: norm]
  end)
Vl.new(title: "AAPL Stock Prices", width: 640, height: 480)
|> Vl.data_from_values(Explorer.DataFrame.to_columns(normalized_aapl_df))
|> Vl.mark(:line)
|> Vl.encode_field(:x, "Date", type: :temporal)
|> Vl.encode_field(:y, "Close", type: :quantitative)
|> Vl.encode_field(:color, "Name", type: :nominal)
|> Kino.VegaLite.new()
defmodule Data do
  def window(inputs, window_size, target_window_size) do
    inputs
    |> Stream.chunk_every(window_size + target_window_size, 1, :discard)
    |> Stream.map(fn window ->
      features =
        window
        |> Enum.take(window_size)
        |> Nx.tensor()
        |> Nx.new_axis(1)

      targets =
        window
        |> Enum.drop(window_size)
        |> Nx.tensor()
        |> Nx.new_axis(1)

      {features, targets}
    end)
  end

  def batch(inputs, batch_size) do
    inputs
    |> Stream.chunk_every(batch_size, batch_size, :discard)
    |> Stream.map(fn windows ->
      {features, targets} = Enum.unzip(windows)
      {Nx.stack(features), Nx.stack(targets)}
    end)
  end
end
train_df =
  Explorer.DataFrame.filter_with(normalized_aapl_df, fn df ->
    Explorer.Series.less(df["Date"], Date.new!(2016, 1, 1))
  end)

test_df =
  Explorer.DataFrame.filter_with(normalized_aapl_df, fn df ->
    Explorer.Series.greater_equal(df["Date"], Date.new!(2016, 1, 1))
  end)
window_size = 5
batch_size = 32

train_prices = Explorer.Series.to_list(train_df["Close"])
test_prices = Explorer.Series.to_list(test_df["Close"])

single_step_train_data =
  train_prices
  |> Data.window(window_size, 1)
  |> Data.batch(batch_size)

single_step_test_data =
  test_prices
  |> Data.window(window_size, 1)
  |> Data.batch(batch_size)

Enum.take(single_step_train_data, 1)

CNN

cnn_model =
  Axon.input("stock_price")
  |> Axon.nx(&Nx.new_axis(&1, -1))
  |> Axon.conv(32, kernel_size: {window_size, 1}, activation: :relu)
  |> Axon.dense(32, activation: :relu)
  |> Axon.dense(1)
template = Nx.template({32, 10, 1}, :f32)
Axon.Display.as_graph(cnn_model, template)
cnn_trained_model_state =
  cnn_model
  |> Axon.Loop.trainer(:mean_squared_error, :adam)
  |> Axon.Loop.metric(:mean_absolute_error)
  |> Axon.Loop.run(single_step_train_data, %{}, epochs: 50, compiler: EXLA)
cnn_model
|> Axon.Loop.evaluator()
|> Axon.Loop.metric(:mean_absolute_error)
|> Axon.Loop.run(single_step_test_data, cnn_trained_model_state, compiler: EXLA)
defmodule Analysis do
  def visualize_predictions(
        model,
        model_state,
        prices,
        window_size,
        target_window_size,
        batch_size
      ) do
    {_, predict_fn} = Axon.build(model, compiler: EXLA)

    windows =
      prices
      |> Data.window(window_size, target_window_size)
      |> Data.batch(batch_size)
      |> Stream.map(&elem(&1, 0))

    predicted =
      Enum.flat_map(windows, fn window ->
        predict_fn.(model_state, window) |> Nx.to_flat_list()
      end)

    predicted = List.duplicate(nil, 10) ++ predicted

    types =
      List.duplicate("AAPL", length(prices)) ++
        List.duplicate("Predicted", length(prices))

    days =
      Enum.to_list(0..(length(prices) - 1)) ++
        Enum.to_list(0..(length(prices) - 1))

    prices = prices ++ predicted

    plot(
      %{
        "days" => days,
        "prices" => prices,
        "types" => types
      },
      "AAPL Stock Price vs. Predicted, CNN Single-Shot"
    )
  end

  defp plot(values, title) do
    Vl.new(title: title, width: 640, height: 480)
    |> Vl.data_from_values(values)
    |> Vl.mark(:line)
    |> Vl.encode_field(:x, "days", type: :temporal)
    |> Vl.encode_field(:y, "prices", type: :quantitative)
    |> Vl.encode_field(:color, "types", type: :nominal)
    |> Kino.VegaLite.new()
  end
end
Analysis.visualize_predictions(
  cnn_model,
  cnn_trained_model_state,
  Explorer.Series.to_list(aapl_df["Close"]),
  window_size,
  1,
  batch_size
)