christiangeorgelucas/time-series-tools
v0.1.1PythonThe 'time-series-tools' package provides a suite of composable nodes for time-series analysis, including decomposition, stationarity tests, and forecasting methods. It wraps the statsmodels library to facilitate robust analysis and modeling of time-series data, ensuring that each node is stateless and deterministic for consistent results.
Published by Christian George Lucas· MIT
Use cases
- •Analyze seasonal trends in sales data
- •Test for stationarity in economic indicators
- •Forecast future stock prices using ARIMA
- •Decompose time-series data for better insights
- •Evaluate Granger causality between two time-series
Nodes (11)
The Acf node computes the sample autocorrelation function of a given time series at specified lags, providing insights into the correlation of the series with its delayed copies. It can also include Bartlett confidence intervals to assess the statistical significance of the autocorrelations.
View source
from statsmodels.tsa.stattools import acf as sm_acf
from gen.messages_pb2 import AcfInput, CorrelogramResult
from gen.axiom_context import AxiomContext
from nodes._common import err, to_pylist, validate_series
def acf(ax: AxiomContext, input: AcfInput) -> CorrelogramResult:
"""Compute the sample autocorrelation function of a series at lags
0..nlags, optionally with Bartlett confidence intervals, wrapping
statsmodels.tsa.stattools.acf.
"""
values = list(input.values)
e = validate_series(values, min_len=2)
if e:
return CorrelogramResult(error=e)
nlags = input.nlags if input.nlags > 0 else None
if nlags is not None and nlags >= len(values):
return CorrelogramResult(error=err("INVALID_ARGUMENT", f"nlags ({nlags}) must be less than the series length ({len(values)})"))
alpha = input.alpha if input.alpha > 0 else None
if alpha is not None and not (0 < alpha < 1):
return CorrelogramResult(error=err("INVALID_ARGUMENT", f"alpha must be in (0, 1), got {alpha}"))
try:
if alpha is not None:
vals, confint = sm_acf(values, nlags=nlags, fft=input.fft, alpha=alpha)
else:
vals = sm_acf(values, nlags=nlags, fft=input.fft, alpha=None)
confint = None
except Exception as exc:
return CorrelogramResult(error=err("COMPUTE_ERROR", str(exc)))
result = CorrelogramResult(values=to_pylist(vals))
if confint is not None:
result.conf_lower.extend(float(c[0]) for c in confint)
result.conf_upper.extend(float(c[1]) for c in confint)
return result
The AdfTest node executes the Augmented Dickey-Fuller test to determine if a time series is stationary, with the null hypothesis indicating non-stationarity. It provides the test statistic, p-value, lags used, critical values at various significance levels, and a verdict on stationarity at the 5% level.
View source
from statsmodels.tsa.stattools import adfuller
from gen.messages_pb2 import AdfInput, AdfResult
from gen.axiom_context import AxiomContext
from nodes._common import err, validate_series
def adf_test(ax: AxiomContext, input: AdfInput) -> AdfResult:
"""Run the Augmented Dickey-Fuller test for a unit root — the null
hypothesis is that the series is NON-stationary — wrapping
statsmodels.tsa.stattools.adfuller.
"""
values = list(input.values)
e = validate_series(values, min_len=4)
if e:
return AdfResult(error=e)
regression = (input.regression or "c").lower()
if regression not in ("c", "ct", "ctt", "n"):
return AdfResult(error=err("INVALID_ARGUMENT", f"unknown regression '{input.regression}', expected 'c', 'ct', 'ctt', or 'n'"))
autolag_raw = input.autolag or "AIC"
autolag = None if autolag_raw.lower() == "none" else autolag_raw
if autolag is not None and autolag.upper() not in ("AIC", "BIC", "T-STAT"):
return AdfResult(error=err("INVALID_ARGUMENT", f"unknown autolag '{input.autolag}', expected 'AIC', 'BIC', 't-stat', or 'none'"))
maxlag = input.maxlag if input.HasField("maxlag") else None
if maxlag is not None and maxlag < 0:
return AdfResult(error=err("INVALID_ARGUMENT", f"maxlag must be >= 0, got {maxlag}"))
if maxlag is not None and maxlag >= len(values) // 2:
return AdfResult(error=err("INVALID_ARGUMENT", f"maxlag ({maxlag}) too large for a series of length {len(values)}"))
try:
stat, pvalue, used_lag, nobs, crit, *_ = adfuller(values, maxlag=maxlag, regression=regression, autolag=autolag)
except Exception as exc:
return AdfResult(error=err("COMPUTE_ERROR", str(exc)))
return AdfResult(
statistic=float(stat),
p_value=float(pvalue),
used_lag=int(used_lag),
nobs=int(nobs),
critical_value_1pct=float(crit.get("1%", float("nan"))),
critical_value_5pct=float(crit.get("5%", float("nan"))),
critical_value_10pct=float(crit.get("10%", float("nan"))),
stationary=bool(pvalue < 0.05),
)
The ArimaForecast node fits a Seasonal ARIMA model to time series data using maximum likelihood estimation and generates forecasts for a specified number of future steps. It returns the forecasted values along with in-sample fitted values, model selection criteria (AIC/BIC/SSE), and a 95% confidence interval for the forecasts.
View source
import numpy as np
from statsmodels.tsa.arima.model import ARIMA
from gen.messages_pb2 import ArimaInput, ForecastResult
from gen.axiom_context import AxiomContext
from nodes._common import MAX_DIFF_ORDER, MAX_ORDER, err, to_pylist, validate_series
_VALID_TRENDS = {"n", "c", "t", "ct"}
def arima_forecast(ax: AxiomContext, input: ArimaInput) -> ForecastResult:
"""Fit a (Seasonal) ARIMA model by maximum likelihood and forecast
forward `horizon` steps, wrapping statsmodels.tsa.arima.model.ARIMA.
"""
values = list(input.values)
e = validate_series(values, min_len=8)
if e:
return ForecastResult(error=e)
p, d, q = input.p, input.d, input.q
sp, sd, sq, sper = input.seasonal_p, input.seasonal_d, input.seasonal_q, input.seasonal_period
for name, v, cap in (
("p", p, MAX_ORDER), ("q", q, MAX_ORDER), ("d", d, MAX_DIFF_ORDER),
("seasonal_p", sp, MAX_ORDER), ("seasonal_q", sq, MAX_ORDER), ("seasonal_d", sd, MAX_DIFF_ORDER),
):
if v < 0 or v > cap:
return ForecastResult(error=err("INVALID_ARGUMENT", f"{name} must be in [0, {cap}], got {v}"))
if sper < 0:
return ForecastResult(error=err("INVALID_ARGUMENT", f"seasonal_period out of range: {sper}"))
if (sp or sd or sq) and sper < 2:
return ForecastResult(error=err("INVALID_ARGUMENT", "seasonal_period must be >= 2 when a seasonal order is set"))
horizon = input.horizon if input.horizon > 0 else 1
trend = input.trend or None
if trend is not None and trend not in _VALID_TRENDS:
return ForecastResult(error=err("INVALID_ARGUMENT", f"unknown trend '{input.trend}', expected one of {sorted(_VALID_TRENDS)}"))
seasonal_order = (sp, sd, sq, sper) if sper >= 2 else (0, 0, 0, 0)
try:
model = ARIMA(values, order=(p, d, q), seasonal_order=seasonal_order, trend=trend)
res = model.fit()
fc = res.get_forecast(steps=horizon)
mean = np.asarray(fc.predicted_mean, dtype=float)
ci = np.asarray(fc.conf_int(alpha=0.05), dtype=float)
fitted = np.asarray(res.fittedvalues, dtype=float)
resid = np.asarray(res.resid, dtype=float)
except Exception as exc:
return ForecastResult(error=err("COMPUTE_ERROR", str(exc)))
return ForecastResult(
forecast=to_pylist(mean),
fitted=to_pylist(fitted),
aic=float(res.aic),
bic=float(res.bic),
sse=float(np.nansum(resid ** 2)),
conf_lower=to_pylist(ci[:, 0]),
conf_upper=to_pylist(ci[:, 1]),
)
The Decompose node splits a time series into its trend, seasonal, and residual components using either STL (Seasonal-Trend decomposition using LOESS) or classical moving-average decomposition. It requires at least two full seasonal periods of data and ensures that multiplicative decomposition is applied only to strictly positive values.
View source
import numpy as np
from statsmodels.tsa.seasonal import STL, seasonal_decompose
from gen.messages_pb2 import DecomposeInput, DecomposeResult
from gen.axiom_context import AxiomContext
from nodes._common import err, to_pylist, validate_series
def decompose(ax: AxiomContext, input: DecomposeInput) -> DecomposeResult:
"""Split a series into trend, seasonal, and residual components via STL
(Seasonal-Trend decomposition using LOESS) or classical moving-average
decomposition, wrapping statsmodels.tsa.seasonal.STL / seasonal_decompose.
Needs at least two full seasonal periods of data.
"""
values = list(input.values)
e = validate_series(values, min_len=4)
if e:
return DecomposeResult(error=e)
method = (input.method or "stl").lower()
if method not in ("stl", "classical"):
return DecomposeResult(error=err("INVALID_ARGUMENT", f"unknown method '{input.method}', expected 'stl' or 'classical'"))
period = input.period
if period < 2:
return DecomposeResult(error=err("INVALID_ARGUMENT", f"period must be >= 2, got {period}"))
if len(values) < 2 * period:
return DecomposeResult(error=err("INVALID_INPUT", f"need at least 2*period ({2 * period}) observations, got {len(values)}"))
arr = np.asarray(values, dtype=float)
try:
if method == "stl":
res = STL(arr, period=period, robust=input.robust).fit()
observed, trend, seasonal, resid = arr, res.trend, res.seasonal, res.resid
else:
model = (input.model or "additive").lower()
if model not in ("additive", "multiplicative"):
return DecomposeResult(error=err("INVALID_ARGUMENT", f"unknown model '{input.model}', expected 'additive' or 'multiplicative'"))
if model == "multiplicative" and np.any(arr <= 0):
return DecomposeResult(error=err("INVALID_ARGUMENT", "multiplicative decomposition requires all values > 0"))
res = seasonal_decompose(arr, model=model, period=period)
observed, trend, seasonal, resid = res.observed, res.trend, res.seasonal, res.resid
except Exception as exc:
return DecomposeResult(error=err("COMPUTE_ERROR", str(exc)))
return DecomposeResult(
observed=to_pylist(observed),
trend=to_pylist(trend),
seasonal=to_pylist(seasonal),
resid=to_pylist(resid),
)
The Detrend node removes a constant (the sample mean) or the best-fit linear trend from a given series, allowing for further analysis without these trends. It outputs a canonical Series envelope, enabling seamless integration with other nodes.
View source
import numpy as np
from statsmodels.tsa.tsatools import detrend as sm_detrend
from gen.messages_pb2 import DetrendInput, Series
from gen.axiom_context import AxiomContext
from nodes._common import err, to_pylist, validate_series
def detrend(ax: AxiomContext, input: DetrendInput) -> Series:
"""Remove a constant (the sample mean) or best-fit linear trend from a
series, wrapping statsmodels.tsa.tsatools.detrend. Emits the canonical
Series envelope, so it chains directly into a second Difference/Detrend
call or into any other node's `values` field.
"""
values = list(input.values)
e = validate_series(values, min_len=2)
if e:
return Series(error=e)
method = (input.method or "linear").lower()
if method not in ("linear", "constant"):
return Series(error=err("INVALID_ARGUMENT", f"unknown method '{input.method}', expected 'linear' or 'constant'"))
order = 1 if method == "linear" else 0
arr = np.asarray(values, dtype=float)
try:
out = sm_detrend(arr, order=order)
except Exception as exc:
return Series(error=err("COMPUTE_ERROR", str(exc)))
return Series(values=to_pylist(out))
The Difference node applies non-seasonal and seasonal differencing to a time series, transforming it from a non-stationary to a stationary series, which is essential for accurate modeling. It outputs a canonical Series that can be directly used in subsequent nodes or additional differencing operations.
View source
import numpy as np
from gen.messages_pb2 import DifferenceInput, Series
from gen.axiom_context import AxiomContext
from nodes._common import MAX_DIFF_ORDER, err, to_pylist, validate_series
def _diff_once(arr: np.ndarray, lag: int) -> np.ndarray:
return arr[lag:] - arr[:-lag]
def difference(ax: AxiomContext, input: DifferenceInput) -> Series:
"""Apply non-seasonal and/or seasonal differencing to a series — the
standard transform for turning a non-stationary series into a stationary
one before modeling. Seasonal differencing (lag = seasonal_periods) is
applied first, then non-seasonal (lag 1) differencing; since both are
linear filters the two commute, so this ordering does not affect the
result. Emits the canonical Series envelope, so it chains directly into
a second Difference/Detrend call or into any other node's `values`
field.
"""
values = list(input.values)
e = validate_series(values, min_len=2)
if e:
return Series(error=e)
order = input.order if input.HasField("order") else 1
if order < 0:
return Series(error=err("INVALID_ARGUMENT", f"order must be >= 0, got {order}"))
if order > MAX_DIFF_ORDER:
return Series(error=err("LIMIT_EXCEEDED", f"order {order} exceeds the {MAX_DIFF_ORDER} limit"))
seasonal_periods = input.seasonal_periods
seasonal_order = 0
if seasonal_periods > 0:
seasonal_order = input.seasonal_order if input.seasonal_order > 0 else 1
if seasonal_order > MAX_DIFF_ORDER:
return Series(error=err("LIMIT_EXCEEDED", f"seasonal_order {seasonal_order} exceeds the {MAX_DIFF_ORDER} limit"))
elif input.seasonal_order > 0:
return Series(error=err("INVALID_ARGUMENT", "seasonal_order was set but seasonal_periods was not"))
min_needed = order + seasonal_order * max(seasonal_periods, 1) + 1
if len(values) < min_needed:
return Series(error=err("INVALID_INPUT", f"need at least {min_needed} observations for this differencing configuration, got {len(values)}"))
out = np.asarray(values, dtype=float)
try:
for _ in range(seasonal_order):
out = _diff_once(out, seasonal_periods)
for _ in range(order):
out = _diff_once(out, 1)
except Exception as exc:
return Series(error=err("COMPUTE_ERROR", str(exc)))
return Series(values=to_pylist(out))
The ExponentialSmoothingForecast node fits a Holt-Winters exponential smoothing model to time series data, allowing for optional additive or multiplicative trends and seasonal components. It forecasts future values for a specified horizon while providing in-sample fitted values and fit diagnostics.
View source
import numpy as np
from statsmodels.tsa.holtwinters import ExponentialSmoothing
from gen.messages_pb2 import EtsInput, ForecastResult
from gen.axiom_context import AxiomContext
from nodes._common import err, to_pylist, validate_series
_TREND_MAP = {"none": None, "add": "add", "mul": "mul"}
_SEASONAL_MAP = {"none": None, "add": "add", "mul": "mul"}
def exponential_smoothing_forecast(ax: AxiomContext, input: EtsInput) -> ForecastResult:
"""Fit a Holt-Winters exponential smoothing model — optional additive or
multiplicative trend (with damping) and seasonal components — by
minimizing sum-of-squared-errors, and forecast forward `horizon` steps,
wrapping statsmodels.tsa.holtwinters.ExponentialSmoothing.
"""
values = list(input.values)
e = validate_series(values, min_len=4)
if e:
return ForecastResult(error=e)
trend_key = (input.trend or "none").lower()
seasonal_key = (input.seasonal or "none").lower()
if trend_key not in _TREND_MAP:
return ForecastResult(error=err("INVALID_ARGUMENT", f"unknown trend '{input.trend}', expected 'none', 'add', or 'mul'"))
if seasonal_key not in _SEASONAL_MAP:
return ForecastResult(error=err("INVALID_ARGUMENT", f"unknown seasonal '{input.seasonal}', expected 'none', 'add', or 'mul'"))
trend = _TREND_MAP[trend_key]
seasonal = _SEASONAL_MAP[seasonal_key]
seasonal_periods = input.seasonal_periods if seasonal else None
if seasonal and seasonal_periods < 2:
return ForecastResult(error=err("INVALID_ARGUMENT", "seasonal_periods must be >= 2 when seasonal is set"))
if seasonal and len(values) < 2 * seasonal_periods:
return ForecastResult(error=err("INVALID_INPUT", f"need at least 2*seasonal_periods ({2 * seasonal_periods}) observations, got {len(values)}"))
arr = np.asarray(values, dtype=float)
if seasonal == "mul" and np.any(arr <= 0):
return ForecastResult(error=err("INVALID_ARGUMENT", "multiplicative seasonal component requires all values > 0"))
if trend == "mul" and np.any(arr <= 0):
return ForecastResult(error=err("INVALID_ARGUMENT", "multiplicative trend requires all values > 0"))
horizon = input.horizon if input.horizon > 0 else 1
damped = bool(input.damped_trend) and trend is not None
try:
model = ExponentialSmoothing(
arr, trend=trend, seasonal=seasonal, seasonal_periods=seasonal_periods,
damped_trend=damped, initialization_method="estimated",
)
res = model.fit()
forecast = np.asarray(res.forecast(horizon), dtype=float)
fitted = np.asarray(res.fittedvalues, dtype=float)
except Exception as exc:
return ForecastResult(error=err("COMPUTE_ERROR", str(exc)))
return ForecastResult(
forecast=to_pylist(forecast),
fitted=to_pylist(fitted),
aic=float(res.aic),
bic=float(res.bic),
sse=float(res.sse),
)
The GrangerCausality node tests whether the historical values of a predictor time series can improve the forecasting of a target time series beyond the target's own past values, evaluating this relationship at multiple lags up to a specified maximum. It provides statistical evidence of precedence rather than definitive causation.
View source
import contextlib
import io
import numpy as np
from statsmodels.tsa.stattools import grangercausalitytests
from gen.messages_pb2 import GrangerInput, GrangerResult
from gen.axiom_context import AxiomContext
from nodes._common import MAX_ORDER, err, validate_series
_MAX_MAXLAG = MAX_ORDER * 2
def granger_causality(ax: AxiomContext, input: GrangerInput) -> GrangerResult:
"""Test whether the past of `predictor` helps forecast `target` beyond
target's own past, at every lag from 1 to maxlag, wrapping
statsmodels.tsa.stattools.grangercausalitytests. `target` and `predictor`
must be the same length and time-aligned. A statistical precedence test,
not proof of causation.
"""
target = list(input.target)
predictor = list(input.predictor)
e = validate_series(target, min_len=4, name="target")
if e:
return GrangerResult(error=e)
e = validate_series(predictor, min_len=4, name="predictor")
if e:
return GrangerResult(error=e)
if len(target) != len(predictor):
return GrangerResult(error=err("INVALID_INPUT", f"target ({len(target)}) and predictor ({len(predictor)}) must be the same length"))
maxlag = input.maxlag
if maxlag < 1:
return GrangerResult(error=err("INVALID_ARGUMENT", f"maxlag must be >= 1, got {maxlag}"))
if maxlag > _MAX_MAXLAG:
return GrangerResult(error=err("LIMIT_EXCEEDED", f"maxlag {maxlag} exceeds the {_MAX_MAXLAG} limit"))
n = len(target)
if maxlag >= n // 2:
return GrangerResult(error=err("INVALID_ARGUMENT", f"maxlag ({maxlag}) too large for a series of length {n}"))
addconst = input.addconst if input.HasField("addconst") else True
arr = np.column_stack([target, predictor])
buf = io.StringIO()
try:
with contextlib.redirect_stdout(buf):
results = grangercausalitytests(arr, maxlag=maxlag, addconst=addconst)
except Exception as exc:
return GrangerResult(error=err("COMPUTE_ERROR", str(exc)))
lags_out, f_stat, f_pval, chi2_pval, lr_pval = [], [], [], [], []
best_lag, best_p = 0, None
for lag in range(1, maxlag + 1):
test = results[lag][0]
fstat, fpval, _, _ = test["ssr_ftest"]
_, chi2p, _ = test["ssr_chi2test"]
_, lrp, _ = test["lrtest"]
lags_out.append(lag)
f_stat.append(float(fstat))
f_pval.append(float(fpval))
chi2_pval.append(float(chi2p))
lr_pval.append(float(lrp))
if best_p is None or fpval < best_p:
best_p, best_lag = fpval, lag
return GrangerResult(
lags=lags_out,
ssr_ftest_stat=f_stat,
ssr_ftest_pvalue=f_pval,
ssr_chi2_pvalue=chi2_pval,
lr_pvalue=lr_pval,
best_lag=best_lag,
)
The KpssTest node executes the KPSS test for stationarity, determining whether a given time series is stationary based on the null hypothesis that it is. It provides the test statistic, interpolated p-value, lags used, critical values, and a verdict on stationarity at the 5% significance level.
View source
import warnings
from statsmodels.tsa.stattools import kpss
from gen.messages_pb2 import KpssInput, KpssResult
from gen.axiom_context import AxiomContext
from nodes._common import err, validate_series
def kpss_test(ax: AxiomContext, input: KpssInput) -> KpssResult:
"""Run the KPSS test for stationarity — the null hypothesis is that the
series IS stationary, the opposite null from AdfTest — wrapping
statsmodels.tsa.stattools.kpss.
"""
values = list(input.values)
e = validate_series(values, min_len=4)
if e:
return KpssResult(error=e)
regression = (input.regression or "c").lower()
if regression not in ("c", "ct"):
return KpssResult(error=err("INVALID_ARGUMENT", f"unknown regression '{input.regression}', expected 'c' or 'ct'"))
nlags = "auto" if input.lags <= 0 else input.lags
if isinstance(nlags, int) and nlags >= len(values):
return KpssResult(error=err("INVALID_ARGUMENT", f"lags ({nlags}) must be less than the series length ({len(values)})"))
try:
# statsmodels warns when the statistic falls outside its tabulated
# p-value range and clips the p-value instead — that clipped p-value
# (still meaningful, just an interval bound) is exactly what we
# return, so the warning is expected noise, not a signal to surface.
with warnings.catch_warnings():
warnings.simplefilter("ignore")
stat, pvalue, lags_used, crit = kpss(values, regression=regression, nlags=nlags)
except Exception as exc:
return KpssResult(error=err("COMPUTE_ERROR", str(exc)))
return KpssResult(
statistic=float(stat),
p_value=float(pvalue),
lags_used=int(lags_used),
critical_value_10pct=float(crit.get("10%", float("nan"))),
critical_value_5pct=float(crit.get("5%", float("nan"))),
critical_value_2_5pct=float(crit.get("2.5%", float("nan"))),
critical_value_1pct=float(crit.get("1%", float("nan"))),
stationary=bool(pvalue >= 0.05),
)
The LjungBoxTest node evaluates whether a series of autocorrelations, typically from a model's residuals, are jointly zero, indicating that the series behaves like white noise at specified lags. It can also provide the Box-Pierce statistic as an optional output.
View source
from statsmodels.stats.diagnostic import acorr_ljungbox
from gen.messages_pb2 import LjungBoxInput, LjungBoxResult
from gen.axiom_context import AxiomContext
from nodes._common import err, to_pylist, validate_series
def ljung_box_test(ax: AxiomContext, input: LjungBoxInput) -> LjungBoxResult:
"""Test whether a group of autocorrelations (typically a model's
residuals) are jointly zero — i.e. whether the series looks like white
noise — at one or more lags, optionally also reporting the older
Box-Pierce statistic, wrapping
statsmodels.stats.diagnostic.acorr_ljungbox.
"""
values = list(input.values)
e = validate_series(values, min_len=2)
if e:
return LjungBoxResult(error=e)
lags = list(input.lags) if len(input.lags) > 0 else None
if lags is not None:
for lag in lags:
if lag < 1 or lag >= len(values):
return LjungBoxResult(error=err("INVALID_ARGUMENT", f"lag {lag} out of range [1, {len(values) - 1}]"))
if input.model_df < 0:
return LjungBoxResult(error=err("INVALID_ARGUMENT", f"model_df must be >= 0, got {input.model_df}"))
try:
df = acorr_ljungbox(values, lags=lags, boxpierce=input.box_pierce, model_df=input.model_df)
except Exception as exc:
return LjungBoxResult(error=err("COMPUTE_ERROR", str(exc)))
result = LjungBoxResult(
lags=[int(x) for x in df.index.tolist()],
lb_stat=to_pylist(df["lb_stat"].to_numpy()),
lb_pvalue=to_pylist(df["lb_pvalue"].to_numpy()),
)
if input.box_pierce:
result.bp_stat.extend(to_pylist(df["bp_stat"].to_numpy()))
result.bp_pvalue.extend(to_pylist(df["bp_pvalue"].to_numpy()))
return result
The Pacf node computes the sample partial autocorrelation function for a given time series across specified lags, allowing for the inclusion of confidence intervals. This node serves as a standard diagnostic tool for determining the order of autoregressive models.
View source
from statsmodels.tsa.stattools import pacf as sm_pacf
from gen.messages_pb2 import PacfInput, CorrelogramResult
from gen.axiom_context import AxiomContext
from nodes._common import err, to_pylist, validate_series
_VALID_METHODS = {"ywadjusted", "ywm", "ols", "ld"}
def pacf(ax: AxiomContext, input: PacfInput) -> CorrelogramResult:
"""Compute the sample partial autocorrelation function of a series at
lags 0..nlags, optionally with confidence intervals, wrapping
statsmodels.tsa.stattools.pacf.
"""
values = list(input.values)
e = validate_series(values, min_len=3)
if e:
return CorrelogramResult(error=e)
method = (input.method or "ywadjusted").lower()
if method not in _VALID_METHODS:
return CorrelogramResult(error=err("INVALID_ARGUMENT", f"unknown method '{input.method}', expected one of {sorted(_VALID_METHODS)}"))
n = len(values)
nlags = input.nlags if input.nlags > 0 else None
if nlags is not None and nlags >= n:
return CorrelogramResult(error=err("INVALID_ARGUMENT", f"nlags ({nlags}) must be less than the series length ({n})"))
alpha = input.alpha if input.alpha > 0 else None
if alpha is not None and not (0 < alpha < 1):
return CorrelogramResult(error=err("INVALID_ARGUMENT", f"alpha must be in (0, 1), got {alpha}"))
try:
if alpha is not None:
vals, confint = sm_pacf(values, nlags=nlags, method=method, alpha=alpha)
else:
vals = sm_pacf(values, nlags=nlags, method=method, alpha=None)
confint = None
except Exception as exc:
return CorrelogramResult(error=err("COMPUTE_ERROR", str(exc)))
result = CorrelogramResult(values=to_pylist(vals))
if confint is not None:
result.conf_lower.extend(float(c[0]) for c in confint)
result.conf_upper.extend(float(c[1]) for c in confint)
return result
Messages (20)
Download .protoThe input time series data for which the autocorrelation function is to be computed.
The number of lags at which to compute the autocorrelation, with a default range from 0 to nlags.
A boolean indicating whether to use the Fast Fourier Transform to compute the autocorrelation.
The significance level for the confidence intervals, which must be between 0 and 1.
An array of numerical values representing the time series to be tested for stationarity.
The maximum number of lags to include in the test; must be non-negative and less than half the length of the time series.
Specifies the type of regression to use in the test, with options including 'c' for constant, 'ct' for constant and trend, 'ctt' for constant, trend, and trend squared, or 'n' for no regression.
Determines the method for selecting the lag length, with options such as 'AIC', 'BIC', 'T-STAT', or 'none' to disable automatic lag selection.
The input time series data to which the ARIMA model will be fitted.
The autoregressive order of the ARIMA model.
The degree of differencing required to make the time series stationary.
The moving average order of the ARIMA model.
The seasonal autoregressive order for the Seasonal ARIMA model.
The seasonal differencing order for the Seasonal ARIMA model.
The seasonal moving average order for the Seasonal ARIMA model.
The number of periods in each season for seasonal effects.
The number of steps ahead to forecast.
The type of trend component to include in the model, if any.
The input time series data for which the autocorrelation function is to be computed.
A list of numerical values representing the time series data to be decomposed.
The decomposition method to use, either 'stl' for Seasonal-Trend decomposition using LOESS or 'classical' for classical moving-average decomposition.
The number of observations that constitute one seasonal period, which must be at least 2.
The model type for classical decomposition, either 'additive' or 'multiplicative'.
A boolean indicating whether to use a robust version of the STL method, which is less sensitive to outliers.
The input series of numerical values to be detrended.
The method used for detrending, which can be 'linear' for a linear trend or 'constant' for the sample mean.
The input time series data that will undergo differencing.
The number of non-seasonal differencing operations to apply.
The number of periods in a season for seasonal differencing.
The number of seasonal differencing operations to apply.
An array of numerical values representing the time series data to be analyzed.
Specifies the type of trend to be used in the model, which can be 'none', 'add', or 'mul'.
Indicates the type of seasonal component, which can be 'none', 'add', or 'mul'.
The number of periods in a seasonal cycle, required when a seasonal component is specified.
A boolean indicating whether to use a damped trend in the model.
The number of steps ahead for which to forecast future values.
The time series that is being predicted.
The time series that is being tested for its predictive power over the target.
The maximum number of lags to consider when testing for Granger causality.
A boolean indicating whether to include a constant term in the regression model.
An array of numerical values representing the time series data to be tested for stationarity.
A string indicating the type of regression to use in the test, either 'c' for constant or 'ct' for constant and trend.
An integer specifying the number of lags to include in the test; if less than or equal to zero, it defaults to 'auto'.
The test statistic calculated from the KPSS test.
The interpolated p-value associated with the test statistic.
The number of lags that were actually used in the test.
The critical value for the 10% significance level.
The critical value for the 5% significance level.
The critical value for the 2.5% significance level.
The critical value for the 1% significance level.
A boolean indicating whether the series is considered stationary based on the 5% p-value threshold.
A list of numerical values representing the time series data to be tested.
A list of integers specifying the lags at which to test the autocorrelations.
A boolean indicating whether to include the Box-Pierce statistic in the results.
An integer representing the degrees of freedom of the model used in the test.
A list of integers specifying the lags at which to test the autocorrelations.
A list of numerical values representing the time series data for which the partial autocorrelation is to be computed.
The number of lags for which the partial autocorrelation is calculated, with a default of None indicating all lags up to the length of the series minus one.
The method used for computing the partial autocorrelation, which can be one of 'ywadjusted', 'ywm', 'ols', or 'ld'.
The significance level for the confidence intervals, which must be a value between 0 and 1.
The input series of numerical values to be detrended.
Use as a tool in any AI agent
christiangeorgelucas/time-series-tools is callable as a tool from any MCP client (Claude, Cursor, or your own agent) via Axiom's hosted MCP server — no install, no download. Add the server, then your agent can search and invoke it, or pin it as a typed tool.
claude mcp add --transport http axiom https://api.axiomide.com/mcp --header "Authorization: Bearer YOUR_AXIOM_API_KEY"Replace YOUR_AXIOM_API_KEY with a key you create in Console → API Keys.
MCP server setup & other clients →Use this package
Sign in to import christiangeorgelucas/time-series-tools into the graph editor — drop it into a flow and hit run. Or call it directly via the API Reference with your API key.
Sign in to useOpen editor