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
| Parameter | Default | Description |
|---|---|---|
enabled | true | Enable/disable the trend component |
changepoints | 10 | Number of potential slope changes |
changepoints_range | 0.8 | Fraction of data where changepoints can occur |
regularization | nil | L1 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).
| Regularization | Effect |
|---|---|
nil or 0 | No penalty, all changepoints can have any value |
0.01 - 0.1 | Light penalty, subtle changes may be zeroed out |
0.1 - 1.0 | Strong 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: 0for 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
- Seasonality - Add yearly and weekly patterns
- Auto-Regression - Capture dependencies on recent values