pycsamt.emtools.diag#

Polar uncertainty diagnostics for CSAMT impedance data.

Adapts the polar-based visualization framework from:
kouadio2025Kouadio K.L. (2025), “k-diagram: Rethinking Forecasting

Uncertainty via Polar-based Visualization”, J. Open Source Softw. 10(116), 8661. DOI: 10.21105/joss.08661

The three core diagnostics from k-diagram are translated to CSAMT:

Coverage evaluation (kouadio2025 eq. 1):

c_j = 1(L_j ≤ ρ_a,obs,j ≤ U_j) Binary coverage of per-frequency observed apparent resistivity within the predicted quantile interval [L_j, U_j].

Frequency-width drift (analogous to Horizon Drift, kouadio2025 Fig 2c):

w̄_b = mean_{j ∈ band_b} (U_j − L_j) / ρ_a,obs,j × 100 [%] Mean relative interval width per frequency band — tracks how prediction uncertainty grows with period (a proxy for depth).

Relative-error polar histogram (analogous to polar violin, Fig 2b):

ε_j = (ρ_a,pred,j − ρ_a,obs,j) / ρ_a,obs,j × 100 [%] Rose diagram of residuals binned by frequency decade.

Functions

coverage_score(y_true, y_lo, y_hi)

Empirical coverage fraction of a prediction interval (kouadio2025 eq.

coverage_table(sites, q_lo, q_hi, *[, ...])

Per-station coverage summary.

plot_polar_coverage(sites, q_lo, q_hi, *[, ...])

Polar coverage plot: angle ∝ log₁₀(f), radius ∝ ρ_a,obs.

plot_polar_errors(sites, model_rho, *[, ...])

Polar rose diagram of relative residuals (ε = (ρ_pred − ρ_obs)/ρ_obs × 100 %).

plot_width_drift(sites, q_lo, q_hi, *[, ...])

Mean relative interval width per frequency band (horizon-drift analogue).

rho_coverage(sites, q_lo, q_hi, *[, ...])

Per-frequency coverage of observed ρ_a within predicted quantile bounds.

rho_error_stats(sites, model_rho, *[, ...])

Per-frequency relative error between observed and predicted ρ_a.

pycsamt.emtools.diag.coverage_score(y_true, y_lo, y_hi)[source]#

Empirical coverage fraction of a prediction interval (kouadio2025 eq. 1).

Parameters:
  • y_true (array-like) – Observed values.

  • y_lo (array-like) – Lower and upper bounds of the predicted interval.

  • y_hi (array-like) – Lower and upper bounds of the predicted interval.

Returns:

cov – Fraction of observations that fall inside [y_lo, y_hi] ∈ [0, 1].

Return type:

float

pycsamt.emtools.diag.rho_coverage(sites, q_lo, q_hi, *, rho_comp='xy', recursive=True, on_dup='replace', strict=False, verbose=0)[source]#

Per-frequency coverage of observed ρ_a within predicted quantile bounds.

For each site and frequency, checks whether the Cagniard apparent resistivity extracted from the observed Z tensor falls inside the predicted interval [L_j, U_j] (kouadio2025 eq. 1):

c_j = 1(L_j ≤ ρ_a,obs,j ≤ U_j)

Parameters:
  • sites (Sites | list) – Observed CSAMT sites.

  • q_lo (dict {station: array} or array or scalar) – Lower / upper quantile bounds aligned with each site’s frequency array. When a dict, keys must match station names; sites without a key are skipped. A scalar broadcasts to every frequency of every site.

  • q_hi (dict {station: array} or array or scalar) – Lower / upper quantile bounds aligned with each site’s frequency array. When a dict, keys must match station names; sites without a key are skipped. A scalar broadcasts to every frequency of every site.

  • rho_comp ({"xy", "yx"}) – Impedance component used to derive ρ_a (default "xy").

  • recursive (bool) – Forwarded to ensure_sites().

  • on_dup (str) – Forwarded to ensure_sites().

  • strict (bool) – Forwarded to ensure_sites().

  • verbose (int) – Forwarded to ensure_sites().

Returns:

Columns: station, freq_hz, period_s, rho_obs, q_lo, q_hi, covered, width_pct. covered is bool; width_pct = 100 × (q_hi−q_lo) / ρ_obs.

Return type:

pd.DataFrame

pycsamt.emtools.diag.rho_error_stats(sites, model_rho, *, rho_comp='xy', recursive=True, on_dup='replace', strict=False, verbose=0)[source]#

Per-frequency relative error between observed and predicted ρ_a.

Computes ε_j = (ρ_a,pred,j − ρ_a,obs,j) / ρ_a,obs,j × 100 % for each station and frequency, analogous to the error distribution visualised in the k-diagram polar violin (kouadio2025 Fig 2b).

Parameters:
  • sites (Sites | list) – Observed CSAMT sites.

  • model_rho (dict {station: array} or Sites-like) – Predicted apparent resistivity. Either a dict mapping station names to 1-D arrays (same length as the corresponding site’s frequency array), or a Sites-like container from which ρ_a is extracted with the same rho_comp setting.

  • rho_comp ({"xy", "yx"}) – Impedance component.

  • recursive (bool) – Forwarded to ensure_sites().

  • on_dup (str) – Forwarded to ensure_sites().

  • strict (bool) – Forwarded to ensure_sites().

  • verbose (int) – Forwarded to ensure_sites().

Returns:

Columns: station, freq_hz, period_s, rho_obs, rho_pred, rel_err_pct, abs_err_pct.

Return type:

pd.DataFrame

pycsamt.emtools.diag.coverage_table(sites, q_lo, q_hi, *, rho_comp='xy', nominal=0.9, recursive=True, on_dup='replace', strict=False, verbose=0)[source]#

Per-station coverage summary.

Parameters:
Returns:

Columns: station, n_freq, empirical_cov, mean_width_pct, calibrated_flag.

Return type:

pd.DataFrame

pycsamt.emtools.diag.plot_polar_coverage(sites, q_lo, q_hi, *, rho_comp='xy', log_radius=True, n_freq_ticks=8, figsize=(7, 7), title='Coverage evaluation', recursive=True, on_dup='replace', strict=False, verbose=0, ax=None)[source]#

Polar coverage plot: angle ∝ log₁₀(f), radius ∝ ρ_a,obs.

Green markers = observed ρ_a within predicted interval (covered); red = outside. Thin radial segments show each [q_lo, q_hi] range. Every station shares (almost) the same frequency grid, so each angular position is really one frequency shared by every station — the angle axis is labelled with that frequency directly (rather than the otherwise-meaningless default degree ticks) so a reader can tell which part of the band a cluster of misses falls in.

Parameters:
  • n_freq_ticks (int, default 8) – Number of evenly (log-)spaced frequency labels drawn around the ring. Set to 0 to fall back to the default degree ticks.

  • sites (Any)

  • q_lo (dict | ndarray | float)

  • q_hi (dict | ndarray | float)

  • rho_comp (str)

  • log_radius (bool)

  • figsize (tuple)

  • title (str)

  • recursive (bool)

  • on_dup (str)

  • strict (bool)

  • verbose (int)

Returns:

ax

Return type:

matplotlib.axes.Axes (polar projection)

pycsamt.emtools.diag.plot_polar_errors(sites, model_rho, *, rho_comp='xy', n_bins=18, figsize=(7, 7), title='Error distribution', recursive=True, on_dup='replace', strict=False, verbose=0, ax=None)[source]#

Polar rose diagram of relative residuals (ε = (ρ_pred − ρ_obs)/ρ_obs × 100 %).

Each angular sector spans one frequency decade. Bar length = mean |ε| within that sector; red = over-prediction (mean ε > 0), blue = under. Analogous to the polar violin in kouadio2025 Fig 2b.

Returns:

ax

Return type:

matplotlib.axes.Axes (polar projection)

Parameters:
pycsamt.emtools.diag.plot_width_drift(sites, q_lo, q_hi, *, rho_comp='xy', n_bands=8, polar=False, figsize=(8, 4), title='Frequency-width drift', recursive=True, on_dup='replace', strict=False, verbose=0, ax=None)[source]#

Mean relative interval width per frequency band (horizon-drift analogue).

Visualises how the predicted interval width — relative to observed ρ_a — changes across the frequency spectrum, a proxy for how model uncertainty grows with probing depth (kouadio2025 § Forecast Horizon Drift).

Parameters:
Returns:

ax

Return type:

matplotlib.axes.Axes