Recursive¶
tsecon.rec(rng, target, fn) is the higher-order Python analogue of Julia's @rec
macro: walks rng, calling fn(target, t) at each step. tsecon.rec_linear(target,
coeffs, lags, rng) is the specialised closed-form for linear recurrences (AR(p),
Fibonacci, lag polynomials) and routes through a Cython kernel when available;
introspect with rec_linear_is_cython(). See
design/cython_strategy.md for the three-flavour
benchmark.
tsecon.recursive ¶
Recursive (sequential) computation over a time series range.
Mirrors TimeSeriesEcon.jl/src/recursive.jl, which exports the @rec
macro. Python has no parse-time macros, so the Julia surface ::
@rec t=3U:10U s[t] = s[t-1] + s[t-2]
ports to a higher-order function call ::
rec(MITRange(MIT(Unit(), 3), MIT(Unit(), 10)), s,
lambda t: s[t - 1] + s[t - 2])
The semantics match: each step writes target[t] = fn(t) before the
next step runs, so a closure that reads target[t - k] always sees the
freshly-committed value.
For multi-target recurrences (two series updated together each step),
write the explicit for t in rng loop — the wrapper buys nothing once
there is more than one target.
Performance notes
Per-iteration cost is dominated by the lambda call and the
:class:~tsecon.tseries.TSeries __getitem__ / __setitem__
dispatch (each indexing step allocates a fresh :class:~tsecon.mit.MIT
for t - k and walks _ts_values_inds). For a single 80-120 period
recurrence (≈ 20-30 years of quarterly data) this is a few hundred
microseconds — fine. For a data pipeline that runs rec thousands of
times the cumulative cost can dominate, which is exactly the workload
M1.5 will benchmark against Julia's @rec and a specialized Cython
kernel for the linear-recurrence common case (AR(p), Fibonacci, lag
polynomials).
rec ¶
Compute target[t] = fn(t) for each t in rng, in order.
The Python equivalent of TimeSeriesEcon.jl's @rec macro. Each
iteration's write is committed before the next iteration runs, so
fn may reference target[t - k] (or any other series) freely:
by the time step t reads target[t - k], that period has
already been written by a prior step (assuming k >= 1 and the
recurrence is well-defined).
The target is mutated in place. Assignment to an MIT outside
target.range extends the storage via the TSeries auto-resize
rule, matching the Julia @rec behaviour.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rng
|
MITRange
|
The range to iterate over. Its frequency must match
|
required |
target
|
TSeries
|
The series being computed. Mutated in place. |
required |
fn
|
Callable[[MIT], scalar]
|
Returns the value to assign at each step. Typically a lambda
closing over |
required |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
Examples:
Fibonacci over a Unit range::
>>> from tsecon import MIT, MITRange, TSeries, rec
>>> from tsecon.frequencies import Unit
>>> import numpy as np
>>> s = TSeries(MIT(Unit(), 1), np.array([1.0, 1.0]))
>>> rec(MITRange(MIT(Unit(), 3), MIT(Unit(), 10)), s,
... lambda t: s[t - 1] + s[t - 2])
>>> [float(s[MIT(Unit(), i)]) for i in (1, 5, 10)]
[1.0, 5.0, 55.0]
AR(1) over a quarterly range, reading from a second series::
rec(rng, consumption,
lambda t: beta * consumption[t - 1] + (1 - beta) * income[t])
Backcasting over a reversed range (Julia's
@rec t=10U:-1:1U s[t] = s[t+1] - g)::
>>> s = TSeries(MIT(Unit(), 1), np.zeros(10))
>>> s[MIT(Unit(), 10)] = 20.0
>>> rec(MITRange(MIT(Unit(), 9), MIT(Unit(), 1), step=-1), s,
... lambda t: s[t + 1] - 2.0)
>>> s.values.tolist()
[2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0, 18.0, 20.0]
Source code in src/tsecon/recursive.py
rec_linear_is_cython ¶
Return True iff the Cython-compiled rec_linear kernel was importable.
Useful for tests, benchmarks, and diagnostic prints — the public
:func:rec_linear itself is implementation-agnostic. When this
returns False the same calls go through the pure-NumPy kernel
in _rec_kernels.py; behaviour is identical, only speed differs.
Source code in src/tsecon/recursive.py
rec_linear ¶
Compute a linear-combination recurrence over rng in place.
Specialised closed-form sibling of :func:rec for the common case
where each step's value is a fixed linear combination of earlier-
written values of the same series. Covers Fibonacci, AR(p),
arbitrary lag polynomials — the recurrences that account for ~80%
of pipeline workloads per the M1 benchmark.
The body of the loop is::
for t in rng:
target[t] = sum(coeffs[k] * target[t - lags[k]] for k in range(len(coeffs)))
Both forward (rng.step == +1) and backward (rng.step == -1)
iteration are supported. The contract on lags matches the
direction so that every read references an already-written or
initial-condition position:
- Forward (
rng.step == +1, walking fromrng.firstup torng.last): alllags[k] >= 1;targetmust contain valid values for everyrng.first - lags[k]position before the call (the initial conditions at the start). - Backward (
rng.step == -1, walking fromrng.firstdown torng.last): alllags[k] <= -1;targetmust contain valid values for everyrng.first - lags[k]position (=rng.first + |lags[k]|, the initial conditions at the end — this is backcasting).
target is resized in place to cover rng if needed (NaN
padding for new positions inside rng is overwritten by the
recurrence; positions outside rng keep whatever they had,
which is where the initial conditions live).
Dispatch
When the Cython extension tsecon._rec_kernels_cy is importable
(the typical wheel install), this function delegates to the
compiled :func:rec_linear_cython kernel — same contract,
much faster. Without the extension, the call goes through
:func:rec_linear_numpy, the pure-Python reference. Use
:func:rec_linear_is_cython to check which path is active.
Notes
rec_linear is the closed-form pure-linear-AR(p) form:
target[t] = Σ_k coeffs[k] * target[t - lags[k]] for nonzero
lags. It deliberately has no constant term and no exogenous
reads — that narrowing is what enables the Cython kernel path.
The two common near-misses:
target[t] = target[t-1] + c(random walk / cumulative sum with constant drift) is notrec_linearterritory; use :func:tsecon.undiffinstead.target[t] = β * target[t-1] + γ * y[t](AR with exogenous reads from another series) is notrec_linearterritory either; use the general :func:recwith a lambda closing over bothtargetandy.
Both near-misses raise an informative ValueError at the call
site rather than producing silent garbage — the first via the
min(lags) >= 1 contract (lags=[0] for the constant-drift
misread), the second by virtue of the closed-form signature
accepting only coeffs × lags (no second series). The
general :func:rec retains the readable nonlinear / multi-series
escape hatch; pick it whenever the recurrence body does not fit
the linear-AR(p) shape.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
TSeries
|
The series being computed. Must have |
required |
coeffs
|
array-like of float
|
Recurrence weights. Will be coerced to a 1-D |
required |
lags
|
array-like of int
|
Nonzero lag offsets, one per coefficient. Sign must match
|
required |
rng
|
MITRange
|
The range to compute, frequency-matched to |
required |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
Examples:
Fibonacci over a Unit range::
>>> import numpy as np
>>> from tsecon import MIT, MITRange, TSeries, rec_linear
>>> from tsecon.frequencies import Unit
>>> s = TSeries(MIT(Unit(), 1), np.array([1.0, 1.0, 0.0, 0.0, 0.0]))
>>> rec_linear(s, [1.0, 1.0], [1, 2],
... MITRange(MIT(Unit(), 3), MIT(Unit(), 5)))
>>> s.values.tolist()
[1.0, 1.0, 2.0, 3.0, 5.0]
AR(2) recurrence over quarterly data::
rec_linear(consumption, [0.5, 0.3], [1, 2], rng)
Backcasting via a reversed range (Julia's
@rec t=10U:-1:1U s[t] = s[t+1] - g with g = 0 constant
drift becomes s[t] = s[t+1])::
rec_linear(s, [1.0], [-1],
MITRange(MIT(Unit(), 10), MIT(Unit(), 1), step=-1))
See Also
rec : General higher-order form target[t] = fn(t). Use it when
the recurrence is nonlinear or reads from series other than
target.
tsecon.undiff : Inverse of :func:tsecon.diff. Use it for
target[t] = target[t-1] + c (random walk with constant
drift) — the most common near-miss for rec_linear.
Source code in src/tsecon/recursive.py
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 | |