Repository navigation
Methods Image
Numeric core every batch method fits on. Source:
image/original.py,image/image.py,image/bases.py,image/fit.py,image/transfer.py,image/analytics.py. Invoke viaOriginal(x, y),.image(basis, order),fit(model, data). API: ../api/fitting.md.
Every batch method in dtfit -- LSI, EAC, the
streaming filters, the stochastic tier -- runs its fit on the image of a
signal rather than on the raw samples. An Original holds the sampled signal;
its Image in a basis at an order is a fixed-size statistic that carries
everything a least-squares fit in that basis needs and nothing else. This page
is the base the other Methods pages point to.
The differential-transformation lineage pairs a signal with its spectrum on a system of functions: DSB balances the two spectra symbolically, LSI and EAC relax that to a least-squares match. The image is the discrete form of that spectrum, built directly from the samples rather than from a fitted continuous model.
Given a basis Phi = (phi_0, ..., phi_K) and samples (x_i, y_i, w_i) mapped
to u_i = 2 (x_i - x0) / (x1 - x0) - 1 on the domain [x0, x1], the image is
S of length K+1, G of shape (K+1) x (K+1), together with the sample
count n and sumsq = sum(w y^2). The least-squares coefficients in the
basis are beta = G^+ S, G^+ the Hermitian pseudo-inverse (singular values
below 1e-15 of the largest are dropped, so an ill-conditioned Gram never
returns a silently wrong answer); beta is derived on demand and never
stored.
S and G are the discrete statistic of the samples, not a quadrature of an
integral: the model is projected on the same grid, `S_f(theta) = Phi^T (w
- f(x; theta))
, so the residualS - S_f(theta)is exactly the least-squares residual ofy ~ Phi(x) thetarestricted to the span ofPhi-- an estimator that is exact NLLS in that span, with zero bias on a uniform, clustered or random grid alike, and parameter RMSE 1.00 to 1.03 times NLLS in the Legendre basis atorder_for`, 1.02 to 1.06 times NLLS in the block basis at four windows per parameter. A quadrature rule would integrate the model against a continuous approximation of the data and inherit whatever bias that approximation carries on an irregular grid; matching the discrete projections of the same samples the model is evaluated on has none.
G carries the correlation of the test functions on the sample grid exactly,
so orthogonality is never required: the Legendre polynomials and the block
indicators are two members of one family of test functions, distinguished
only by their span and locality, not by a different estimator.
An image is additive over sample sets: Image.merge adds S, G, n and
sumsq (and sumy, wsum) whatever the two sample sets are, and joins the
sample positions in the grid. It is nested in order: Image.truncate(K')
for K' < K keeps the leading K'+1 rows and columns of S and G, exact
for the Legendre basis. It is serializable: to_dict and from_dict
round-trip the whole image, and two images compare equal when their basis,
domain, grid, counts and arrays agree.
What an image does not contain is the samples themselves. That is what buys the properties above: a size fixed by the order on a uniform grid (an explicit grid keeps its positions), a statistic streams can accumulate chunk by chunk (ImageStream), a map-reduce that only ever adds sums, and a checkpoint that is the sums.
fit(model, data, ...) takes an Original or an Image. An Original is
imaged first, at order (default order_for(model), below). The model is
projected on the image's own grid with the image's own weights, and the
whitened residual
is minimized: nonlinear least squares restricted to the span of the basis.
Levenberg-Marquardt runs when there are no bounds; trust-region runs with
bounds, followed by a differential-evolution stage only when every bound is
finite and the local solve fails, returns a non-finite cost, or explains less
than half the weighted total sum of squares -- announced with a UserWarning
when it triggers.
Covariance comes from the SVD of the Jacobian of r, scaled by RSS / (n - p) unless absolute_sigma=True; a parameter with a component in a null
direction of the Jacobian is not identified and is reported with inf on
its diagonal entry and nan off it, rather than a spuriously small
variance. RSS from an Original is the raw residual sum of squares over
the samples; from an Image it is the identity
exact when the model lies in the span of the basis -- the result records
which one was used as rss_source, plus image_order and basis_name.
order_for(model, params, domain, tol=0.02, max_order=64) is the smallest
Legendre order at which every parameter sensitivity df/dtheta is
represented to relative L2 error tol, floored at n_params - 1.
coverage(model, params, image) is the largest truncation error of the
sensitivities at the image's own order, measured against a reference
expansion of max(2K, 64) modes; fit raises a UserWarning when that
exceeds tol, meaning the image is too coarse to identify the model's
parameters.
Robustness is a property of the image, decided once at construction, before
any model is involved. robust=True on fit, fit(basis="legendre"), fit(basis="block") or
Original.image runs Huber IRLS on the basis regression y ~ Phi beta:
each of five passes fits the regression at the current weights, reads its
residual and MAD scale s, and sets sample i's weight to w_i * min(1, c s / |r_i|) with c = 1.345. Those weights enter S and G directly; fitting,
merging and the analytics that follow are unchanged, because they only ever
see S, G, n and sumsq.
Measured with 10 percent outliers at ten sigma, the robust image gives
parameter RMSE 0.34 to 0.38 of a plain NLLS fit at order 12 -- the same
reduction scipy's soft_l1 loss gives. It is built at order_for(model)
rather than at a generous order: a higher-order regression has enough
freedom to bend around an outlier and absorb part of it, which weakens the
robust weighting. Because robustness lives in the image and not in fit,
fit(robust=True) given an already-built Image raises: the reweighting
needs the samples, which an Image does not carry.
legendre is the LSI basis: Legendre polynomials P_0 .. P_order on [-1, 1], order the polynomial degree. block is the EAC basis: indicator
functions of order equal, half-open windows in u. A Basis exposes
evaluate(u) -> Phi (shape (len(u), n_coef)) and the property n_coef;
Image.transfer dispatches on the basis name to legendre_transfer and
block_transfer (below). The experimental package carries more families
on its own spectral machinery -- Fourier, Chebyshev, Laguerre -- see
the experimental adaptations API.
For the Legendre basis, a coarse polynomial restricted to a local domain
inside it is a polynomial of the same degree in the local variable, so
Phi_coarse = Phi_local @ A exactly, with A depending only on the two
domains (legendre_transfer, local order at least the coarse order):
n and sumsq add. For the block basis a coarse window must be a
union of fine windows, and the transfer (block_transfer) is a 0/1
aggregation, exact whenever each coarse window is a union of fine windows.
assemble(images, domain, order) transfers every image onto one coarse domain and sums them --
the streams page builds streams and block map-reduce on
top of exactly this.
An image carries enough to answer questions about the signal without going
back to the samples. Every read-out below is a method on Image and a
function in dtfit.image.analytics; they read only beta = G^+ S and the
coefficient covariance per unit noise variance V = G^+.
effective_order() is the largest j whose coefficient stands three sigma
above the noise, |beta_j| > 3 sqrt(s2 V_jj). The noise scale s2 is
estimated from the orders above the answer itself: it starts as the median
of beta_j^2 / V_jj over the upper half of the orders, rescaled by the
chi-square median so no single large coefficient drags it, and is refined
from the mean of the tail the current answer leaves. On a Legendre
polynomial of known degree plus noise at order 24 the answer is the degree
in 79 to 88 percent of draws and never below it.
noise_sigma() reads the noise off those tail orders,
sqrt(mean_j(beta_j^2 / V_jj)) for j above the effective order, and
returns None (with a RuntimeWarning) when fewer than eight orders are
left, since a short tail estimates nothing. It is an upper bound when the
image order sits close to the signal's own: on a damped oscillation it reads
1.76 times the true sigma at order 24, 1.10 at order 32 and 1.04 at order
40.
decay() fits both a geometric law |beta_j| ~ a r^j and an algebraic law
|beta_j| ~ c j^-p to the coefficients between order 2 and the effective
order, and reports both rates with both r2 values and the name of the
better fit. The r2 is the part to read: an oscillatory signal's Legendre
coefficients rise before they fall, and neither law describes them (a
damped cosine gives 0.099 and 0.019, an exponential 0.974 and 0.845).
test_equal(other) asks whether two images are of the same signal:
d = beta_a - beta_b against d^T (s2_a V_a + s2_b V_b)^+ d, chi-square
with the rank of the combined covariance. The two images may hold any
sample sets, weights and sample counts -- only basis, order and domain
must agree. Measured false-alarm rate at alpha = 0.05 over 2000
replicates: 0.046 under Gaussian noise, 0.052 under Student-t with three
degrees of freedom, 0.048 under Laplace, unaffected by mixing a weighted
and an unweighted image (0.050 over 400 replicates).
test_structure(model, params) asks the other question, whether a model
explains everything the basis resolves: the leftover projections
d = S - S_f against d^T (s2 G)^+ d, which is the drop in residual sum
of squares between the model and the best fit in the span, in units of
the noise variance. test_structure takes its noise scale from the
basis-regression residual (sumsq - S^T beta) / (n - rank(G)), which
exists at any order and in any basis; test_equal takes each image's own
such residual instead, s2_a and s2_b, since the two images may carry
different weights. Both return a ChiSquareTest (statistic, dof,
pvalue, alpha, reject). In a test module, reach both as Image
methods or as analytics.test_equal/analytics.test_structure, not by
importing the bare names -- pytest collects a module-level test_equal
as a test.
simulate(n=None, sigma=None, rng=None) goes the other way: the
reconstruction plus Gaussian noise, on the image's own grid when n is
None and on n evenly spaced positions otherwise, with sigma defaulting
to noise_sigma().
rng = np.random.default_rng(0)
x = np.linspace(0.0, 10.0, 500)
clean = np.exp(0.9 * x)
y = clean + rng.normal(0, 0.05, x.size)
img = Original(x, y).image("legendre", 40)
print(round(img.noise_sigma(), 4), img.effective_order())
d = img.decay()
print(d.kind, round(d.ratio, 3), round(d.geometric_r2, 3), round(d.algebraic_r2, 3))
same = Original(x, clean + rng.normal(0, 0.05, x.size)).image("legendre", 40)
warm = Original(x, 1.02 * clean + rng.normal(0, 0.05, x.size)).image("legendre", 40)
print(round(img.test_equal(same).pvalue, 3), img.test_equal(warm).reject)
res = fit("a*exp(b*t)", img, "t", p0=[1.0, 1.0])
left = img.test_structure("a*exp(b*t)", res.coeffs, "t")
print(round(left.statistic, 2), left.dof, round(left.pvalue, 3), left.reject)
print(img.test_structure("a + b*t", [0.0, 1000.0], "t").reject)
xs, ys = img.simulate(200, rng=np.random.default_rng(1))
print(xs.shape, round(float(np.std(ys - img.reconstruct(xs))), 4))
resid = Original(x, y).diagnostics("a*exp(b*t)", res.coeffs, "t")
print(round(resid["durbin_watson"], 3), round(resid["lag1_autocorr"], 3))The noise level comes back as 0.048 against the 0.05 that was added, and
twelve orders carry signal; the coefficients fall geometrically by 0.321
per order, a law that explains 0.974 of their spread against the algebraic
law's 0.845. The two clean records are not distinguished (p = 0.895), the
two-percent-warmer one is. The exponential the data came from leaves nothing
in the span (34.63 on 39 degrees of freedom, p = 0.669); a straight line
is rejected. simulate(200, rng=...) draws a record on a fresh grid with
the noise the image measured (0.0443), and Original.diagnostics
confirms the residuals are white (Durbin-Watson 2.043, lag-1
autocorrelation -0.022).
rng = np.random.default_rng(0)
x = np.linspace(0.0, 3.0, 200)
y = 1.0 * np.exp(0.8 * x) + rng.normal(0, 0.05, x.size)
original = Original(x, y)
order = order_for("a*exp(b*x)", [1.0, 0.8], original.domain, var="x")
image = original.image("legendre", order)
res_orig = fit("a*exp(b*x)", original, var="x", basis="legendre", order=order, p0=[1.0, 0.8])
res_img = fit("a*exp(b*x)", image, var="x", p0=[1.0, 0.8])
print(dict(zip(res_orig.names, res_orig.coeffs)), res_orig.rss_source)
print(dict(zip(res_img.names, res_img.coeffs)), res_img.rss_source)
half = x.size // 2
left = Original(x[:half], y[:half], domain=original.domain).image("legendre", order)
right = Original(x[half:], y[half:], domain=original.domain).image("legendre", order)
res_merged = fit("a*exp(b*x)", left.merge(right), var="x", p0=[1.0, 0.8])
print(dict(zip(res_merged.names, res_merged.coeffs)))
y_out = y.copy()
y_out[::20] += 5.0
outlier = Original(x, y_out)
res_plain = fit("a*exp(b*x)", outlier.image("legendre", order), var="x", p0=[1.0, 0.8])
res_robust = fit("a*exp(b*x)", outlier.image("legendre", order, robust=True), var="x", p0=[1.0, 0.8])
print(dict(zip(res_plain.names, res_plain.coeffs)))
print(dict(zip(res_robust.names, res_robust.coeffs)))order_for picks order 3 for this model on this domain. The fit from the
Original and the fit from its Image agree to the last printed digit,
a=1.0025, b=0.7988, with rss_source "samples" for the first and
"image" for the second. Imaging the two halves of the samples separately
and merging before fitting gives the same a=1.0025, b=0.7988: the image is
additive regardless of how the samples were split. With one sample in twenty
pushed 5.0 high, a hundred times the noise scale, the plain image is pulled
to a=1.188, b=0.743; the
robust image recovers a=1.006, b=0.798, close to the outlier-free fit.
LSI and EAC are presets of fit in the
Legendre and block bases; ImageStream runs the same S
and G update over a running accumulator, block images or channel batches;
the ImageFilter(basis="block") and
ImageFilter(basis="legendre") read a window image each step; and the
stochastic tier builds its own second-order image -- lagged sums, dyadic block
sums and a fixed-grid DFT -- and fits the functionals it reads off that with
the same LSI and EAC presets -- see
the stochastic tier.
Published automatically from the repo's wiki/ directory by the wiki workflow. All pages are maintained directly in wiki/.
Start here
Guides
Examples
- Overview
- 01 Quickstart
- 02 Fitting methods
- 03 Models & auto
- 04 sklearn estimator
- 05 Streaming
- 06 Scaling
- 07 Diagnostics
- 08 Stochastic
API reference
Methods (math)
Experimental
Validation