{"name": "PlanX GeoStats Lab", "package_name": "planx_geostats", "version": "3.10.0", "experimental": false, "qgis_min": "3.28.0", "qgis_max": "4.99.0", "downloads": 45, "uploaded_by": "geo140195philo", "upload_datetime": "2026-09-19T07:32:09.314806", "changelog": "## [3.10.0] - 2026-09-19\r\n\r\n- 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 \u2014 is this location at or below the level \u2014 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.\r\n- 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 \u0130zmir fixture the two are far apart \u2014 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 \u2014 which is precisely why the mapped level and the sample frequency are allowed to differ.\r\n- 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 \u2014 on the \u0130zmir 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).\r\n- 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 \u2014 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 \u0130zmir 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.\r\n- 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 \u2014 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.\r\n- 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.\r\n- 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 \u2014 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.\r\n- 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.\r\n- 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.\r\n- 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.\r\n\r\n## [3.9.0] - 2026-09-18\r\n\r\n- Add group **07 | Interpolation and Geostatistics** with two algorithms, completing the classical geostatistics workflow the plugin's name evokes \u2014 the plugin previously had no kriging and no semivariogram tool of any kind.\r\n- 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.\r\n- Add **Ordinary / Universal Kriging Interpolation** (`kriging_interpolation`). Builds a surface from a point layer plus a fitted variogram \u2014 re-fitted inline or read from the semivariogram tool's parameter layer \u2014 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.\r\n- 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.\r\n- 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 \u2014 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.\r\n- 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.\r\n- 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.\r\n- 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\u2013Marquardt solve on `numpy` alone.\r\n- Verified on **both runtimes**, QGIS 3.44.12-Solothurn (LTR) and QGIS 4.2.0-Bel\u00e9m do Par\u00e1: 46 kriging runtime checks, 76 semivariogram runtime checks, 59 pure-engine checks, and the catalog, core and packaging gates.\r\n\r\n## [3.8.2] - 2026-09-18\r\n\r\n- 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.\r\n- 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.\r\n- 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.\r\n- The manual advertised the previous version in four places (title, sidebar, hero chip, footer); all four now match the plugin.\r\n- 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.", "external_deps": null, "download_url": "https://plugins.qgis.org/plugins/planx_geostats/version/3.10.0/download/"}