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

Soothsayer Demo

soothsayer-demo.livemd

Soothsayer Demo

# Livebook Setup

Mix.install([
  {:soothsayer, path: "~/code/soothsayer"},
  {:explorer, "~> 0.9.1"},
  {:vega_lite, ">= 0.0.0"},
  {:kino_vega_lite, ">= 0.0.0"},
  {:kino_explorer, "~> 0.1.20"},
  {:req, "~> 0.5.6"}
])

alias Explorer.DataFrame
alias Explorer.Series
alias VegaLite, as: Vl

Nx.global_default_backend(EXLA.Backend)

Section

# Generate synthetic data

start_date = ~D[2020-01-01]
end_date = ~D[2023-12-31]
dates = Date.range(start_date, end_date)

y =
  Enum.map(dates, fn date ->
    days_since_start = Date.diff(date, start_date)
    trend = 1000 + 0.5 * days_since_start
    yearly_seasonality = 50 * :math.sin(2 * :math.pi() * days_since_start / 365.25)
    weekly_seasonality = 20 * :math.cos(2 * :math.pi() * Date.day_of_week(date) / 7)
    noise = :rand.normal(0, 200)
    trend + yearly_seasonality + weekly_seasonality + noise
  end)

df = DataFrame.new(%{"ds" => dates, "y" => y})
Vl.new(width: 800, height: 600)
|> Vl.data_from_values(df, only: ["ds", "y"])
|> Vl.mark(:line)
|> Vl.encode_field(:x, "ds", type: :temporal)
|> Vl.encode_field(:y, "y", type: :quantitative)
# Create and fit Soothsayer model

model =
  Soothsayer.new(%{
    trend_config: %{enabled: true},
    seasonality_config: %{
      yearly: %{enabled: true},
      weekly: %{enabled: true}
    },
    epochs: 10
  })

fitted_model = Soothsayer.fit(model, df)
# Make predictions and extract components
predictions = Soothsayer.predict(fitted_model, df["ds"])

df_with_predictions =
  df
  |> DataFrame.put("yhat", predictions)

df_with_predictions
Vl.new(width: 800, height: 600)
|> Vl.data_from_values(df_with_predictions, only: ["ds", "y", "yhat"])
|> Vl.layers([
  Vl.new()
  |> Vl.mark(:line)
  |> Vl.encode_field(:x, "ds", type: :temporal)
  |> Vl.encode_field(:y, "y", type: :quantitative),
  Vl.new()
  |> Vl.mark(:line, color: "tomato")
  |> Vl.encode_field(:x, "ds", type: :temporal)
  |> Vl.encode_field(:y, "yhat", type: :quantitative)
])
Vl.new(width: 800, height: 600)
|> Vl.data_from_values(df_with_predictions, only: ["ds", "y", "yhat"])
|> Vl.layers([
  Vl.new()
  |> Vl.mark(:line)
  |> Vl.encode_field(:x, "ds",
    type: :temporal,
    scale: [domain: ["2023-05-01", "2023-06-30"]]
  )
  |> Vl.encode_field(:y, "y", type: :quantitative),
  Vl.new()
  |> Vl.mark(:line, color: "tomato")
  |> Vl.encode_field(:x, "ds",
    type: :temporal,
    scale: [domain: ["2023-05-01", "2023-06-30"]]
  )
  |> Vl.encode_field(:y, "yhat", type: :quantitative)
])
input = %{
  "trend" => Nx.template({1, 1}, :f32),
  "yearly" => Nx.template({1, 12}, :f32),  # Assuming 6 Fourier terms for yearly seasonality
  "weekly" => Nx.template({1, 6}, :f32)    # Assuming 3 Fourier terms for weekly seasonality
}

# Display the graph
Axon.Display.as_graph(model.network, input)
components = Soothsayer.predict_components(fitted_model, df["ds"])

df_with_components =
  df
  |> DataFrame.put("yhat", predictions)
  |> DataFrame.put("trend", components.trend)
  |> DataFrame.put("yearly_seasonality", components.yearly_seasonality)
  |> DataFrame.put("weekly_seasonality", components.weekly_seasonality)
Vl.new(width: 800, height: 600)
|> Vl.data_from_values(df_with_components)
|> Vl.layers([
  Vl.new()
  |> Vl.mark(:line)
  |> Vl.encode_field(:x, "ds", type: :temporal)
  |> Vl.encode_field(:y, "trend", type: :quantitative),
])
Vl.new(width: 800, height: 600)
|> Vl.data_from_values(df_with_components)
|> Vl.layers([
  Vl.new()
  |> Vl.mark(:line)
  |> Vl.encode_field(:x, "ds", type: :temporal)
  |> Vl.encode_field(:y, "yearly_seasonality", type: :quantitative),
])
Vl.new(width: 800, height: 600)
|> Vl.data_from_values(df_with_components)
|> Vl.layers([
  Vl.new()
  |> Vl.mark(:line)
  |> Vl.encode_field(:x, "ds", type: :temporal, scale: [domain: ["2023-01-01", "2023-06-30"]])
  |> Vl.encode_field(:y, "weekly_seasonality", type: :quantitative, scale: [domain: [1000, 1400]])
])

Real data

Daily energy price data over the 4 years from Spain.

real_df = DataFrame.from_csv!("https://raw.githubusercontent.com/ourownstory/neuralprophet-data/main/kaggle-energy/datasets/tutorial01.csv")

real_df =
  real_df
  |> DataFrame.put("ds", real_df["ds"] |> Explorer.Series.cast(:date))
Vl.new(width: 800, height: 600)
|> Vl.data_from_values(real_df, only: ["ds", "y"])
|> Vl.mark(:point)
|> Vl.encode_field(:x, "ds", type: :temporal)
|> Vl.encode_field(:y, "y", type: :quantitative)
model = Soothsayer.new(%{epochs: 100})
fitted_model = Soothsayer.fit(model, real_df)
predictions = Soothsayer.predict(fitted_model, real_df["ds"])

real_df_with_predictions =
  real_df
  |> DataFrame.put("yhat", predictions)

real_df_with_predictions
Vl.new(width: 800, height: 600)
|> Vl.data_from_values(real_df_with_predictions, only: ["ds", "y", "yhat"])
|> Vl.layers([
  Vl.new()
  |> Vl.mark(:line)
  |> Vl.encode_field(:x, "ds", type: :temporal)
  |> Vl.encode_field(:y, "y", type: :quantitative),
  Vl.new()
  |> Vl.mark(:line, color: "tomato")
  |> Vl.encode_field(:x, "ds", type: :temporal)
  |> Vl.encode_field(:y, "yhat", type: :quantitative)
])