deepecgkit.explainability¶
Attribution for trained ECG models. Each method assigns every sample of every lead a contribution to one output class, and the surrounding helpers reduce that map to a distribution over leads and to a beat-aligned profile with its P, QRS and T shares.
The package re-exports every symbol below, so from deepecgkit.explainability import ECGExplainer
works whichever submodule a name is defined in. The plotting helpers are imported on first use, so
importing the package does not pull matplotlib into the path of every command.
Explainer¶
explain
¶
explain(
data: ExplainInput,
model: Module | None = None,
checkpoint: str | Path | None = None,
weights: str | Path | None = None,
model_name: str | None = None,
input_channels: int | None = None,
output_size: int | None = None,
target: TargetSpec = None,
split: str = "test",
**kwargs: Any,
) -> Attribution
Attribute a model's predictions in one call.
Exactly one weight source must be given: an already-built model, a checkpoint path, or pretrained weights.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
ExplainInput
|
An array, tensor, file path, Dataset, DataLoader, or ECGDataModule |
required |
model
|
Module | None
|
An already-built model |
None
|
checkpoint
|
str | Path | None
|
Path to a checkpoint |
None
|
weights
|
str | Path | None
|
Registered pretrained weight name, or a path to a weights file |
None
|
model_name
|
str | None
|
Registered model name; optional for a checkpoint that records its own |
None
|
input_channels
|
int | None
|
Lead count, when it cannot be recovered from the weights |
None
|
output_size
|
int | None
|
Classifier width, when it cannot be recovered |
None
|
target
|
TargetSpec
|
None for each sample's own predicted class, an integer, or one index per sample |
None
|
split
|
str
|
Which split to use when data is an ECGDataModule |
'test'
|
**kwargs
|
Any
|
Forwarded to ECGExplainer |
{}
|
Returns:
| Type | Description |
|---|---|
Attribution
|
The attribution, with its lead distribution and beat profile. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If not exactly one weight source is given, or a checkpoint neither records its architecture nor is given a model name. |
Example
result = explain(signals, checkpoint="best.ckpt", model_name="kanres") result.pqrst_fractions["P"] 0.31
Source code in deepecgkit/explainability/attribution.py
ECGExplainer
¶
Runs an attribution method over data and summarises it for ECG.
The explainer holds the same responsibilities as FeatureExtractor, with one
inversion: it must run with gradients enabled, including when a caller
invokes it from inside an evaluation loop that has already entered
torch.no_grad().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
Module
|
Any registered model |
required |
model_name
|
str | None
|
Registry name recorded on the results, inferred from the model when omitted |
None
|
method
|
str
|
One of METHODS |
'saliency'
|
batch_size
|
int
|
Recordings per attribution pass |
8
|
device
|
str
|
Device name, or auto to prefer CUDA then MPS then CPU |
'auto'
|
sampling_rate
|
float
|
Samples per second, used for beat alignment and plots |
500.0
|
lead_names
|
list[str] | None
|
Display names for the leads, defaulting to the standard twelve-lead order when twelve leads are present |
None
|
max_samples
|
int | None
|
Stop after this many recordings, or None for all of them |
None
|
ig_steps
|
int
|
Path length for integrated gradients |
64
|
occlusion_window
|
int
|
Span width in samples for occlusion |
100
|
occlusion_stride
|
int
|
Step between spans for occlusion |
50
|
align_beats
|
bool
|
Detect R-peaks and build the beat profile |
True
|
detect_lead
|
int | None
|
Lead index R-peaks are detected on, defaulting to the second lead when there is more than one |
None
|
progress
|
bool
|
Show a progress bar when tqdm is installed |
False
|
Example
explainer = ECGExplainer.from_checkpoint("best.ckpt", "kanres") result = explainer.explain(signals) result.lead_importance.shape (100, 12)
Source code in deepecgkit/explainability/attribution.py
524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 | |
input_channels
property
¶
Lead count recovered from the model, or None when unrecoverable.
from_checkpoint
classmethod
¶
from_checkpoint(
checkpoint: str | Path,
model_name: str | None = None,
input_channels: int | None = None,
output_size: int | None = None,
**kwargs: Any,
) -> ECGExplainer
Build an explainer from a saved checkpoint.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
checkpoint
|
str | Path
|
Path to an ECGTrainer checkpoint or a bare state dict |
required |
model_name
|
str | None
|
Registered model name matching the weights; optional when the checkpoint records it |
None
|
input_channels
|
int | None
|
Lead count, required for architectures whose weights do not encode it |
None
|
output_size
|
int | None
|
Classifier width, normally recovered from the weights |
None
|
**kwargs
|
Any
|
Forwarded to the constructor |
{}
|
Returns:
| Type | Description |
|---|---|
ECGExplainer
|
An explainer wrapping the loaded model. |
Source code in deepecgkit/explainability/attribution.py
from_pretrained
classmethod
¶
from_pretrained(
weights: str | Path,
model_name: str | None = None,
**kwargs: Any,
) -> ECGExplainer
Build an explainer from registered pretrained weights or a file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weights
|
str | Path
|
Name of registered pretrained weights, or a path |
required |
model_name
|
str | None
|
Registered model name, inferred from the weight registry when the weights are registered |
None
|
**kwargs
|
Any
|
Forwarded to the constructor, except that unrecognised arguments are passed to the model instead |
{}
|
Returns:
| Type | Description |
|---|---|
ECGExplainer
|
An explainer wrapping the loaded model. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the model name is neither given nor inferable. |
Source code in deepecgkit/explainability/attribution.py
explain
¶
Attribute a model's predictions over the data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
ExplainInput
|
An array, tensor, file path, Dataset, DataLoader, or ECGDataModule |
required |
target
|
TargetSpec
|
None for each sample's own predicted class, an integer for one class throughout, or one index per sample |
None
|
split
|
str
|
Which split to use when data is an ECGDataModule |
'test'
|
Returns:
| Type | Description |
|---|---|
Attribution
|
The attribution, with the lead distribution always present and the |
Attribution
|
beat profile present when align_beats is set and beats were found. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the split or method is unknown. |
Source code in deepecgkit/explainability/attribution.py
654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 | |
Attribution
dataclass
¶
Attribution for a batch of recordings, with its ECG-native summaries.
Attributes:
| Name | Type | Description |
|---|---|---|
values |
ndarray
|
Per-lead per-timestep attribution of shape (batch, leads, time) |
signal |
ndarray
|
The signal the model actually saw, of the same shape, kept so attribution and trace can never fall out of alignment |
lead_importance |
ndarray
|
Distribution over leads of shape (batch, leads), rows summing to one |
target |
ndarray
|
Class index explained for each sample, of shape (batch,) |
method |
str
|
Which of METHODS produced the values |
model_name |
str | None
|
Registry name of the model, for provenance |
sampling_rate |
float
|
Samples per second the time axis assumes |
crop_offset |
int
|
Index in the original recording where these values begin, non-zero when the signal was centre-cropped to fit the model |
lead_names |
list[str] | None
|
Display names aligned with the lead axis |
beat_profile |
ndarray | None
|
Mean attribution over R-peak-aligned beats, of shape (leads, window), or None when beat alignment was not run |
beat_times_ms |
ndarray | None
|
Time axis of the beat window in milliseconds relative to the R-peak, or None |
pqrst_fractions |
dict[str, float] | None
|
Share of beat-profile mass in each cardiac region, plus the baseline remainder, or None |
pqrst_enrichment |
dict[str, float] | None
|
Each region's share divided by its share of the beat window's duration, so one means chance, or None |
beat_count |
int
|
Number of beats averaged into the profile |
Source code in deepecgkit/explainability/attribution.py
395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 | |
mean_lead_importance
¶
Average the per-sample lead distribution over the batch.
Returns:
| Type | Description |
|---|---|
ndarray
|
Array of shape (leads,) summing to one. |
Source code in deepecgkit/explainability/attribution.py
summary
¶
Describe the attribution in JSON-serialisable form.
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
A dictionary holding the provenance, the mean lead distribution |
dict[str, Any]
|
keyed by lead name, and the cardiac-region breakdown. |
Source code in deepecgkit/explainability/attribution.py
save
¶
Write the attribution arrays to disk.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
Destination ending in .npz |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the suffix is not .npz. The arrays are per-lead and per-timestep, so the flat formats FeatureSet supports would lose the axis that makes them worth keeping. |
Source code in deepecgkit/explainability/attribution.py
Attribution methods¶
saliency
¶
saliency(
model: Module,
signals: Tensor,
target: TargetSpec = None,
absolute: bool = True,
) -> torch.Tensor
Attribute by the gradient of the target logit with respect to the input.
The cheapest of the three methods, at one forward and one backward pass, and the noisiest: the gradient measures sensitivity at a single point, so it reacts to sharp local structure such as the QRS complex whether or not the model's decision depends on it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
Module
|
Any registered model, already on the right device |
required |
signals
|
Tensor
|
Batch of shape (batch, leads, time) |
required |
target
|
TargetSpec
|
None for the model's own prediction, an integer, or one index per sample |
None
|
absolute
|
bool
|
Return gradient magnitude rather than signed gradient |
True
|
Returns:
| Type | Description |
|---|---|
Tensor
|
Attribution of shape (batch, leads, time), matching the input. |
Source code in deepecgkit/explainability/attribution.py
integrated_gradients
¶
integrated_gradients(
model: Module,
signals: Tensor,
target: TargetSpec = None,
steps: int = 64,
baseline: Tensor | None = None,
) -> torch.Tensor
Attribute by integrating gradients along a path from a baseline.
Gradients are sampled at step midpoints, which makes the Riemann sum symmetric and lets the result satisfy the completeness axiom closely: the attribution sums to the difference between the model's score on the signal and on the baseline. Attribution is signed, because that identity is the method's value and taking magnitudes would destroy it.
The default baseline of zeros is the sensible one for z-scored ECG, where zero is the per-lead mean, and a poor one for unnormalised millivolts.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
Module
|
Any registered model, already on the right device |
required |
signals
|
Tensor
|
Batch of shape (batch, leads, time) |
required |
target
|
TargetSpec
|
None for the model's own prediction, an integer, or one index per sample |
None
|
steps
|
int
|
Number of points along the path |
64
|
baseline
|
Tensor | None
|
Reference input of the same shape, defaulting to zeros |
None
|
Returns:
| Type | Description |
|---|---|
Tensor
|
Signed attribution of shape (batch, leads, time), matching the input. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If steps is not positive, or the baseline shape differs. |
Source code in deepecgkit/explainability/attribution.py
occlusion
¶
occlusion(
model: Module,
signals: Tensor,
target: TargetSpec = None,
window: int = 100,
stride: int = 50,
baseline: float = 0.0,
) -> torch.Tensor
Attribute by how much the score drops when a span is blanked out.
One lead-and-span at a time is replaced by the baseline and the drop in the target logit is spread over that span, averaging where windows overlap. The result is signed: a positive value means blanking the span lowered the score, so the span supported the prediction.
Attribution bleeds one window width past any sharp boundary in the model's receptive field, because a window straddling the boundary perturbs samples on both sides and the single resulting score change is smeared across all of it. Narrower windows sharpen the result and cost proportionally more forward passes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
Module
|
Any registered model, already on the right device |
required |
signals
|
Tensor
|
Batch of shape (batch, leads, time) |
required |
target
|
TargetSpec
|
None for the model's own prediction, an integer, or one index per sample |
None
|
window
|
int
|
Width in samples of the occluded span |
100
|
stride
|
int
|
Step in samples between successive spans |
50
|
baseline
|
float
|
Value written into the occluded span |
0.0
|
Returns:
| Type | Description |
|---|---|
Tensor
|
Signed attribution of shape (batch, leads, time), matching the input. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If window or stride is not positive. |
Source code in deepecgkit/explainability/attribution.py
lead_importance
¶
Reduce per-timestep attribution to a distribution over leads.
Each lead is scored by its mean absolute attribution, then the scores are normalised to sum to one. Magnitude rather than sign, because the question is how much a lead mattered rather than which way it pushed; mean rather than sum, so the scores stay comparable if leads ever carry different lengths.
A single-lead model yields [1.0]. That is correct rather than
degenerate: with one lead there is nowhere else for the evidence to be.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
values
|
ndarray
|
Attribution of shape (batch, leads, time) |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Array of shape (batch, leads) whose rows sum to one. A row whose |
ndarray
|
attribution is entirely zero becomes uniform, since a model that reacted |
ndarray
|
to nothing gives no reason to prefer any lead. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the attribution is not three-dimensional. |
Source code in deepecgkit/explainability/attribution.py
Beat alignment¶
detect_r_peaks
¶
detect_r_peaks(
signal: ndarray,
sampling_rate: float = 500.0,
bandpass: tuple[float, float] = _QRS_BANDPASS_HZ,
min_rr_seconds: float = _MINIMUM_RR_SECONDS,
threshold_factor: float = _THRESHOLD_FACTOR,
integration_seconds: float = _INTEGRATION_SECONDS,
) -> np.ndarray
Locate R-peaks in a single-lead signal.
The pipeline is the classical Pan-Tompkins shape: band-pass to the QRS band, differentiate, square, integrate over a moving window, then take peaks above a threshold with a refractory distance between them. On synthetic ECG with known R positions this recovers every peak to the exact sample.
The band is a parameter rather than a constant because it encodes an assumption about where QRS energy lives. Real ECG puts it near 5 to 15 Hz, but synthetic test signals often do not, and a detector that silently returns the wrong beats is worse than one that is asked to be told.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
signal
|
ndarray
|
One-dimensional signal of shape (time,) |
required |
sampling_rate
|
float
|
Samples per second |
500.0
|
bandpass
|
tuple[float, float]
|
Low and high cutoff in Hz for the QRS band |
_QRS_BANDPASS_HZ
|
min_rr_seconds
|
float
|
Refractory period, the closest two peaks may be |
_MINIMUM_RR_SECONDS
|
threshold_factor
|
float
|
Peaks must exceed mean plus this many standard deviations of the integrated energy |
_THRESHOLD_FACTOR
|
integration_seconds
|
float
|
Width of the moving-average window |
_INTEGRATION_SECONDS
|
Returns:
| Type | Description |
|---|---|
ndarray
|
Sorted sample indices of the detected R-peaks, empty when the signal is |
ndarray
|
too short or carries no energy. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the signal is not one-dimensional, or the band is not a rising pair below the Nyquist frequency. |
Source code in deepecgkit/explainability/beats.py
beat_aligned_attribution
¶
beat_aligned_attribution(
values: ndarray,
r_peaks: ndarray,
sampling_rate: float = 500.0,
window_ms: tuple[int, int] = BEAT_WINDOW_MS,
) -> tuple[np.ndarray, np.ndarray, int]
Average attribution over R-peak-aligned windows.
Beats whose window would run past either end of the recording are dropped rather than padded or clipped, and the surviving count is returned so a caller can report it. A truncated beat averaged in alongside whole ones would pull the profile toward the edges of the recording, which is an artefact of where the recording happened to start.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
values
|
ndarray
|
Attribution of shape (leads, time) |
required |
r_peaks
|
ndarray
|
Sample indices of the R-peaks |
required |
sampling_rate
|
float
|
Samples per second |
500.0
|
window_ms
|
tuple[int, int]
|
Start and end of the beat window in milliseconds relative to the R-peak, negative before it |
BEAT_WINDOW_MS
|
Returns:
| Type | Description |
|---|---|
ndarray
|
Tuple of the mean profile of shape (leads, window), the window's time |
ndarray
|
axis in milliseconds, and the number of beats averaged. When no beat |
int
|
fits, the profile is all zeros and the count is zero. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the attribution is not two-dimensional, or the window does not start before it ends. |
Source code in deepecgkit/explainability/beats.py
pqrst_fractions
¶
pqrst_fractions(
profile: ndarray,
times_ms: ndarray,
windows_ms: dict[str, tuple[int, int]] | None = None,
) -> dict[str, float]
Split a beat profile into its P, QRS and T shares.
Shares are taken against the total mass of the profile, not against the mass inside the three windows, so they do not sum to one. The remainder is the baseline between the named regions, and hiding it by renormalising would make a model that attends mostly to the isoelectric line look like one that attends to the P wave.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
profile
|
ndarray
|
Mean beat profile of shape (leads, window) or (window,) |
required |
times_ms
|
ndarray
|
Time axis of the window in milliseconds relative to the R-peak |
required |
windows_ms
|
dict[str, tuple[int, int]] | None
|
Region boundaries in milliseconds, defaulting to PQRST_WINDOWS_MS |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, float]
|
Fraction of total profile mass falling in each named region, plus a |
dict[str, float]
|
"baseline" entry holding the remainder. All zeros when the profile |
dict[str, float]
|
carries no mass. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the time axis does not match the profile's last axis. |
Source code in deepecgkit/explainability/beats.py
pqrst_enrichment
¶
pqrst_enrichment(
fractions: dict[str, float],
times_ms: ndarray,
windows_ms: dict[str, tuple[int, int]] | None = None,
) -> dict[str, float]
Compare each region's attribution share against its share of the beat.
A value of one means the region received exactly the attention its width would earn by chance, above one means the model concentrates there, and below one means it avoids the region. This is the number the P-wave claim turns on: a P share of 0.09 sounds small until it is read against the P window's 0.25 share of the beat, at which point it says the model attends to the P-wave region at roughly a third of chance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fractions
|
dict[str, float]
|
Output of pqrst_fractions |
required |
times_ms
|
ndarray
|
Time axis of the beat window in milliseconds |
required |
windows_ms
|
dict[str, tuple[int, int]] | None
|
Region boundaries in milliseconds, defaulting to PQRST_WINDOWS_MS |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, float]
|
Ratio of attribution share to duration share for each region. A region |
dict[str, float]
|
occupying no samples yields 0.0 rather than an infinity. |
Source code in deepecgkit/explainability/beats.py
region_duration_fractions
¶
region_duration_fractions(
times_ms: ndarray,
windows_ms: dict[str, tuple[int, int]] | None = None,
) -> dict[str, float]
Return each region's share of the beat window's duration.
This is the profile a model spreading its attention evenly over the cardiac cycle would produce, and it is what makes a raw share interpretable. The P window is 200 ms of an 800 ms beat, so 25 per cent of the attribution falling in it is not evidence of anything.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
times_ms
|
ndarray
|
Time axis of the beat window in milliseconds relative to the R-peak |
required |
windows_ms
|
dict[str, tuple[int, int]] | None
|
Region boundaries in milliseconds, defaulting to PQRST_WINDOWS_MS |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, float]
|
Fraction of the window's samples falling in each named region, plus a |
dict[str, float]
|
"baseline" entry holding the remainder. |
Source code in deepecgkit/explainability/beats.py
Model constraints¶
length_bounds
¶
Return the smallest and largest signal length a model accepts.
A live min_input_length attribute wins over the table, because AFModel's
floor is derived from its recording_length and is 7971 at 30 seconds but
1393 at 6 or 10. The table cannot know which was built.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
Module
|
The model the attribution will run against |
required |
model_name
|
str | None
|
Registry name, inferred from the model when omitted |
None
|
Returns:
| Type | Description |
|---|---|
int
|
Tuple of the minimum length and the maximum length, where the maximum is |
int | None
|
None for the seventeen architectures that have no ceiling. |
Source code in deepecgkit/explainability/constraints.py
fit_to_constraints
¶
fit_to_constraints(
signals: Tensor,
model: Module,
model_name: str | None = None,
) -> tuple[torch.Tensor, int]
Centre-crop a batch so the model can consume it, and report the offset.
Cropping is centred to match ECGStandardizer(clip_method="center"), so a
recording trimmed here covers the same span it would have during training.
The returned offset lets a plot label the time axis against the original
recording rather than against the crop.
A signal that is too short cannot be fixed by cropping, so that case raises instead of padding: padding would feed the model fabricated samples and the resulting attribution would describe the padding as much as the ECG.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
signals
|
Tensor
|
Batch of shape (batch, leads, time) |
required |
model
|
Module
|
The model the attribution will run against |
required |
model_name
|
str | None
|
Registry name, inferred from the model when omitted |
None
|
Returns:
| Type | Description |
|---|---|
Tensor
|
Tuple of the possibly cropped batch and the index in the original signal |
int
|
where the returned batch begins. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the batch is not three-dimensional, if its lead count does not match the model, or if it is shorter than the model's minimum. |
Source code in deepecgkit/explainability/constraints.py
Visualisation¶
plot_attribution
¶
plot_attribution(
signal: ndarray,
values: ndarray,
sampling_rate: float = 500.0,
leads: list[str] | None = None,
time_offset: float = 0.0,
title: str = "ECG Attribution",
save_path: str | None = None,
unit: str | None = None,
) -> None
Plot each lead with its attribution shaded behind the trace.
Attribution magnitude is drawn as a background band on one colour scale shared by every lead, so a lead the model ignored looks empty next to one it relied on. Scaling each lead independently would render those two identically, which is the misreading the lead-importance plot exists to prevent.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
signal
|
ndarray
|
ECG of shape (leads, time) |
required |
values
|
ndarray
|
Attribution of the same shape |
required |
sampling_rate
|
float
|
Samples per second |
500.0
|
leads
|
list[str] | None
|
Display names, one per lead |
None
|
time_offset
|
float
|
Seconds into the original recording where this window starts, so a centre-cropped attribution still reads correctly |
0.0
|
title
|
str
|
Figure title |
'ECG Attribution'
|
save_path
|
str | None
|
Where to write the figure, or None to show it |
None
|
unit
|
str | None
|
Unit of the amplitude axis, such as "mV" for a raw recording. The signal a model saw is normalised and has none, so none is shown by default. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the signal is not two-dimensional or the shapes differ. |
Source code in deepecgkit/explainability/visualization.py
plot_lead_importance
¶
plot_lead_importance(
importance: ndarray,
leads: list[str] | None = None,
title: str = "Lead Importance",
save_path: str | None = None,
) -> None
Plot the share of attribution each lead carries.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
importance
|
ndarray
|
Distribution of shape (leads,), or (batch, leads) to be averaged over the batch |
required |
leads
|
list[str] | None
|
Display names, one per lead |
None
|
title
|
str
|
Figure title |
'Lead Importance'
|
save_path
|
str | None
|
Where to write the figure, or None to show it |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the distribution has more than two dimensions. |
Source code in deepecgkit/explainability/visualization.py
plot_beat_profile
¶
plot_beat_profile(
profile: ndarray,
times_ms: ndarray,
leads: list[str] | None = None,
windows_ms: dict[str, tuple[int, int]] | None = None,
title: str = "Beat-Aligned Attribution",
save_path: str | None = None,
) -> None
Plot mean attribution across the cardiac cycle with the regions shaded.
The P, QRS and T windows are drawn behind the curve so the reported shares can be checked against the shape they came from, rather than being taken on trust from a table.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
profile
|
ndarray
|
Mean beat profile of shape (leads, window) or (window,) |
required |
times_ms
|
ndarray
|
Time axis of the window in milliseconds relative to the R-peak |
required |
leads
|
list[str] | None
|
Display names, one per lead, used for the per-lead traces |
None
|
windows_ms
|
dict[str, tuple[int, int]] | None
|
Region boundaries in milliseconds, defaulting to PQRST_WINDOWS_MS |
None
|
title
|
str
|
Figure title |
'Beat-Aligned Attribution'
|
save_path
|
str | None
|
Where to write the figure, or None to show it |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the time axis does not match the profile window. |