Migrate variography/pair-sampling and grouped statistics/cosampling, and convolution from xDEM with modular API - #925
Open
rhugonnet wants to merge 6 commits into
Conversation
This was referenced Sep 4, 2026
Member
Author
|
@adehecq @belletva @marinebcht Almost done here, I will merge first, then write the whole new documentation (and refined benchmarking) pages in a separate PR. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR migrates and expands two main groups of features from xDEM:
Note that "grouped" statistics includes "zonal" statistics, for instance when the input is a vector file.
Most of these features are moved or adapted from xDEM code. Notably, all implementations support Dask inputs and laziness. In particular, the variography module implements a specific log-lag pairwise sampling scheme for both regular and irregular data to efficiently estimate a variogram from large rasters or point clouds.
Also, the NaN-robust
convolutionfunctions of xDEM are also moved here intofilters, to support both Numba/SciPy efficiently and tie to filters where relevant.Otherwise, most changes are distributed into two modules:
sampling/now contains a newpairsampling(for variography) andcosampling/stratified(for grouped stats) with asupport(common helper functions) in addition tosubsampling.stats/now contains a newvariographymodule, and refactors the previousstats.pymodule (+ some processing code that non-optimally lived in raster/pointcloud classes themselves) into three submodules:reductionmodule (that applies the statistical reducers, supporting Dask/MP there directly),selectionmodule (that selects appropriate data using routines fromsampling/) andgroupingmodule (that contains all the grouping logic). Thestats.pymodule now only contains parent functions used by the raster/point cloud objects.To support Multiprocessing for statistics, this module also adds a new
multiproc/readers.pymodule that contains helper to read only specific chunks of rasters/points over multiple passes by storing metadata more practically (while other modules always did a full pass over the entire raster, withmap_overlap/block).I also added Dask support for point cloud reprojection, for both Multiprocessing/Dask, which was fairly straightforward (each chunk can be reprojected independently), but required some logic specific to LAS/LAZ/COPC. I used the opportunity to move functions previous packed in
pointcloud/baseandpointcloud/pd_accessorinto a newpointcloud/dataframe.pywhich contains low-level helper function often reused (mirroringraster/array.py) and a newpointcloud/transformation.py(containing the logic for chunked reprojection, especially for Multiprocessing).The API now exposes for both raster and point cloud objects/accessors, added to
base.pymodules of point cloud and raster:cosample()andstats(by=)for grouped stats (not passingby=computes global stats on the whole object)pairsample()andvariogram()Finally, by tying these new functions to xDEM uncertainty/coreg module directly (in a parallel PR), a couple small bugs came up (like floating precision differences for
interp_pointswith Dask/MP), hence the small fixes in other modules with small added tests.Resolves #895
Resolves #876
Context of previous implementations
Those features have been present in different forms in xDEM for a while (specifically for uncertainty/coregistration), and have long been planned to be migrated here in GeoUtils as generic features that can naturally interface with others.
See discussions: GlacioHack/xdem#588, GlacioHack/xdem#378, GlacioHack/xdem#947
For co-sampling, the code is largely migrated from a
cosampling.pymodule initially drafted in GlacioHack/xdem#759, which aimed to replace the pre-processing functions of coregistration/uncertainty propagation (which always require 2 datasets, point or raster; sampled at the same location).For variography and pair-sampling, the code is inspired by the old
spatialstatsof xDEM and migrated from a_metricspace.pymodule initially drafted in GlacioHack/xdem#759 with aim to improve upon Dask-support and efficient pairwise sampling, built on top of SciKit-GStat. We also add variogram conversion across backends (GPyTorch, GSTools and SciKit-GStat) borrowing logic from code I largely wrote in https://github.com/geo-smart/spacetime-elevation.For grouped statistics, this PR supersedes #668, #774 and #815 after discussions in #895 and elsewhere. It aims to replace
nd_binning(SciPy-based) in xDEM used widely for coregistration and uncertainty propagation. The co-sampling logic is the same as above, and the plotting/management of the grouped statistics is inspired from that of xDEM, but the Dask/MP implementation is new to this PR (and probably its biggest piece).For filters, this PR moves generic SciPy/Numba NaN-supporting convolution from xDEM as discussed here: as discussed here: GlacioHack/xdem#300.
Implementation details
Variography and pair-sampling
For variography, we create our own "light"
Variogramobject, that is easier to optimize for efficiency on large datasets and allows to inter-operate with other packages (GSTools, GPyTorch, SciKit-GStat).This is because there is no single geostatistical package in Python that has it all (the kriging packages have better interpretability with empirical variograms, the GP ones are more computationally efficient for applying kriging but more black-box for kernel estimation/visualization). Additionally, the most modular variography Python package (which is SciKit-GStat) has a practical but heavy
Variogramclass that holds too much information at once (all pairwise distances). So we need a wrapper for variography to be efficient for Dask/MP on large datasets.Because variography analyzes PAIRS of observations, it is inherently hard to scale on large data without subsampling: a 10,000 x 10,000 raster has 100 million obs, so roughly 100 M x 100 M / 2 = 5 quadrillion pairs of obs; which is impossible to sample on any hardware (and anyway useless because the spatial correlation is often largely consistent in space).
Thus, in GeoUtils, we focus on adding efficient pair-sampling to naturally make the link to large datasets. The pair sampling is done in log-lag space to adequately sample pairs across all distances (otherwise a pure random sample has low probability of having two neighbouring pixels sampled, for instance), and supports Dask input (but not Multiprocessing, it is a bit too complex for it yet). This expands previous work I did in SciKit-GStat. The full detail of the implementations is a bit complex, and it is available in the classes docstring.
Once this pair sampling is done, our wrapper calls SciKit-GStat for empirical binning of the pairs, and then model fit (the computationally cheap part). Here, we have to use some tricks to fake the presence of some class SciKit-GStat attributes that would normally be too large to fit in memory, and make our pair sampling connect to its underlying MetricSpace class. A bit hacky, but it works! 😄 (And we can adjust more cleanly if a 2.0 comes out in SciKit-GStat at some point, to which I might contribute)
The idea is that the new
gu.Variogramcan be exported to any desired format by the user (to_gstools(),to_gpytorch(). And, later on, within GeoUtils, it can be passed to any interpolation/reprojection/gridding function to trigger kriging:interp_points(method="kriging", variogram=gu.Variogram(...)).This is not implemented yet, but it is a simple link to GSTools/GPyTorch, and it will make kriging easy, modular, and relevant to the whole package as a core resampling method which can happen at low level (during CRS reprojection, for instance)!
Grouped stats and co-sampling
For grouped stats, we need to do two things: 1/ compare the inputs on a similar spatial support (the object being analyzed, and the grouping objects) and 2/ apply the statistics and have an implementation that works with chunks.
For 1/, inputs can be rasters or point clouds (continuous binning or categorical grouping), or vectors (zonal = categorical grouping). We have to choose a reference spatial support to compare to, which by default is the input object on which the stats are computed (raster or point cloud), but this can be selected using
at=. For a vector input, we first create a categorical masking on the reference.Then, we re-use logic coded in co-sampling.
When comparing raster and point, we have essentially 4 modes (
raster_point_mode):interp_points)reduce_points)grid(method="idw" or "linear")grid(method="average"))The co-sampling then returns all datasets sampled at the same location following the above modes, and reference coordinates. Dask support here is directly derived from that in
interp_points,rasterize, etc. Nothing new except linking to those existing functions.From there, a couple routines help perform grouping in coordination with stratified sampling: subsampling within each groups. Those are pretty straightforward by reusing the subsampling module.
For 2/, we need to compute the statistics in chunks, and return them for the whole object.
We can do this exactly with a typical split-aggregate-combine workflow for many estimators (mean, STD, RMSE, etc). But other estimators require the full group loaded at once (median, NMAD, etc).
As this is not rocket science (pretty simple logic), we code our own so that it can also interface with other aspects (subsampling, vector geometry considerations etc) and run both in Dask and Multiprocessing.
In order we do these steps:
reproject(),create_mask(),interp_points()andgrid().min,maxchunk accumulation to do out-of-memory).Then, done!
Benchmarking tests show the performance is on-par with Flox, so we have a good implementation 🙂.
Filtering and convolution
I have moved the Numba/SciPy dual-convolution implementation of xDEM in GeoUtils. I used the opportunity to link it to our existing filters, so that it is used consistently. It adds more modularity than the old
uniform_filterimplementation used formean_filterbefore thegeneric_filterof SciPy came out (e.g., we can pass any shape, including circular, instead of just the square ofuniform_filter). I also added aminmaxfilter in Numba, so that all of our filters consistently support both computational engines: SciPy or Numba! 😄