Version: [5235] PlanX GeoStats Lab 3.10.0

## [3.10.0] - 2026-09-19

- Add **Indicator Kriging (Threshold Exceedance Probability)**
(`indicator_kriging`), completing group 07 with the tool that answers
a probability question rather than a value one. Each threshold turns
the observations into a 0/1 variable — is this location at or below
the level — and that variable is kriged on its own by ordinary
kriging, so the exceedance probability is produced by construction
rather than by assuming the field is normal around a prediction. The
tool writes one band per threshold, an E-type estimate of the field
rebuilt from the repaired probabilities, and an optional E-type
variance, and its report carries the per-threshold fitted family,
nugget, partial sill, range and \(R^2\), the size of the
order-relation repair, a Brier score with a skill score against the
base rate, and a five-bin equal-count calibration table over the
held-out scores.
- Explain the number that looks like a bug, with the measurements
behind it. The bands' average probability over the mapped area does
not equal the proportion of observations below the threshold, and on
the bundled İzmir fixture the two are far apart — mapped 0.617 /
0.8228 / 0.8976 against sample frequencies 0.2506 / 0.5013 / 0.7494.
That is ordinary kriging's far field converging on the *declustered*
mean, not a broken probability, and the report now prints the mapped,
sample and cell-declustered frequencies side by side for every
threshold together with the declustering sweep's range (0.5467 to
0.9504 across cell sizes of 2163.75 to 32456.2 map units), the spread,
and the busiest cell's count (359), and names the threshold furthest
from its declustered reference. New
`core/indicator_engine.py::declustered_frequencies` computes that
reference over a sweep of cell sizes rather than at one nominated
size, because the answer moves with the choice and a single size
cannot show that it does. For the top threshold the fitted range is
981.4 map units against far targets thousands of units away, so the
semivariance is exactly the sill at every observation, the kriging
weights come out between 0.0000 and 0.0038 against an equal weight of
0.002584, and the result is forced by the observation configuration —
which is precisely why the mapped level and the sample frequency are
allowed to differ.
- Record the algebra that removes a standing source of confusion about
the E-type level. If the class probabilities at every cell were the
sample's own marginal class frequencies, the E-type identity returns
the observed mean exactly — on the İzmir run the four class means
1.7931, 21.4255, 30.8315 and 39.1842 weighted by those frequencies
give 23.290 against an observed mean of 23.2891. Weighted by the
declustered frequencies they give 11.912, against a report E-type mean
of 11.8342. The E-type gap is therefore the mapped-versus-sample
distinction almost entirely, and the class-representative effect is
only the residual between 11.912 and 11.8342. The report's note now
says this instead of attributing the gap to the class midpoints, and
the "class-midpoint effect" wording is gone from both the note and the
caveat because the engine returns the class **mean** (the interval
midpoint is only the fallback for an empty class).
- Add **Interpolation Model Comparison** (`interp_compare`), mirroring
ArcGIS's *Compare Geostatistical Layers*. It scores IDW, natural
neighbour, radial basis function, global polynomial trend of orders 1
to 3, and ordinary kriging under one shared protocol — a single seeded
fold split with every model refitted inside every fold, so no exact
interpolator can score against a surface fitted to its own datum. It
reports the ranking with a paired margin against the leader, its
standard error and that margin in standard errors, plus the fraction
of locations on which each method was strictly closer, and it computes
every error statistic over the intersection of the observations that
all scored methods could predict rather than over different sets per
method. On the İzmir fixture the top two methods are 1.8 standard
errors apart and swap places on 50.8 and 49.2 per cent of locations,
so the report's verdict is that this sample does not separate them;
the third and fourth places trail by 8.2 and 7.6 standard errors,
which are findings. It writes a report and a CSV and deliberately no
raster, naming the tool that produces the winner's surface instead.
- Fix a latent `NameError` in the indicator report builder. A
comprehension computing the weak-fit thresholds referenced a name that
existed only as a local of `processAlgorithm`, so report generation
could only ever succeed on a run whose editor never noticed — the line
is reached only once a report is actually written. The comprehension
is now the module-level `weak_fit_indices(fits)`, called from both the
run log and the report, and the flake8 gate's `F821` is what surfaced
it.
- Remove two internal inconsistencies in the indicator report. The
surface statistics were summarised from the unmasked arrays behind the
written rasters while the band table was taken over the written cells,
so the report stated "cells written as no-data: 5,514" beside
statistics that included those cells; the statistics now come from the
masked grids and the two tables agree exactly (mean exceedance
0.220871 against the band table's 0.2209, E-type mean 11.8342 against
a hand-computed 11.834). And the cross-validation column labelled
"mean predicted exceedance probability" is the mean of the cumulative
\(P(Z \le z_k)\), not an exceedance probability; it is now labelled as
such in the report, in the threshold table and in the engine
docstring.
- Clarify what the cross-validation check can and cannot see: every
score in it is taken at an observation, and indicator kriging is exact
at an observation, so a surface can pass it and still sit far from the
mapped area's own frequency — those are two different quantities when
the sampling is clustered. The engine docstring and the report's
caveats now say so, and the caveat on the \(R^2\) column states the
converse too: a good \(R^2\) says the fitted curve tracks the binned
cloud, not that the indicator has a sill inside the lag range, and
where it does not the kriging extrapolates the trend past the
observations instead of returning to the overall frequency.
- Add coverage that runs in CI rather than only in a scratch harness.
`tests/smoke_core.py` gained `load_core_package`, which loads the
geostatistics engines into a synthetic package so their relative
imports resolve without a QGIS runtime, and a declustering test
asserting that a scattered sample decomposes to its own frequencies,
that a crowded cluster is discounted rather than restated, that the
answer follows the values and not the geometry, that a sample with no
spatial extent falls back to the plain frequency, and that an empty
threshold list is refused. The test was mutation-checked: stubbing the
function with one that restates the sample makes it fail.
- Documentation and catalogue to match: two new manual cards
(`alg-indicator`, `alg-interp-compare`) in both byte-identical copies,
sidebar entries, See-also cross-links added to the semivariogram,
kriging, IDW, RBF, natural neighbour, trend surface and EBK cards,
README catalog rows and counts, and two QA matrix rows. Manual and
README counts now read 90 algorithms, matching the 90 registered
algorithms and the 90 `HELP_SLUGS` entries, and
`MIN_EXPECTED_ALGORITHM_COUNT` is raised from 88 to 90.
- No new required dependency: `core/indicator_engine.py` is `numpy`
alone, reusing the same binned empirical variogram and the same
weighted-least-squares fitting rule as the kriging tool, so an
indicator variogram and a value variogram fitted by this plugin are
directly comparable.

## [3.9.0] - 2026-09-18

- Add group **07 | Interpolation and Geostatistics** with two
algorithms, completing the classical geostatistics workflow the
plugin's name evokes — the plugin previously had no kriging and no
semivariogram tool of any kind.
- Add **Empirical Semivariogram & Model Fitting**
(`empirical_semivariogram`). Estimates how spatial similarity decays
with distance using Matheron's estimator, then fits one of five
theoretical models (spherical, exponential, gaussian, linear, stable)
by weighted least squares with the Cressie (1985)
\(N(h)/\hat{\gamma}(h)^2\) weighting, minimised in log-parameter space
so the nugget, sill and range are positive by construction rather than
by clamping. Reports the fitted parameters, the fit of the curve to
the binned points, directional empirical variograms with a polar rose
for seeing anisotropy, automatic quality flags, and a full
analyst-facing report. Writes a single-feature parameter layer that
the kriging tool reads directly, so the fitted model does not have to
be retyped between the two dialogs.
- Add **Ordinary / Universal Kriging Interpolation**
(`kriging_interpolation`). Builds a surface from a point layer plus a
fitted variogram — re-fitted inline or read from the semivariogram
tool's parameter layer — and writes **kriging's own analytically
derived variance** as a second raster beside the prediction. Universal
kriging adds a linear or quadratic polynomial drift for non-stationary
fields. Both rasters are Float32 GeoTIFFs with the input CRS, extent
and cell size, and cells the model cannot evaluate are written as
nodata rather than as zero.
- Fix a real defect in universal kriging found by a failing test
rather than assumed: the variogram was being fitted to the raw trended
values instead of the drift residuals. On a test field whose true
range was 220 map units, the trend inflated the fitted range to 513
and made universal kriging predict *worse* than ordinary kriging;
fitting on residuals recovers 238 and universal kriging then predicts
better, as it should. `quality_flags()` also gained a flag for a
fitted range that has saturated at the maximum lag.
- Bound kriging memory by construction. Predicted cells are solved in
chunks with the right-hand side held under a fixed byte budget,
because a single solve for every cell allocates one double per
(observation + drift) pair per cell — measured at roughly 18 GB for
600 observations over a 4-million-cell grid. Chunking changes the
result only at the level of floating-point rounding (about 1e-14
relative), which is asserted rather than claimed.
- State the tool's cost instead of hanging. Both tools report their
point-count guidance in the dialog: kriging caps observations at 2000
by default and refuses a larger input with a message naming the count
and the cap, and leave-one-out cross-validation is on by default with
its extra cost made explicit. Timings quoted in the help text and
manual were measured on the development machine rather than estimated.
- Fix a wording defect in the shared report caveats block, found while
building these tools: `caveats_html()` hard-coded text naming "the
distance band or K value", parameters that neither interpolation tool
has, so both new reports told the analyst to tune something that does
not exist. The helper now takes an optional per-method caveats
argument that replaces those two lines; when it is omitted the
original wording is used, so the 81 pre-existing algorithms' reports
are unchanged.
- New core modules: `core/raster_io.py` (GeoTIFF writing through
`QgsRasterFileWriter` and `QgsRasterBlock`, no `osgeo` import, with
the writing contract established by direct probe on both runtimes) and
`core/geostat_engines.py` additions (`krige_chunked`, `detrend`,
variogram quality flags). No new required dependency: fitting is a
Levenberg–Marquardt solve on `numpy` alone.
- Verified on **both runtimes**, QGIS 3.44.12-Solothurn (LTR) and QGIS
4.2.0-Belém do Pará: 46 kriging runtime checks, 76 semivariogram
runtime checks, 59 pure-engine checks, and the catalog, core and
packaging gates.

## [3.8.2] - 2026-09-18

- Repair the documentation path end to end. Every one of the 81
algorithms' Help button links pointed at a retired documentation host
and returned a 404; `algorithms/_mixins.py` now names the canonical
host that `metadata.txt` and the QGIS Hub already advertise, and a new
smoke test pins both to the same host so they cannot drift apart
again.
- Fix a missing closing tag in the Spatial Durbin manual card. The
card's body was never closed, so the `see-also` and Literature blocks
sat inside the Interpretation Guide box instead of beside it, and the
unclosed element swallowed the layout of the section that follows. A
new smoke test asserts per-card tag balance across all 81 manual
entries, so an unbalanced card now fails the build instead of being
silently repaired by the browser.
- Fix two CatBoost formulas that never rendered. An unescaped angle
bracket inside a LaTeX subscript (`\sigma_{<i}`) was consumed by the
HTML parser as the start of an italic element, so MathJax received
broken source. Both occurrences now use the `&lt;` entity.
- The manual advertised the previous version in four places (title,
sidebar, hero chip, footer); all four now match the plugin.
- Repair the release-verification gate itself.
`tests/smoke_provider_catalog.py` had been failing since 3.8.1 because
README's release-verification command still named 3.8.0, and because
`run_all()` aborts on the first failure, the remaining 20 checks in
that suite had not run since that release.

yes

geo140195philo

2026-09-19T12:32:09.314806+00:00

3.28.0

4.99.0

None

no

Version management

Plugin details