Skip to content

Note

Click here to download the full example code

declustering

a coal seam drilled on a regular mesh, then infilled where the seam is thick. the infill holes over-represent thick coal, so the plain mean of the boreholes overstates the thickness of the seam. declustering weights each hole by the area it stands for: cell declustering by the number of holes sharing its cell, polygonal declustering by the area nearer to it than to any other hole.

import sys
from pathlib import Path

HERE = Path(__file__).parent if "__file__" in globals() else Path.cwd()
sys.path.insert(0, str(HERE.parents[1]))
import boitata as bt
import matplotlib.pyplot as plt
import numpy as np
from common import ACCENT, GRAY, INK, map_axes, save

data = bt.datasets.coal_seam_thickness()
holes, lease = data["boreholes"], data["boundary"]
thickness = holes["THICKNESS_M"]
x, y = holes.x, holes.y
print(f"{len(holes)} holes, mean thickness {thickness.mean():.2f} m")

fig, ax = plt.subplots(figsize=(7, 4.4), layout="constrained")
drawn = ax.scatter(x, y, c=thickness, s=14, edgecolors=INK, linewidths=0.3)
ax.plot(*lease.coords[:, :2].T, color=GRAY, lw=0.8)
fig.colorbar(drawn, ax=ax, shrink=0.8, label="Thickness (m)")
map_axes(ax, "Boreholes: infill where the seam is thick")
save(fig, "holes")

Boreholes: infill where the seam is thick

Out:

295 holes, mean thickness 2.23 m

cell declustering

each hole gets the inverse of the number of holes in its cell. too small a cell holds one hole and changes nothing; too large a cell holds the whole lease. in between, when the infill targets high values, the declustered mean dips. cell_declustering scans cell sizes, averages each over 25 grid origins, and keeps the size with the lowest mean. the weights average 1.

cell = bt.cell_declustering(holes, "THICKNESS_M", sizes=np.arange(100.0, 3100.0, 100.0))
print(
    f"cell size {cell.cell_size:.0f} m, declustered mean {np.average(thickness, weights=cell.weights):.2f} m"
)
print(f"scanned mean at that size {cell.means[cell.sizes == cell.cell_size][0]:.2f} m")

fig, ax = plt.subplots(figsize=(6, 3.4), layout="constrained")
bt.plot.declustering(cell, naive=thickness.mean(), ax=ax, color=ACCENT, lw=1.6)
ax.set(title="Declustered mean against cell size", xlabel="Cell size (m)", ylabel="Mean thickness (m)")
save(fig, "cell_sizes")

Declustered mean against cell size

Out:

cell size 700 m, declustered mean 1.91 m
scanned mean at that size 1.91 m

the mean falls from 100 m cells, which hold about one hole each, to a flat minimum between 700 and 900 m. it then rises slowly as the cells grow to hold mesh and infill holes alike. choosing the cell size is a judgment, and the minimum guides it only when you know the infill targets high values, as here. the curve averages 25 grid origins, while the returned weights come from one origin at the corner of the data, so their mean (the dot) sits a little above the curve.

polygonal declustering

polygon_declustering weights each hole by the area of the grid nodes nearest to it, over the bounding box of the holes. it needs no cell size, but the holes on the edge take all the area out to the box, and the box differs from the lease.

polygon = bt.polygon_declustering(holes, "THICKNESS_M")
print(f"polygonal declustered mean {np.average(thickness, weights=polygon.weights):.2f} m")
fig, axes = plt.subplots(1, 2, figsize=(10, 3.6), layout="constrained", sharey=True)
for ax, result, name in zip(axes, [cell, polygon], ["Cell", "Polygonal"], strict=True):
    w = result.weights
    drawn = ax.scatter(x, y, c=w, s=14, cmap="cividis", vmin=0, vmax=np.percentile(w, 98))
    ax.plot(*lease.coords[:, :2].T, color=GRAY, lw=0.8)
    map_axes(ax, f"{name} weights")
    fig.colorbar(drawn, ax=ax, shrink=0.8)
axes[1].set_ylabel("")
save(fig, "weights")

Cell weights, Polygonal weights

Out:

polygonal declustered mean 1.95 m

both methods give the infill holes small weights and the sparse mesh large ones, and they agree on the declustered mean. the largest polygonal weights sit on the edge of the drilling, where the box reaches past the lease. the declustered histograms shift toward thin coal.

bins = np.linspace(0, np.ceil(thickness.max()), 25)
fig, ax = plt.subplots(figsize=(6, 3.4), layout="constrained")
ax.hist(thickness, bins, weights=np.full(len(holes), 1 / len(holes)), color="#e6e6e6", label="equal weights")
for result, color, label in [(cell, ACCENT, "cell"), (polygon, INK, "polygonal")]:
    ax.hist(
        thickness,
        bins,
        weights=result.weights / len(holes),
        histtype="step",
        color=color,
        lw=1.4,
        label=label,
    )
ax.set(title="Thickness, declustered", xlabel="Thickness (m)", ylabel="Proportion")
ax.legend()
save(fig, "histograms")

Thickness, declustered

these weights describe the data: histograms, statistics per domain, top cuts and the target of a normal-score transform. weight declustering derives declustering weights from estimation weights, from what each hole contributes to estimating the whole lease.

Total running time of the script: ( 0 minutes 0.579 seconds)

Download Python source code: example_03_03.py

Download Jupyter notebook: example_03_03.ipynb

Gallery generated by mkdocs-gallery