Quickstart¶
Estimate a trend¶
Smooth the series, then differentiate the smooth:
import numpy as np
import pandas as pd
from incline import sgolay_trend
df = pd.DataFrame(
{"value": np.linspace(0, 10, 100) + np.random.normal(0, 0.5, 100)},
index=pd.date_range("2020-01-01", periods=100),
)
result = sgolay_trend(df, window_length=15, degree=3)
result[["smoothed_value", "derivative_value"]].head()
The derivative is reported per unit of the time axis — per day for a daily
DatetimeIndex, per unit of your time column if you pass one.
Ask for uncertainty¶
An estimate without a standard error is hard to act on. Pass with_uncertainty=True:
result = sgolay_trend(df, with_uncertainty=True)
result[
[
"derivative_value",
"derivative_standard_error",
"derivative_ci_lower",
"derivative_ci_upper",
"significant_trend",
"uncertainty_method",
]
].head()
It is opt-in because the exact route costs one smoother evaluation per observation, and that should be a decision rather than a surprise.
significant_trend is True where the interval excludes zero — where the data
support saying the series is moving at all.
The output columns¶
Every estimator returns the same columns, whichever method produced them:
Column |
Meaning |
|---|---|
|
The fitted curve |
|
Its derivative, per unit time |
|
Which smoother ran |
|
Which derivative this is |
|
Standard error, or NaN |
|
Interval bounds, or NaN |
|
|
|
Interval confidence level, or NaN |
|
Whether the interval covers the whole curve at once |
|
Whether pilot-fit bias correction was applied |
|
Whether the interval excludes zero |
A NaN standard error paired with uncertainty_method of None is a deliberate, documented
state — never a missing column — so downstream code can always index it.
Smoothers add their own settings as extra columns: window_length for
Savitzky-Golay, span for LOESS, bandwidth for local polynomials, and so on.
Choosing a method¶
from incline import (
naive_trend, # central differences; the baseline to beat
sgolay_trend, # local polynomial on a fixed window
local_polynomial_trend, # kernel-weighted local regression
loess_trend, # LOESS
smoothing_spline_trend, # cubic smoothing spline
l1_trend_filter, # piecewise-polynomial with sparse kinks
gp_trend, # Gaussian process
kalman_trend, # local linear trend state-space model
)
If you have no strong preference, use the default smoothing spline:
from incline import estimate_trend
result = estimate_trend(df, with_uncertainty=True)
Pass method= explicitly when the scientific question calls for another
smoother. For example, use method="local_poly" for a fixed local-polynomial
operator or method="l1_filter" for piecewise-polynomial trends with sparse
kinks.
L1 trend filtering requires an explicit smoothing choice. Use an absolute
penalty, or use penalty_fraction to express it relative to the exact
polynomial-saturation threshold:
piecewise = l1_trend_filter(
df,
penalty_fraction=chosen_penalty_fraction,
)
Select the penalty against the feature scale and out-of-sample behavior you care about; the package does not hide an arbitrary choice behind a default.
Second derivatives¶
acceleration = sgolay_trend(df, derivative_order=2, window_length=21)
Not every method supports every order — a Matérn-3/2 Gaussian process is only once differentiable, and asking for more raises rather than returning a number.
An explicit time column¶
When the index is not the time axis:
df = pd.DataFrame({"t": [0.0, 1.5, 2.0, 4.5], "value": [1.0, 2.0, 2.5, 5.0]})
result = sgolay_trend(df, value_column="value", time_column="t")
Ranking many series¶
from incline import trending, estimate, SavitzkyGolay
estimates = {
name: estimate(SavitzkyGolay(), series, with_uncertainty=True)
for name, series in your_series.items()
}
ranked = trending(estimates, window_length=5, aggregation="mean")
ranked[["id", "trend", "trend_standard_error", "significant", "rank"]]
Standard errors computed upstream are carried through, so the ranking can tell you which of the leaders are actually distinguishable from flat.