Migration from Julia¶
Work in progress
This page is a stub. An exhaustive idiom table will be folded in here once the tutorial port has settled.
A reader coming from
TimeSeriesEcon.jl will
recognise every concept in tsecon. The list below catalogues the visible
differences — what the spellings, not the semantics, look like.
| Concept | Julia | Python (tsecon) |
|---|---|---|
| Quarterly literal | 2020Q1 |
qq(2020, 1) |
| Monthly literal | 2020M3 |
mm(2020, 3) |
| Yearly literal | 2020Y |
yy(2020) |
| Inclusive range | 2020Q1:2021Q4 |
MITRange(qq(2020, 1), qq(2021, 4)) |
| Frequency-of | frequencyof(t) |
frequency_of(t) |
| In-place op | lag!(x) / lead!(x) |
lag_inplace(x) / lead_inplace(x) |
| Whole-object arithmetic | x + y vs x .+ y |
x + y (one spelling — always element-wise) |
| Dot-broadcast | log.(x) |
np.log(x) |
| Macro recurrence | @rec / @rec(rng, expr) |
tsecon.rec(rng, target, fn) |
| Linear recurrence | @rec-via-AR(p) idiom |
tsecon.rec_linear(target, coeffs, lags, rng) |
| Workspace deletion | delete!(w, :start) |
del w.start |
| Range step | 2000M1:2:2000M8 |
MITRange(mm(2000, 1), mm(2000, 8), step=2) |
| Reversed range (backcast) | 10U:-1:1U |
MITRange(MIT(Unit(),10), MIT(Unit(),1), step=-1) |
| Last element | x[end] |
x[x.lastdate] |
overlay (TSeries first-non-NaN wins) |
overlay(x1, x2, x3) |
overlay(x1, x2, x3) |
overlay (forced range) |
overlay(2020Q1:2020Q4, x1, x2) |
overlay(x1, x2, rng=MITRange(qq(2020,1), qq(2020,4))) |
compare / @compare |
@compare(v1, v2, atol=1e-5) |
compare(v1, v2, atol=1e-5) (returns CompareResult) |
reindex (label shift) |
reindex(t, 2021Q1 => 1U) |
reindex(t, (qq(2021, 1), MIT(Unit(), 1))) |
rangeof with drop= |
rangeof(t, drop=1) |
rangeof(t, drop=1) |
rangeof(workspace; method=) |
rangeof(w, method=union) |
rangeof(w, method="union") |
TSeries(rng, ini::Function) |
TSeries(rng, zeros) / TSeries(rng, rand) |
TSeries(rng, np.zeros) / TSeries(rng, rng_np.random) |
mean(mvts; dims=1) (per-column) |
mean(mvts; dims=1) |
mean(mvts, axis=0) (NumPy convention) |
mean(mvts; dims=2) (per-row) |
mean(mvts; dims=2) |
mean(mvts, axis=1) |
| Matrix product (coefficient-matrix × series) | A * t |
A @ t (PEP 465; returns ndarray) |
| TSeries-of-vectors dot | _vals(t1) * _vals(t2) (raises in Julia) |
t1 @ t2 (inner product scalar) |
| Transpose / adjoint | transpose(t) / adjoint(mvts) |
not ported — use np.asarray(x).T |
| In-place copy Workspace → MVTSeries | copyto!(mvts, w; verbose=, trange=) |
tsecon.copyto(mvts, w, *, verbose=, trange=) |
@weval macro (Workspace-scoped eval) |
@weval w b + a |
w.b + w.a (preferred) or with w.as_namespace() as ns: ns.b + ns.a |
Semantics that are identical¶
- MIT intersection on element-wise ops (
x + y). - Resize-on-assign when an out-of-range MIT key is written to.
- Frequency-mismatch raises on arithmetic between mismatched-frequency series.
fconvert(t, target_freq, method=…)round-trip behaviour (mean / sum / first / last / min / max).
Semantics that subtly differ¶
MIT == intisFalse. Useint(mit)to extract the underlying offset.- Default container fill is NaN, not uninitialised.
TSeries(rng)is filled with NaN; Julia'sTSeries(rng)leaves the buffer at whatever was on the heap. copy=Falseis the default constructor mode, matching xarray and matching the Julia upstream's pass-the-array-by-reference behaviour. Reach forcopy=True(or.copy()) for an alias break.- Recurrences split into two entry points. Julia's
@recis a single parse-time macro that accepts any RHS expression. Python lacks parse-time macros, sotseconships two entry points:rec(rng, target, fn)for the general higher-order form (any nonlinear / multi-series body, expressed as alambda), andrec_linear(target, coeffs, lags, rng)for the closed-form pure-linear-AR(p) narrowing (target[t] = Σ_k coeffs[k] * target[t - lags[k]], no constant term, no exogenous reads). The narrowing is what enables the Cython-backed kernel path; fortarget[t] = target[t-1] + creach forundiffinstead, fortarget[t] = β·target[t-1] + γ·y[t]reach for the generalrec. comparereturns aCompareResult, not aBool. Julia'scompare(v1, v2)prints to stdout and returnsBool; the Python form returns a structuredCompareResult(equal, differences)that is truthy on equality, whose__str__reproduces the printed diff, and whose.differencesexposes one entry per leaf for callers that want to introspect rather than re-parse stdout. The classicif compare(v1, v2): ...one-liner still works;quiet=Truesuppresses the stdout side effect when only the structured result is needed (e.g. inside a test runner). Julia's@comparemacro folds into the samecomparefunction: it existed only to capture variable names for the printed diff, which Python users can supply directly vialeft=/right=.reindextakes a 2-tuple, not aPair. Julia'sPair{<:MIT,<:MIT}becomes a Python(old_mit, new_mit)2-tuple. Dispatch over MIT / MITRange / TSeries / MVTSeries / Workspace is identical; thecopy=kwarg is the same (defaultFalse, wrap-by-default).Statistics.*axis kwarg isaxis=, notdims=. Julia'smean(mvts; dims=1)reduces along rows (per-column); Python uses NumPy'saxis=0for the same operation.dims=2(per-row, Julia) maps toaxis=1(per-row, Python).axis=None(default) reduces flat to a scalar — the existing behaviour. Returns:axis=0→ single-row MVTSeries (column names preserved);axis=1→ 1-D TSeries indexed by the input range. The convention switch is the same "Python idiom wins for kwargs that are language-conventional" rule used elsewhere in this table; the underlying behaviour matches Julia.- Matrix multiply uses
@, not*. Julia'slinalg.jloverloads*for matrix-times-TSeries / -MVTSeries; the Python port uses PEP 465's@instead, leaving element-wise*to mean element-wise (which is how NumPy / pandas / xarray spell it). Both behave like Julia otherwise: the result is a plainnumpy.ndarraywith frequency / range / column-name labels stripped (Julia's overloads forward to*(_vals(A), _vals(B))and return a bareVector/Matrix; the testx * x3 == _vals(x) * _vals(x3)upstream documents the same).transpose/adjointare not ported — neither has clean semantics on a TSeries / MVTSeries whose row axis is time. Seereference/linalg.md. One related divergence worth flagging: in Julia,Vector * Vectorraises aMethodError, so_vals(t1) * _vals(t2)doesn't actually work even though the overload is defined; in Python,t1 @ t2returns the inner product (NumPy 1-D matmul semantics) — the Python form is more useful, not less. copytois a free function, not a method or a!-suffixed mutator. Julia spells the in-place materialiserBase.copyto!(mvts, w; verbose=, trange=)— the!suffix is the Julia convention for an in-place mutator. Python has no syntactic mutation marker;tseconexportscopyto(dst: MVTSeries, src: Workspace, *, verbose=False, trange=None)as a free function, mirroring NumPy'snp.copyto(dst, src)signature and convention. The function is still in-place —id(dst._values)is preserved across the call — and returnsdstfor chaining. The per-column write goes through the destination's existing__setitem__with frequency / contained-range checks; a non-TSeriesvalue at a matching key raisesTypeError(matching Julia's behaviour, which would error inside the innercopyto!).verbose=Trueemits a singleUserWarningat the end of the loop with all missing-from-workspace column names joined, matching Julia's end-of-loop@warn.set_holidays_map(country, subdivision)delegates topython-holidays. The Julia upstream ships bundled CSVs (TimeSeriesEcon.jl/src/holidays/) and reads them on first call. Python delegates to thepython-holidaysPyPI package as the single source of truth — install the optional extra withpip install "TimeSeriesEconPy[holidays]". Country / subdivision codes follow ISO 3166 conventions rather than the Julia CSVs' space-separated forms; a small number of names diverge (e.g. Julia's"Yukon Territory"is"YT"in Python). Useget_holidays_options()andget_holidays_options("CA")to discover the supported codes. The map's default range (bdaily("1970-01-01")tobdaily("2049-12-31")) is identical.@wevalbecomes plain attribute access (or a snapshot context manager). Julia's@weval(w, EXPR)macro rewritesEXPRso bare identifiers resolve againstw's namespace; Python has no AST-rewriting macros. The Pythonic spellings are direct attribute access (w.b + w.a, the default — almost always sufficient) or, when several unprefixed reads cluster, the snapshot context managerWorkspace.as_namespace():
with w.as_namespace() as ns:
w.b1 = ns.b + ns.a # ports Julia's `w.b1 = @weval w b + a`
w.alpha = math.sin(math.pi) + ns.a - ns.a
The namespace is a types.SimpleNamespace snapshot taken at
__enter__: mutations to w after entry are invisible, and mutating
the namespace does not write back to w. Use w.x = ... directly
for writes. Values are held by reference, so mutating a contained
TSeries / MVTSeries through the namespace still mutates the same
object inside w. Non-identifier keys (e.g. "1x") are silently
omitted from the snapshot — keep using w["..."] for those. No
eval-based form is provided: deferring identifier resolution to
runtime eval of arbitrary expression strings would break static
tooling (mypy, IDE refactors, "find references") and is a security
footgun on workspaces loaded from untrusted sources.