Quality-Control Confidence Scoring#
pycsamt.emtools.qc turns transfer-function quality control into
tables and figures. It does two related jobs:
summarize station-level data quality, coverage, SNR, tipper presence, and phase-tensor skew;
compute confidence ratios from several finite, bounded scores so that stations and individual station-frequency samples can be ranked, plotted, masked, or down-weighted before inversion.
Full callable signatures live in the API reference. This page explains the confidence-ratio formulation, the table workflow, the plotting workflow, and how confidence values should be read in practice.
Why QC Is More Than Coverage#
A station can be complete and still be questionable. frac_ok=1.0
only says that impedance rows are finite. It does not say that the
off-diagonal modes agree, that diagonal leakage is small, that phase is
smooth, that uncertainty is low, or that the station is coherent with
its neighbours.
The QC module therefore separates three ideas:
coverageAre the required transfer-function rows finite?
confidenceHow trustworthy is a station or a station-frequency cell after combining coverage, uncertainty, tensor-shape, phase, and spatial criteria?
flagsWhich simple thresholds does a station or frequency cell fail?
Load A Survey#
All QC functions accept the usual pyCSAMT inputs, but a reproducible
script should normalize once with ensure_sites.
1from pathlib import Path
2
3from pycsamt.emtools import ensure_sites
4
5survey = ensure_sites(
6 Path("data/AMT/WILLY_DATA/L18PLT"),
7 recursive=True,
8 on_dup="replace",
9 strict=True,
10 verbose=1,
11)
Use strict=True for reports and automated processing. Use
strict=False in exploratory notebooks if you want empty plots to
render as “no data” messages.
The Confidence Ratio#
The composite confidence ratio is a weighted mean of available component scores:
Missing scores are ignored. Finite scores are clipped to [0, 1].
The default weights are:
Score |
Weight |
Meaning |
|---|---|---|
|
|
Finite impedance rows or components. |
|
|
Median relative impedance error. |
|
|
Similarity of |
|
|
Penalty for diagonal leakage into off-diagonal response. |
|
|
Penalty for abrupt off-diagonal phase jumps. |
|
|
Coherence with neighbouring stations. |
The default confidence bands are:
CR >= 0.95: safe / retained;0.85 <= CR < 0.95: recoverable or marginal;CR < 0.85: reject, down-weight, or manually review.
Compute A Confidence Ratio Directly#
Use confidence_ratio when you already have scores and want to apply
the same weighted formula used by the tables.
1from pycsamt.emtools.qc import confidence_ratio
2
3scores = {
4 "coverage": 1.00,
5 "uncertainty": 0.82,
6 "offdiag": 0.76,
7 "diagonal": 0.55,
8 "phase": 0.90,
9 "spatial": 0.88,
10}
11
12cr, cr_err = confidence_ratio(
13 scores,
14 n_freq=53,
15 return_error=True,
16)
17
18print(f"CR={cr:.3f} +/- {cr_err:.3f}")
CR=0.861 +/- 0.141
confidence_err is the spread of the available component scores. If
only one score is available, it falls back to
sqrt(CR * (1 - CR) / n_freq).
Station QC Summary#
build_qc_table is the first station-level table. It reports the
number of frequencies, finite-row coverage, tipper availability, median
row SNR when z_err exists, period range, and optional phase-tensor
skew.
1from pycsamt.emtools import build_qc_table, ensure_sites
2
3survey = ensure_sites("data/AMT/WILLY_DATA/L18PLT", strict=True)
4
5qc = build_qc_table(
6 survey,
7 include_skew=True,
8 api=False,
9)
10
11print(
12 qc[
13 [
14 "station",
15 "n_freq",
16 "n_ok",
17 "frac_ok",
18 "n_tip",
19 "snr_med",
20 "pmin",
21 "pmax",
22 "skew_med",
23 ]
24 ].head()
25)
station n_freq n_ok frac_ok ... snr_med pmin pmax skew_med
0 18-001A 53 53 1.0 ... 17.658396 0.000096 0.992063 50.326802
1 18-002U 53 53 1.0 ... 16.687366 0.000096 0.992063 36.059416
2 18-003A 53 53 1.0 ... 12.031672 0.000096 0.992063 31.245824
3 18-004A 53 53 1.0 ... 10.430580 0.000096 0.992063 31.005169
4 18-005U 53 53 1.0 ... 14.360341 0.000096 0.992063 36.404849
[5 rows x 9 columns]
Read frac_ok as completeness, not as full confidence. Read
snr_med as a row-level signal-to-error ratio when impedance error
tensors are available. Read skew_med as structural/tensor
complexity, not automatically as bad acquisition.
Station Flags#
qc_flags adds simple threshold labels to the station QC table.
1from pycsamt.emtools import qc_flags
2
3flags = qc_flags(
4 "data/AMT/WILLY_DATA/L18PLT",
5 min_frac_ok=0.60,
6 min_snr_med=2.0,
7 max_skew_med=6.0,
8)
9
10flagged = flags[flags["flags"] != ""]
11print(flagged[["station", "frac_ok", "snr_med", "skew_med", "flags"]])
station frac_ok snr_med skew_med flags
0 18-001A 1.0 17.658396 50.326802 high_skew
1 18-002U 1.0 16.687366 36.059416 high_skew
2 18-003A 1.0 12.031672 31.245824 high_skew
3 18-004A 1.0 10.430580 31.005169 high_skew
4 18-005U 1.0 14.360341 36.404849 high_skew
5 18-006A 1.0 13.272516 30.359830 high_skew
6 18-007U 1.0 14.556106 34.174772 high_skew
7 18-008U 1.0 15.890594 40.862593 high_skew
8 18-009A 1.0 14.084767 25.288856 high_skew
9 18-010U 1.0 13.677553 26.006304 high_skew
10 18-011A 1.0 12.575896 27.534040 high_skew
11 18-012A 1.0 12.873938 32.307319 high_skew
12 18-013U 1.0 11.944265 29.705384 high_skew
13 18-014A 1.0 14.847554 35.458520 high_skew
14 18-015U 1.0 13.456819 22.459269 high_skew
15 18-016A 1.0 14.746954 23.525350 high_skew
16 18-017U 1.0 13.951389 22.912833 high_skew
17 18-018A 1.0 19.352988 66.547818 high_skew
18 18-019U 1.0 14.483638 61.945234 high_skew
19 18-020A 1.0 18.506868 45.332864 high_skew
20 18-021B 1.0 9.339868 52.388774 high_skew
21 18-021U 1.0 14.119590 55.017266 high_skew
22 18-022U 1.0 8.557456 65.349813 high_skew
23 18-022V 1.0 11.268011 66.787861 high_skew
24 18-023A 1.0 9.854384 67.022970 high_skew
25 18-023V 1.0 12.137127 59.306877 high_skew
26 18-024U 1.0 8.987776 63.853268 high_skew
27 18-025A 1.0 11.770850 55.354566 high_skew
Possible station flags include low_coverage, low_snr, and
high_skew. A high-skew flag can be a real structural signal. It
does not mean the station should automatically be deleted.
Presence Confidence Versus Composite Confidence#
station_confidence_table has two modes. method="presence" is
coverage-only. method="composite" combines all available component
scores.
1from pycsamt.emtools import station_confidence_table
2
3presence = station_confidence_table(
4 "data/AMT/WILLY_DATA/L18PLT",
5 method="presence",
6 api=False,
7)
8composite = station_confidence_table(
9 "data/AMT/WILLY_DATA/L18PLT",
10 method="composite",
11 api=False,
12)
13
14print("presence range")
15print(presence["confidence"].min(), presence["confidence"].max())
16
17print("composite range")
18print(composite["confidence"].min(), composite["confidence"].max())
19
20ranked = composite.sort_values("confidence")
21print(ranked[["station", "confidence", "coverage", "uncertainty",
22 "offdiag", "diagonal", "phase", "spatial"]].head())
presence range
1.0 1.0
composite range
0.5440199944767435 0.8119223880398303
station confidence coverage ... diagonal phase spatial
22 18-022U 0.544020 1.0 ... 0.000000 0.941638 0.000000
21 18-021U 0.570986 1.0 ... 0.000000 0.938165 0.000000
17 18-018A 0.578410 1.0 ... 0.086979 0.957546 0.000000
16 18-017U 0.595479 1.0 ... 0.261699 0.969783 0.038160
18 18-019U 0.615292 1.0 ... 0.000000 0.959348 0.541804
[5 rows x 8 columns]
If presence confidence is high everywhere but composite confidence varies, the survey is complete but not equally trustworthy everywhere. That is common in real EM data.
Customize Confidence Weights#
Use custom weights when a project has a clear processing policy. For example, inversion preparation may emphasize uncertainty and coverage, while structural interpretation may care more about off-diagonal and spatial coherence.
1from pycsamt.emtools import station_confidence_table
2
3weights = {
4 "coverage": 0.40,
5 "uncertainty": 0.30,
6 "offdiag": 0.10,
7 "diagonal": 0.05,
8 "phase": 0.05,
9 "spatial": 0.10,
10}
11
12table = station_confidence_table(
13 "data/AMT/WILLY_DATA/L18PLT",
14 method="composite",
15 weights=weights,
16 relerr_threshold=0.25,
17 offdiag_tolerance_log10=0.40,
18 diagonal_leakage_max=0.40,
19 phase_jump_tolerance_deg=90.0,
20 spatial_tolerance_log10=0.60,
21 api=False,
22)
23
24print(table.sort_values("confidence").head())
station distance_m confidence ... diagonal phase spatial
22 18-022U 4400.0 0.630495 ... 0.000000 0.941638 0.000000
21 18-021U 4200.0 0.659512 ... 0.000000 0.938165 0.000000
17 18-018A 3400.0 0.666682 ... 0.201107 0.957546 0.000000
16 18-017U 3200.0 0.672222 ... 0.353986 0.969783 0.038160
14 18-015U 2800.0 0.708737 ... 0.514479 0.965328 0.575751
[5 rows x 13 columns]
Changing thresholds changes the meaning of the scores. Record custom weights and thresholds in reports so another user can reproduce your confidence classes.
Frequency-Level Confidence#
frequency_confidence_table returns one row per station-frequency
sample. This is the table to use for period-band decisions, masks, and
inversion down-weighting.
1from pycsamt.emtools import frequency_confidence_table
2
3freq_qc = frequency_confidence_table(
4 "data/AMT/WILLY_DATA/L18PLT",
5 method="composite",
6 ci_hi=0.95,
7 ci_lo=0.85,
8 api=False,
9)
10
11print(freq_qc.columns.tolist())
12print(freq_qc[["station", "frequency_hz", "period_s",
13 "confidence", "flags"]].head())
14
15rejected = freq_qc[freq_qc["flags"].str.contains("reject", na=False)]
16print("rejected cells:", len(rejected))
['station', 'station_index', 'distance_m', 'frequency_hz', 'period_s', 'log10_period', 'confidence', 'confidence_err', 'method', 'n_components', 'coverage', 'uncertainty', 'offdiag', 'diagonal', 'phase', 'spatial', 'logrho_proxy', 'flags']
station ... flags
0 18-001A ... recoverable,high_error,offdiag_mismatch,diagon...
1 18-001A ... reject,high_error,offdiag_mismatch,diagonal_le...
2 18-001A ... reject,high_error,offdiag_mismatch,diagonal_le...
3 18-001A ... reject,high_error,offdiag_mismatch,diagonal_le...
4 18-001A ... reject,high_error,offdiag_mismatch,diagonal_le...
[5 rows x 5 columns]
rejected cells: 1479
Frequency flags can include reject, recoverable, missing,
high_error, offdiag_mismatch, diagonal_leakage,
phase_jump, and spatial_outlier.
Build A Mask From Confidence#
The QC module does not force one masking policy. You can build one from the confidence table.
1from pycsamt.emtools import frequency_confidence_table
2
3table = frequency_confidence_table(
4 "data/AMT/WILLY_DATA/L18PLT",
5 method="composite",
6 ci_lo=0.85,
7 api=False,
8)
9
10keep = table["confidence"] >= 0.85
11review = (table["confidence"] >= 0.70) & (table["confidence"] < 0.85)
12drop = table["confidence"] < 0.70
13
14print("keep:", int(keep.sum()))
15print("review:", int(review.sum()))
16print("drop:", int(drop.sum()))
keep: 5
review: 528
drop: 951
Use a review band instead of a hard delete when the flagged frequencies line up with known structural complexity. Low confidence is a prompt for inspection, not always proof of bad data.
Station Confidence Profile#
plot_confidence_profile plots station confidence along the line. It
uses green, pink, and red markers for safe, recoverable, and rejected
stations.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools import plot_confidence_profile
4
5fig, ax = plt.subplots(figsize=(9.5, 4.2))
6plot_confidence_profile(
7 "data/AMT/WILLY_DATA/L18PLT",
8 method="composite",
9 ci_hi=0.95,
10 ci_lo=0.85,
11 shade_mode="score",
12 station_label_step=2,
13 show_errorbars=True,
14 ax=ax,
15)
16
17fig.tight_layout()
18fig.savefig("confidence_profile_l18plt.png", dpi=200)
19plt.close(fig)
If no station coordinate metadata are available, distance falls back to
regular spacing controlled by spacing_m.
Frequency Confidence Pseudo-Section#
plot_frequency_confidence_psection shows confidence by station and
period. It can plot any metric column from the frequency table, not only
confidence.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools import plot_frequency_confidence_psection
4
5fig, ax = plt.subplots(figsize=(10.0, 4.8))
6plot_frequency_confidence_psection(
7 "data/AMT/WILLY_DATA/L18PLT",
8 method="composite",
9 metric="confidence",
10 station_label_step=2,
11 ax=ax,
12)
13
14fig.savefig("frequency_confidence_psection.png", dpi=200)
15plt.close(fig)
Change metric to "uncertainty", "offdiag",
"diagonal", "phase", or "spatial" to see which component
is driving low confidence.
Single-Station Spectrum#
plot_station_confidence_spectrum overlays the overall confidence
curve and component scores for one station.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools import plot_station_confidence_spectrum
4
5fig, ax = plt.subplots(figsize=(7.5, 4.2))
6plot_station_confidence_spectrum(
7 "data/AMT/WILLY_DATA/L18PLT",
8 station="18-022U",
9 method="composite",
10 ax=ax,
11)
12
13fig.savefig("station_confidence_spectrum_18-022U.png", dpi=200)
14plt.close(fig)
Use this plot when you know a station is weak and want to see whether the problem comes from uncertainty, diagonal leakage, off-diagonal mismatch, phase jumps, or spatial incoherence.
Single-Station Dashboard#
plot_station_confidence_dashboard breaks the same information into
separate panels. It is easier to read than the overlaid spectrum when
many score components overlap.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools import plot_station_confidence_dashboard
4
5fig = plot_station_confidence_dashboard(
6 "data/AMT/WILLY_DATA/L18PLT",
7 station="18-022U",
8 method="composite",
9 ci_hi=0.95,
10 ci_lo=0.85,
11 figsize=(11.0, 7.0),
12)
13
14fig.savefig("station_confidence_dashboard_18-022U.png", dpi=200)
15plt.close(fig)
Use dashboards for station-by-station review before deciding whether a low-confidence station should be edited, down-weighted, or retained.
Period-Band Summary#
plot_confidence_band_summary collapses the frequency table by
period. It plots median and mean confidence and shades the fraction of
stations in rejected or recoverable bands.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools import plot_confidence_band_summary
4
5fig, ax = plt.subplots(figsize=(8.5, 4.2))
6plot_confidence_band_summary(
7 "data/AMT/WILLY_DATA/L18PLT",
8 method="composite",
9 ci_hi=0.95,
10 ci_lo=0.85,
11 ax=ax,
12)
13
14fig.savefig("confidence_band_summary.png", dpi=200)
15plt.close(fig)
This view is useful when deciding whether a whole period band should be edited, down-weighted, or treated cautiously.
Coverage And SNR Quicklook#
plot_qc_quicklook combines three first-pass plots: a presence
pseudo-section, an SNR pseudo-section, and an SNR histogram.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools.qc import plot_qc_quicklook
4
5fig = plot_qc_quicklook(
6 "data/AMT/WILLY_DATA/L18PLT",
7 figsize=(10.0, 8.0),
8)
9
10fig.savefig("qc_quicklook_l18plt.png", dpi=200)
11plt.close(fig)
If the SNR histogram says error tensors are not available, the survey can still be inspected, but uncertainty-based scores will be missing from the composite confidence ratio.
Coverage Pseudo-Section And SNR Histogram#
For more control, call the helper plots separately.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools.qc import plot_coverage_psection, plot_snr_hist
4
5fig, axes = plt.subplots(1, 2, figsize=(12.0, 4.5))
6
7plot_coverage_psection(
8 "data/AMT/WILLY_DATA/L18PLT",
9 metric="presence",
10 ax=axes[0],
11)
12axes[0].set_title("Finite-row presence")
13
14plot_snr_hist(
15 "data/AMT/WILLY_DATA/L18PLT",
16 bins=40,
17 ax=axes[1],
18)
19axes[1].set_title("Row SNR distribution")
20
21fig.tight_layout()
22fig.savefig("coverage_and_snr.png", dpi=200)
23plt.close(fig)
metric="presence" shows finite rows. metric="snr" colours by
row SNR when z_err exists. metric="offdiag" shows an
off-diagonal amplitude proxy.
Consistency Fan#
plot_consistency_fan propagates impedance errors through apparent
resistivity by Monte Carlo sampling. It can compare xy and yx
apparent-resistivity bands for one station.
1import matplotlib.pyplot as plt
2import numpy as np
3
4from pycsamt.emtools.qc import overlay_noise_cone, plot_consistency_fan
5
6fig, ax = plt.subplots(figsize=(8.8, 4.5))
7plot_consistency_fan(
8 "data/AMT/WILLY_DATA/L18PLT",
9 station="18-016A",
10 comps=("xy", "yx"),
11 pcts=(10.0, 50.0, 90.0),
12 n_draws=300,
13 ax=ax,
14)
15ax.set_yscale("log")
16
17period = np.logspace(-4, 0, 30)
18overlay_noise_cone(
19 ax,
20 period,
21 lo=np.full(period.size, 10.0),
22 hi=np.full(period.size, 100.0),
23 color="0.5",
24 alpha=0.20,
25)
26
27fig.savefig("consistency_fan_18-016A.png", dpi=200)
28plt.close(fig)
The noise cone overlay is a visual reference band. It is not estimated by the QC module. Supply project-specific lower and upper bounds when you use it in a report.
XY/YX Crossover Map#
plot_xyyx_crossover_map marks periods where rho_xy and
rho_yx swap which one is larger. It is a cheap anisotropy and
mode-consistency diagnostic.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools.qc import (
4 overlay_spectral_holes,
5 plot_xyyx_crossover_map,
6)
7
8fig, ax = plt.subplots(figsize=(9.5, 4.8))
9plot_xyyx_crossover_map(
10 "data/AMT/WILLY_DATA/L18PLT",
11 ax=ax,
12)
13overlay_spectral_holes(
14 ax,
15 "data/AMT/WILLY_DATA/L18PLT",
16 thresh_dec=0.30,
17)
18
19fig.savefig("xy_yx_crossover_map.png", dpi=200)
20plt.close(fig)
overlay_spectral_holes shades large gaps in log-period sampling on
top of a pseudo-section-style axis. Lower thresh_dec only when you
intentionally want to reveal small grid spacing differences.
Propagation To Inversion#
For MARE2DEM exports created from EDI data, CR-derived uncertainty propagation can be enabled with:
1from pycsamt.models.mare2dem.edi import make_mt_data_from_edi
2
3make_mt_data_from_edi(
4 "data/AMT/WILLY_DATA/L18PLT",
5 "mare2dem_data_with_confidence.emdata",
6 confidence_weighting=True,
7)
The effective relative impedance error is inflated as confidence decreases:
The defaults are CR_min=0.05 and p=1. The usual propagation is
then:
Confidence weighting should increase uncertainty for low-confidence data. It should not make any datum artificially more precise.
Build A QC Report Bundle#
The following script writes station tables, frequency tables, and the main QC figures for one line.
1from pathlib import Path
2
3import matplotlib.pyplot as plt
4
5from pycsamt.emtools import (
6 build_qc_table,
7 ensure_sites,
8 frequency_confidence_table,
9 plot_confidence_band_summary,
10 plot_confidence_profile,
11 plot_frequency_confidence_psection,
12 qc_flags,
13 station_confidence_table,
14)
15from pycsamt.emtools.qc import plot_qc_quicklook
16
17out = Path("qc_report_l18plt")
18out.mkdir(parents=True, exist_ok=True)
19
20survey = ensure_sites("data/AMT/WILLY_DATA/L18PLT", strict=True)
21
22build_qc_table(survey, api=False).to_csv(
23 out / "station_qc_summary.csv",
24 index=False,
25)
26qc_flags(survey).to_csv(out / "station_qc_flags.csv", index=False)
27station_confidence_table(survey, method="composite", api=False).to_csv(
28 out / "station_confidence.csv",
29 index=False,
30)
31frequency_confidence_table(survey, method="composite", api=False).to_csv(
32 out / "frequency_confidence.csv",
33 index=False,
34)
35
36fig, ax = plt.subplots(figsize=(9.5, 4.2))
37plot_confidence_profile(survey, method="composite", ax=ax)
38fig.tight_layout()
39fig.savefig(out / "confidence_profile.png", dpi=200)
40plt.close(fig)
41
42fig, ax = plt.subplots(figsize=(10.0, 4.8))
43plot_frequency_confidence_psection(survey, method="composite", ax=ax)
44fig.savefig(out / "frequency_confidence_psection.png", dpi=200)
45plt.close(fig)
46
47fig, ax = plt.subplots(figsize=(8.5, 4.2))
48plot_confidence_band_summary(survey, method="composite", ax=ax)
49fig.savefig(out / "confidence_band_summary.png", dpi=200)
50plt.close(fig)
51
52fig = plot_qc_quicklook(survey)
53fig.savefig(out / "qc_quicklook.png", dpi=200)
54plt.close(fig)
Reading QC Results#
Use QC scores as evidence, not as an automatic delete button.
coverageis lowThe station or frequency is genuinely incomplete. Investigate loading, editing, or acquisition gaps.
uncertaintyis lowError tensors are large relative to impedance magnitude. This is a strong reason to down-weight or review.
offdiagis lowZxyandZyxamplitudes disagree beyond the selected tolerance. Compare with anisotropy, impedance, and tensor tools.diagonalis lowDiagonal leakage is high. Check dimensionality and coordinate orientation before calling it acquisition noise.
phaseis lowOff-diagonal phase changes abruptly with frequency. Check frequency editing and processing history.
spatialis lowThe station differs from neighbouring stations at the same frequency or in median response. Check station metadata and local geology.
Worked Example#
The gallery example applies the station, frequency, profile, dashboard, quicklook, fan, crossover, and hole-overlay workflows to the bundled L18PLT survey.
Open the rendered gallery page here: Quality-control confidence scoring (pycsamt.emtools.qc).