The trend component captures long-term growth or decline in your data. Soothsayer uses piecewise linear trends with automatic changepoint detection, allowing the trend to change slope at multiple points.

This is useful for capturing:

  • Product launches that accelerate growth
  • Market shifts that slow growth
  • Policy changes that affect trajectory
  • Seasonal baseline shifts

How It Works

The trend is modeled as:

trend(t) = k * t + m + sum(delta_j * max(0, t - s_j))

Where:

  • k = base growth rate (learned)
  • m = offset (learned)
  • s_j = changepoint positions (computed from data)
  • delta_j = rate adjustments at each changepoint (learned)

The model learns which changepoints matter and how much the slope changes at each one.

For more details on the math, see NeuralProphet's Trend documentation.

Configuration

model = Soothsayer.new(%{
  trend: %{
    enabled: true,           # Enable trend component (default: true)
    changepoints: 10,      # Number of potential changepoints (default: 10)
    changepoints_range: 0.8, # Place in first 80% of data (default: 0.8)
    regularization: nil      # L1 penalty on rate changes (default: nil)
  }
})

Parameters

ParameterDefaultDescription
enabledtrueEnable/disable the trend component
changepoints10Number of potential slope changes
changepoints_range0.8Fraction of data where changepoints can occur
regularizationnilL1 penalty to encourage sparse changepoints

Simple Linear Trend

For data with a consistent growth rate, disable changepoints:

model = Soothsayer.new(%{
  trend: %{changepoints: 0}
})

This gives you a simple linear trend: trend(t) = k * t + m

Piecewise Linear Trend

For data where the growth rate changes over time:

model = Soothsayer.new(%{
  trend: %{changepoints: 10, changepoints_range: 0.8}
})

The changepoints_range parameter controls where changepoints can be placed. Setting it to 0.8 means changepoints are only placed in the first 80% of the data, preventing overfitting at the end of the series.

Example: Detecting Slope Changes

alias Explorer.DataFrame

# Data with a slope change: flat first year, steep second year
n_days = 730
dates = Enum.map(0..(n_days - 1), fn i -> Date.add(~D[2020-01-01], i) end)

y = Enum.map(0..(n_days - 1), fn i ->
  # Slope changes from 0.1 to 3.0 at day 365
  trend = if i < 365, do: 100 + 0.1 * i, else: 100 + 0.1 * 365 + 3.0 * (i - 365)
  noise = :rand.normal(0, 10)
  trend + noise
end)

df = DataFrame.new(%{"ds" => dates, "y" => y})

# Model WITHOUT changepoints (misses the slope change)
model_linear = Soothsayer.new(%{
  trend: %{changepoints: 0},
  seasonality: %{yearly: %{enabled: false}, weekly: %{enabled: false}},
  epochs: 100
})

# Model WITH changepoints (captures the slope change)
model_piecewise = Soothsayer.new(%{
  trend: %{changepoints: 10, changepoints_range: 0.8},
  seasonality: %{yearly: %{enabled: false}, weekly: %{enabled: false}},
  epochs: 100
})

fitted_linear = Soothsayer.fit(model_linear, df)
fitted_piecewise = Soothsayer.fit(model_piecewise, df)

The piecewise model will capture the slope change much better than the simple linear model.

Regularization

When you're unsure how many changepoints you need, set more than necessary and use regularization to prune unimportant ones:

model = Soothsayer.new(%{
  trend: %{
    changepoints: 25,      # More than we likely need
    regularization: 0.1      # L1 penalty pushes small changes toward zero
  }
})

Higher regularization values encourage sparser changepoints (fewer slope changes).

RegularizationEffect
nil or 0No penalty, all changepoints can have any value
0.01 - 0.1Light penalty, subtle changes may be zeroed out
0.1 - 1.0Strong penalty, only significant changes remain

Choosing Parameters

changepoints:

  • Start with the default (10)
  • Increase if you expect many slope changes
  • Decrease if you expect a smooth trend
  • Use changepoints: 0 for simple linear trend

changepoints_range:

  • Default (0.8) works well for most cases
  • Decrease if you have a short forecast horizon
  • Increase if slope changes occur late in your data

regularization:

  • Start with nil (no regularization)
  • Add if you see overfitting (trend follows noise too closely)
  • Higher values = smoother trend with fewer changes

Network Architecture

With changepoints enabled, the trend input has shape {batch_size, 1 + changepoints}:

# The network receives:
# - Column 0: normalized time t
# - Columns 1-n: changepoint features max(0, t - s_j)

input_shape = {nil, 1 + changepoints}

See the Interactive Livebook Tutorial for network visualization examples.

Next Steps