Skip to main content
Open llms.txtCopy tools

Piecewise Linear Seasonal

PiecewiseLinearSeasonalForecaster is the local model for interpretable trend, changepoints, seasonalities, events, extra regressors, and component decomposition. Use it when the forecast needs to show which named components move the prediction, not just the final point estimate.

When To Use

Use this model for one regular demand, fare, duration, or trip-distance series when the forecast should be explained as:

  • a piecewise trend with optional changepoints;
  • one or more repeating seasonal components;
  • known event windows or known future regressors;
  • additive or multiplicative component effects;
  • external trend belief adjustments for forecast horizons;
  • recent residual shock propagation when the fitted trend has under- or over-predicted for several consecutive observations;
  • residual intervals or quantile-style uncertainty summaries.

It is a good fit for demand with event windows, recurring weekday and hour effects, or lane-level aggregate demand where a scientist needs to inspect the trend and seasonal contribution separately.

Prefer seasonal naive, theta, ETS, ARIMA, or Kalman when the question is only a small local baseline. Prefer CartoBoostLagForecaster or AutoForecaster when many related zones or lanes should share supervised lag features.

Interactive Example

Piecewise linear seasonal panel forecast

Runs piecewise_linear_seasonal against a bundled route-demand sample.

Ready to run in this page.

The embedded example runs in this page and returns forecast rows from the interactive example. Use the full Python API when you need to persist component decomposition, fitted history diagnostics, residuals, trend movement, and named components for a report.

Scientific Role

Piecewise linear seasonal forecasting is a component model. It asks whether the future can be explained by a trend path plus named seasonal, event, and regressor effects. That makes it useful when the model output must support a claim such as "the weekday seasonality explains the recurring lift, while the trend changed after this cutoff."

The component rows are part of the evidence. If a benchmark or report uses this model, preserve the forecast table, component decomposition, changepoints, input cadence, regressors, and validation split with the metrics.

Assumptions And Failure Modes

The model assumes the configured seasonal periods and event/regressor columns match the forecast cadence and are known at prediction time. It can fail when the strongest signal is cross-zone borrowing, sparse intermittent demand, unmodeled disruptions, or a supervised panel effect that a local component model cannot see.

Common failure modes in panel data:

Failure modeScientific interpretationComparison to run
Trend changes too often.Changepoint flexibility is fitting noise.ETS, Kalman, or fewer changepoints.
Seasonal component is phase-shifted.Season length or timestamp cadence is wrong.Seasonal naive on the same cadence.
Event effect persists outside its window.The event window is standing in for missing covariates.Lag model with explicit calendar features.
One lane fits and another fails.Local components do not transfer across panels.CartoBoost lag or AutoForecaster.

Python Example

from cartoboost.forecasting import ForecastFrame, PiecewiseLinearSeasonalForecaster

frame = ForecastFrame.from_pandas(
hourly_airport_pickups,
timestamp_col="pickup_hour",
target_col="demand",
series_id_col="series_id",
freq="h",
known_future_covariates=["hour", "day_of_week", "holiday_event"],
)

model = PiecewiseLinearSeasonalForecaster(
growth="linear",
changepoints=8,
weekly_fourier_order=3,
daily_fourier_order=4,
prediction_interval_levels=(0.8, 0.95),
)

forecast = model.fit(frame).predict(24)
components = model.components(24)
components_frame = model.components_frame(24)
history_components = model.history_components()
history_frame = model.history_components_frame()

Irregular History

PiecewiseLinearSeasonalForecaster can fit Prophet-style irregular history without filling missing dates. Use this when older aggregate rows are weekly and recent operational rows are daily or business-daily. The model fits on the actual timestamps and uses freq only as the default horizon cadence.

from cartoboost.forecasting import ForecastFrame, PiecewiseLinearSeasonalForecaster

frame = ForecastFrame.from_pandas(
taxi_route_training_rows,
timestamp_col="pickup_day",
target_col="demand",
freq="D",
allow_irregular=True,
)

model = PiecewiseLinearSeasonalForecaster(
growth="linear",
changepoint_prior_scale=0.05,
weekly_fourier_order=3,
)

forecast = model.fit(frame).predict(
5,
future_timestamps=[
"2026-06-30",
"2026-07-01",
"2026-07-02",
"2026-07-06",
"2026-07-07",
],
)

Pass explicit future_timestamps or future_timestamps_by_series when the forecast calendar should skip weekends or holidays. Models whose training logic depends on equal row spacing, including ETS, ARIMA, theta, Kalman, intermittent demand, lag, neural, and auto selectors, still require a regular ForecastFrame.

Missing Targets

Prophet drops rows whose y value is missing before fitting, while still using the full timestamp range as the history calendar. CartoBoost exposes the same behavior with allow_missing_targets=True: NaN targets are allowed in ForecastFrame, infinity is still rejected, and the piecewise model fits only observed target rows. Horizon forecasts start after the latest timestamp in the input, even when the final row has a missing target. Historical diagnostics retain the missing-target timestamps: history_components_frame() emits a finite fitted value for each missing target row while leaving actual and residual null.

frame = ForecastFrame.from_pandas(
taxi_route_training_rows,
timestamp_col="pickup_day",
target_col="demand",
freq="D",
allow_missing_targets=True,
)

forecast = PiecewiseLinearSeasonalForecaster(
growth="linear",
weekly_fourier_order=3,
).fit(frame).predict(7)

This option is model-scoped. Use it for PiecewiseLinearSeasonalForecaster or last-value baselines when upstream training data carries future or withheld timestamps with missing targets. Do not use it to mask failed data joins or missing covariates. If the piecewise model uses an extra regressor, capacity regressor, floor regressor, or condition flag, that covariate must be present and finite on the rows where the component is evaluated.

Prophet-shaped holiday and changepoint inputs are accepted as configuration ergonomics for users migrating existing component-model workflows. The default automatic changepoint count is 25, matching Prophet's public default. CartoBoost's default changepoint_range is 1.0 so lane-level nowcasts can place trend breaks across the full training window when recent demand is moving; set changepoint_range=0.8 when you need Prophet's stricter default placement. Use n_changepoints for automatic changepoint placement, pass explicit dates through changepoints=[...] or changepoint_timestamps=[...], and pass a Prophet-style holidays table with holiday, ds, optional lower_window, upper_window, and prior_scale columns. Holidays become named event windows, and prior scales become per-event regularization settings before fitting. Prophet-style changepoint_prior_scale, seasonality_prior_scale, seasonality_mode, and holidays_mode aliases map to changepoint penalty, seasonality penalty, component mode, and event mode fields. The interactive forecast example accepts the same modeling aliases in camelCase: nChangepoints, changepointPriorScale, seasonalityPriorScale, holidaysPriorScale, seasonalityMode, holidaysMode, intervalWidth, and Prophet-shaped holidays rows with holiday, ds, lowerWindow, upperWindow, and priorScale.

Built-in country holiday calendars are available before fitting through model.add_country_holidays("US") or the constructor argument country_holidays="US". This path requires the optional holidays package: install holidays when country calendars are needed. Explicit holidays dataframes do not require the third-party package.

Prophet-Style Plotting

The local plotting layer provides full public plotting-utility parity with the prophet.plot module from prophet==1.2.2, the package version resolved by CartoBoost's benchmark dependency range prophet>=1.1,<1.3. Use cartoboost.plotting.plot, plot_components, plot_forecast_component, plot_weekly, plot_yearly, plot_seasonality, add_changepoints_to_plot, plot_cross_validation_metric, the matching Plotly helpers, and the helper-prop functions when a report expects Prophet's plotting API names.

The proof is maintained in Plotting: it lists every public prophet.plot 1.2.2 utility beside the matching local implementation and points to the parity tests in tests/python/test_plotting.py. This parity is limited to plotting utilities over Prophet-shaped forecast/component data; the reusable model API remains PiecewiseLinearSeasonalForecaster, not a prophet alias.

Trend Beliefs And Residual Shocks

Use trend_adjustments when forecast-time market beliefs should move the local trend path without changing the fitted historical coefficients. The mapping is keyed by forecast horizon, with values interpreted as trend multipliers. For example, {1: 1.01, 2: 1.02} raises the horizon-1 trend by 1 percent and the horizon-2 trend by 2 percent before the final forecast is assembled. Panel models can use trend_adjustments_by_series for route- or zone-specific beliefs; per-series values override global horizon values.

model = PiecewiseLinearSeasonalForecaster(
changepoints=8,
weekly_fourier_order=3,
trend_adjustments={1: 1.01, 2: 1.02, 3: 1.03},
trend_adjustments_by_series={
"132": {1: 1.04, 2: 1.05},
},
)

Use residual shocks when recent same-sign residuals indicate that the local trend is persistently under- or over-predicting a market. Set residual_shock_window to the required run length, residual_shock_scale to the fraction of the recent average residual to pass through, and residual_shock_decay to control how quickly the shock fades across horizons. The default scale is zero, so shock propagation is opt-in.

model = PiecewiseLinearSeasonalForecaster(
changepoints=8,
weekly_fourier_order=3,
residual_shock_window=3,
residual_shock_scale=0.5,
residual_shock_decay=0.8,
)

Component records include trend, adjusted_trend, trend_adjustment_multiplier, trend_adjustment, and residual_shock so a report can separate fitted trend, external market belief, and recent residual carry-forward.

Historical Component Diagnostics

Use history_components() after fitting when rolling-origin backtests need to explain why a cutoff won or lost. Use components() for future horizon decomposition and components_frame(horizon) when future component records should be flattened into the same dotted pandas columns as history_components_frame(), such as components.seasonal_total, components.weekly, or components.events.airport_surge.

Historical component methods return one row for every training observation and are computed from the fitted coefficients and the original covariates. Each row includes:

FieldMeaning
actualObserved target for the training timestamp.
fittedIn-sample fitted value from the same component equation used for prediction.
residualactual - fitted, useful for diagnosing systematic under- or over-fit before a holdout.
trendFitted trend path before forecast-time external trend adjustments.
trend_movementChange in fitted trend versus the previous training row for that series.
fitted_movementChange in fitted total value versus the previous training row for that series.
componentsNamed component contributions, including seasonal_total, yearly, weekly, daily, custom seasonalities, event totals, and regressor totals.

history_components_frame() and components_frame(horizon) keep top-level fields and expand nested components with dotted column names:

ColumnDiagnostic use
trendPlot the fitted level/trend path against actual to see whether the model is tracking the lane's current demand level.
trend_movementCompare week-over-week trend movement with the last observed week-over-week target movement. Near-zero movement during a rising lane usually means the trend is too stiff.
components.seasonal_totalCheck whether seasonality is doing most of the work or offsetting trend in the wrong direction.
components.yearly, components.weekly, components.dailyInspect built-in Fourier seasonal effects separately when the aggregate seasonal total looks plausible but one cadence is wrong.
components.events.*Verify that holiday or event windows explain known calendar shocks instead of forcing the trend to absorb them.
components.regressors.*Verify known future or historical regressor contribution and sign.

For whole-dataset transformations, concatenate the historical and future component frames after adding any project-specific split label:

import pandas as pd

history_frame = model.history_components_frame().assign(split="history")
future_frame = model.components_frame(14).assign(split="forecast")
component_frame = pd.concat([history_frame, future_frame], ignore_index=True)

| residual | Identify whether the model is already biased immediately before the holdout. |

For a weekly backtest with 12 cutoffs, run the model once per cutoff, holding out one additional week each time, then persist both the holdout predictions and history_components_frame() from that cutoff fit. When Prophet beats CartoBoost with a negative bias, inspect the last several historical component rows before each cutoff:

diagnostics_by_cutoff = {}

for cutoff, train in weekly_cutoff_training_frames:
model = PiecewiseLinearSeasonalForecaster(
n_changepoints=25,
weekly_fourier_order=3,
changepoint_range=1.0,
).fit(train)
diagnostics_by_cutoff[str(cutoff)] = {
"history_components": model.history_components_frame(),
"forecast_components": model.components(1)["records"],
"forecast": model.predict(1).to_pandas(),
}

To recreate a trend/seasonality comparison table, concatenate the stored history frames with the cutoff label:

import pandas as pd

history = pd.concat(
frame.assign(cutoff=cutoff)
for cutoff, frame in diagnostics_by_cutoff.items()
)

trend_columns = [
"cutoff",
"series_id",
"timestamp",
"actual",
"fitted",
"residual",
"trend",
"trend_movement",
"components.seasonal_total",
]

trend_table = history[trend_columns].sort_values(
["cutoff", "series_id", "timestamp"]
)

If trend_movement is near zero while the last observed weeks are rising, the trend is too stiff for the lane. If seasonal_total is large and offsetting the trend in the wrong direction, inspect the cadence and Fourier order. If recent residuals are consistently positive before the cutoff, the model is under-predicting the lane before it ever reaches the holdout; consider fewer or better placed changepoints, explicit event/regressor inputs, or an opt-in residual shock rather than treating the holdout error as a pure forecast-horizon problem.

CartoBoost defaults to n_changepoints=25 and changepoint_range=1.0. This keeps the automatic changepoint count aligned with Prophet's public default but allows late trend breaks across the full training window, which matters for one-week-ahead lane nowcasts where the most recent movement carries the holdout. Use changepoint_range=0.8 only when you intentionally want Prophet's stricter placement window for an apples-to-apples tuning probe.

The embedded example exposes piecewise_linear_seasonal for a quick forecast run. The full plotting workflow can request forecast components and fitted history diagnostics, then plot trend, fitted values, residuals, built-in seasonalities, custom seasonalities, event windows, regressors, aggregate non-trend totals, fitted movement, and trend movement.

Validation

Select changepoints, seasonalities, events, and regressors using training-side rolling origins. Compare against seasonal naive and a simpler trend model on identical rows. Report errors by horizon and inspect residuals around events.

Limitations

  • Additive components can miss interactions between location, event, and seasonality.
  • Too many changepoints or Fourier terms can overfit short histories.
  • Future regressors must be known at forecast time.
  • Irregular timestamps and missing targets require explicit handling.