Skip to content

Note

Click here to download the full example code

smooth trend

a moving-window trend: at each location, the declustered average of the boreholes under a gaussian kernel. maps of seam thickness for several bandwidths, the bandwidth chosen by leave-one-out error, the residuals, and the local proportions of the mining categories.

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, HIGHLIGHT, INK, map_axes, save

the coal seam was drilled on a regular mesh, then infilled where it is thick, so the boreholes over-represent thick coal. cell declustering weights correct that, and the trend averages the holes with these weights.

data = bt.datasets.coal_seam_thickness()
holes, grid, lease = data["boreholes"], data["grid"], data["boundary"]
weights = bt.cell_declustering(holes, "THICKNESS_M", cell_size=500.0).weights
thickness = holes["THICKNESS_M"]
print(
    f"{len(holes)} holes, mean {thickness.mean():.2f} m, declustered {np.average(thickness, weights=weights):.2f} m"
)

(x0, y0, _), (x1, y1, _) = grid.bounds
extent = (x0, x1, y0, y1)
outside = grid["INSIDE"] == 0


def image(ax, values, **kwargs):
    values = grid.grid(np.where(outside, np.nan, values))[0]
    shown = ax.imshow(values, origin="lower", extent=extent, **kwargs)
    ax.plot(*lease.coords[:, :2].T, color=INK, lw=0.6)
    return shown

Out:

295 holes, mean 2.23 m, declustered 1.98 m

bandwidth

detrend with a bandwidth fits a smooth trend instead of a polynomial. the bandwidth is the standard deviation of the kernel: a small one follows every hole, a large one keeps only the regional shape. rotation and ratios, as for a variogram, make the kernel anisotropic.

fig, axes = plt.subplots(1, 3, figsize=(11, 3.2), sharey=True)
norm = plt.Normalize(0.5, 5.0)
for ax, bandwidth in zip(axes, (150.0, 400.0, 1500.0)):
    trend, _ = bt.detrend(holes, "THICKNESS_M", bandwidth=bandwidth, weights=weights)
    shown = image(ax, trend.predict(grid), norm=norm)
    map_axes(ax, f"Bandwidth {bandwidth:.0f} m")
for ax in axes[1:]:
    ax.set_ylabel("")
fig.colorbar(shown, ax=axes, label="Thickness trend (m)", shrink=0.8)
save(fig, "bandwidths")

Bandwidth 150 m, Bandwidth 400 m, Bandwidth 1500 m

automatic choice

given several bandwidths, detrend keeps the one with the least leave-one-out error: it compares each hole with the trend of the other holes and averages the squared differences with the declustering weights. too small a bandwidth chases noise, and too large a one misses the shape. noisier data get a larger bandwidth.

candidates = [100, 150, 200, 300, 400, 600, 800, 1200, 1600, 2400]
trend, residuals = bt.detrend(holes, "THICKNESS_M", bandwidth=candidates, weights=weights)
print(f"chosen bandwidth {trend.bandwidth:.0f} m")

fig, (left, right) = plt.subplots(1, 2, figsize=(10, 3.4), gridspec_kw={"width_ratios": [1, 1.3]})
left.plot(trend.bandwidths, trend.scores, "o-", color=GRAY, ms=4)
best = trend.scores.argmin()
left.plot(trend.bandwidths[best], trend.scores[best], "o", color=HIGHLIGHT, ms=7)
left.set_xscale("log")
left.set_xlabel("Bandwidth (m)")
left.set_ylabel("Leave-one-out error (m²)")
left.set_title("Bandwidth choice")
shown = image(right, trend.predict(grid), norm=norm)
right.scatter(*holes.coords[:, :2].T, s=2, color=INK)
map_axes(right, f"Trend, bandwidth {trend.bandwidth:.0f} m")
fig.colorbar(shown, ax=right, label="Thickness (m)", shrink=0.8)
save(fig, "automatic")

Bandwidth choice, Trend, bandwidth 300 m

Out:

chosen bandwidth 300 m

residuals

the residuals, thickness minus trend at the holes, keep the short-scale variation. they center on zero and correlate weakly with the trend, but their spread grows with it: thick coal varies more, a proportional effect. normal-scoring within classes of the trend, as SGS does with a trend, removes both.

at_holes = trend.predict(holes)
print(f"residual mean {np.average(residuals, weights=weights):+.3f} m, sd {residuals.std():.2f} m")
print(f"correlation with the trend {np.corrcoef(at_holes, residuals)[0, 1]:+.2f}")

fig, (left, right) = plt.subplots(
    1, 2, figsize=(10.5, 3.6), gridspec_kw={"width_ratios": [1.3, 1]}, layout="constrained"
)
left.plot(*lease.coords[:, :2].T, color=INK, lw=0.6)
limit = np.abs(residuals).max()
dots = left.scatter(*holes.coords[:, :2].T, c=residuals, s=10, cmap="RdBu_r", vmin=-limit, vmax=limit)
map_axes(left, "Residuals at the holes")
fig.colorbar(dots, ax=left, label="Thickness − trend (m)", shrink=0.8)
right.scatter(at_holes, residuals, s=6, color=ACCENT, alpha=0.6)
right.axhline(0, color=GRAY, lw=0.8)
right.set_xlabel("Trend (m)")
right.set_ylabel("Residual (m)")
right.set_title("Residual against trend")
save(fig, "residuals")

Residuals at the holes, Residual against trend

Out:

residual mean -0.011 m, sd 0.42 m
correlation with the trend +0.20

the trend at the holes and at the cells can feed a simulation (SGS): SGS.fit(holes, "THICKNESS_M", trend=trend.predict(holes)) then simulate(grid, trend=trend.predict(grid)) simulates the thickness conditioned on the local trend. cells beyond four bandwidths of every hole have no trend (NaN), so drop them first.

category proportions

with categorical=True, detrend smooths the indicator of each category instead. the trend is then a table of local proportions, one column per category, each in [0, 1] and summing to 1 at every cell.

categories, _ = bt.detrend(holes, "CATEGORY", bandwidth=candidates, weights=weights, categorical=True)
proportions = categories.predict(grid)
total = sum(proportions[name] for name in categories.categories)
print(
    f"bandwidth {categories.bandwidth:.0f} m; proportions sum to 1: {np.allclose(total[~np.isnan(total)], 1.0)}"
)

fig, axes = plt.subplots(1, 3, figsize=(11, 3.2), sharey=True)
for ax, name in zip(axes, categories.categories):
    shown = image(ax, proportions[name], vmin=0.0, vmax=1.0, cmap="Greys")
    map_axes(ax, name.capitalize())
for ax in axes[1:]:
    ax.set_ylabel("")
fig.colorbar(shown, ax=axes, label="Proportion", shrink=0.8)
save(fig, "proportions")

Mechanized, Selective, Uneconomic

Out:

bandwidth 600 m; proportions sum to 1: True

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

Download Python source code: example_04_08.py

Download Jupyter notebook: example_04_08.ipynb

Gallery generated by mkdocs-gallery