deepecgkit 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. deepecgkit-0.3.0/PKG-INFO +150 -0
  2. deepecgkit-0.3.0/README.md +113 -0
  3. deepecgkit-0.3.0/deepecgkit/__init__.py +104 -0
  4. deepecgkit-0.3.0/deepecgkit/_batching.py +115 -0
  5. deepecgkit-0.3.0/deepecgkit/_model_loading.py +90 -0
  6. deepecgkit-0.3.0/deepecgkit/analysis/__init__.py +71 -0
  7. deepecgkit-0.3.0/deepecgkit/analysis/aggregation.py +431 -0
  8. deepecgkit-0.3.0/deepecgkit/analysis/latex.py +730 -0
  9. deepecgkit-0.3.0/deepecgkit/analysis/ranking.py +666 -0
  10. deepecgkit-0.3.0/deepecgkit/analysis/statistics.py +379 -0
  11. deepecgkit-0.3.0/deepecgkit/cli/__init__.py +106 -0
  12. deepecgkit-0.3.0/deepecgkit/cli/_common.py +598 -0
  13. deepecgkit-0.3.0/deepecgkit/cli/commands/__init__.py +6 -0
  14. deepecgkit-0.3.0/deepecgkit/cli/commands/analysis.py +426 -0
  15. deepecgkit-0.3.0/deepecgkit/cli/commands/evaluate.py +476 -0
  16. deepecgkit-0.3.0/deepecgkit/cli/commands/explain.py +681 -0
  17. deepecgkit-0.3.0/deepecgkit/cli/commands/extract.py +413 -0
  18. deepecgkit-0.3.0/deepecgkit/cli/commands/info.py +210 -0
  19. deepecgkit-0.3.0/deepecgkit/cli/commands/predict.py +328 -0
  20. deepecgkit-0.3.0/deepecgkit/cli/commands/probe.py +600 -0
  21. deepecgkit-0.3.0/deepecgkit/cli/commands/train.py +609 -0
  22. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/cli/config.py +2 -2
  23. deepecgkit-0.3.0/deepecgkit/cli/defaults.py +45 -0
  24. deepecgkit-0.3.0/deepecgkit/cli/logger.py +67 -0
  25. deepecgkit-0.3.0/deepecgkit/cli/main.py +323 -0
  26. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/cli/registry.py +16 -6
  27. deepecgkit-0.3.0/deepecgkit/cli/run_dir.py +219 -0
  28. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/datasets/__init__.py +18 -0
  29. deepecgkit-0.3.0/deepecgkit/datasets/af_classification.py +358 -0
  30. deepecgkit-0.3.0/deepecgkit/datasets/base.py +474 -0
  31. deepecgkit-0.3.0/deepecgkit/datasets/io.py +273 -0
  32. deepecgkit-0.3.0/deepecgkit/datasets/ltafdb.py +407 -0
  33. deepecgkit-0.3.0/deepecgkit/datasets/mitbih_afdb.py +226 -0
  34. deepecgkit-0.3.0/deepecgkit/datasets/modules.py +327 -0
  35. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/datasets/preprocessing.py +146 -15
  36. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/datasets/ptbxl.py +128 -104
  37. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/datasets/splitting.py +31 -11
  38. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/datasets/unified_af.py +159 -68
  39. deepecgkit-0.3.0/deepecgkit/evaluation/__init__.py +183 -0
  40. deepecgkit-0.3.0/deepecgkit/evaluation/calibration.py +420 -0
  41. deepecgkit-0.3.0/deepecgkit/evaluation/evaluator.py +731 -0
  42. deepecgkit-0.3.0/deepecgkit/evaluation/metrics.py +632 -0
  43. deepecgkit-0.3.0/deepecgkit/evaluation/report.py +390 -0
  44. deepecgkit-0.3.0/deepecgkit/evaluation/representation.py +973 -0
  45. deepecgkit-0.3.0/deepecgkit/evaluation/visualization.py +538 -0
  46. deepecgkit-0.3.0/deepecgkit/explainability/__init__.py +91 -0
  47. deepecgkit-0.3.0/deepecgkit/explainability/attribution.py +914 -0
  48. deepecgkit-0.3.0/deepecgkit/explainability/beats.py +293 -0
  49. deepecgkit-0.3.0/deepecgkit/explainability/constraints.py +154 -0
  50. deepecgkit-0.3.0/deepecgkit/explainability/visualization.py +209 -0
  51. deepecgkit-0.3.0/deepecgkit/features.py +439 -0
  52. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/__init__.py +28 -2
  53. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/af_classifier.py +52 -60
  54. deepecgkit-0.3.0/deepecgkit/models/base.py +68 -0
  55. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/convnext_v2_1d.py +3 -11
  56. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/crnn.py +11 -18
  57. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/deep_res_cnn.py +34 -5
  58. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/ecg_dualnet.py +11 -40
  59. deepecgkit-0.3.0/deepecgkit/models/factory.py +204 -0
  60. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/fcn_wang.py +9 -12
  61. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/gru_model.py +6 -9
  62. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/inception_time.py +2 -11
  63. deepecgkit-0.2.0/deepecgkit/models/kanres_wide_x.py → deepecgkit-0.3.0/deepecgkit/models/kanres_deep_x.py +10 -13
  64. deepecgkit-0.2.0/deepecgkit/models/kanres_x.py → deepecgkit-0.3.0/deepecgkit/models/kanres_wide_x.py +4 -52
  65. deepecgkit-0.3.0/deepecgkit/models/kanres_x.py +10 -0
  66. deepecgkit-0.3.0/deepecgkit/models/layers.py +74 -0
  67. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/lstm_model.py +10 -18
  68. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/mamba1d.py +5 -12
  69. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/medformer.py +14 -13
  70. deepecgkit-0.3.0/deepecgkit/models/mlp_wang.py +77 -0
  71. deepecgkit-0.3.0/deepecgkit/models/pretrained.py +238 -0
  72. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/resnet1d.py +10 -15
  73. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/resnet1d_wang.py +2 -10
  74. deepecgkit-0.3.0/deepecgkit/models/resnet_hannun.py +154 -0
  75. deepecgkit-0.3.0/deepecgkit/models/rocket.py +95 -0
  76. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/se_resnet1d.py +7 -12
  77. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/simple_cnn.py +4 -10
  78. deepecgkit-0.3.0/deepecgkit/models/spectrogram_cnn.py +103 -0
  79. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/tcn.py +2 -11
  80. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/transformer_ecg.py +4 -37
  81. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/xresnet1d.py +8 -12
  82. deepecgkit-0.3.0/deepecgkit/models/xresnet1d101_benchmark.py +62 -0
  83. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/xresnet1d_benchmark.py +19 -24
  84. deepecgkit-0.3.0/deepecgkit/py.typed +0 -0
  85. deepecgkit-0.3.0/deepecgkit/registry.py +185 -0
  86. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/training/train.py +223 -84
  87. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/utils/__init__.py +6 -5
  88. deepecgkit-0.3.0/deepecgkit/utils/checkpoints.py +46 -0
  89. deepecgkit-0.3.0/deepecgkit/utils/device.py +74 -0
  90. deepecgkit-0.3.0/deepecgkit/utils/download.py +512 -0
  91. deepecgkit-0.3.0/deepecgkit/utils/paths.py +26 -0
  92. deepecgkit-0.3.0/deepecgkit/utils/plotting.py +28 -0
  93. deepecgkit-0.3.0/deepecgkit/utils/provenance.py +129 -0
  94. deepecgkit-0.3.0/deepecgkit/utils/weights.py +148 -0
  95. deepecgkit-0.3.0/deepecgkit.egg-info/PKG-INFO +150 -0
  96. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit.egg-info/SOURCES.txt +54 -1
  97. deepecgkit-0.3.0/deepecgkit.egg-info/requires.txt +22 -0
  98. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/pyproject.toml +51 -44
  99. deepecgkit-0.3.0/tests/test_analysis_aggregation.py +269 -0
  100. deepecgkit-0.3.0/tests/test_analysis_latex.py +337 -0
  101. deepecgkit-0.3.0/tests/test_analysis_ranking.py +348 -0
  102. deepecgkit-0.3.0/tests/test_analysis_statistics.py +218 -0
  103. deepecgkit-0.3.0/tests/test_bugfixes.py +271 -0
  104. deepecgkit-0.3.0/tests/test_cli.py +2329 -0
  105. deepecgkit-0.3.0/tests/test_datasets.py +1659 -0
  106. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/tests/test_deepecgkit.py +125 -21
  107. deepecgkit-0.3.0/tests/test_device.py +118 -0
  108. deepecgkit-0.3.0/tests/test_docs.py +263 -0
  109. deepecgkit-0.3.0/tests/test_evaluation.py +1387 -0
  110. deepecgkit-0.3.0/tests/test_examples.py +251 -0
  111. deepecgkit-0.3.0/tests/test_explainability.py +967 -0
  112. deepecgkit-0.3.0/tests/test_features.py +576 -0
  113. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/tests/test_models.py +166 -8
  114. deepecgkit-0.3.0/tests/test_protocol_fixes.py +270 -0
  115. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/tests/test_registry.py +13 -3
  116. deepecgkit-0.3.0/tests/test_representation.py +569 -0
  117. deepecgkit-0.3.0/tests/test_run_experiments.py +381 -0
  118. deepecgkit-0.3.0/tests/test_state_dict_compat.py +65 -0
  119. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/tests/test_training.py +263 -16
  120. deepecgkit-0.3.0/tests/test_utils.py +712 -0
  121. deepecgkit-0.2.0/PKG-INFO +0 -103
  122. deepecgkit-0.2.0/README.md +0 -67
  123. deepecgkit-0.2.0/deepecgkit/__init__.py +0 -17
  124. deepecgkit-0.2.0/deepecgkit/cli/__init__.py +0 -61
  125. deepecgkit-0.2.0/deepecgkit/cli/commands/__init__.py +0 -16
  126. deepecgkit-0.2.0/deepecgkit/cli/commands/evaluate.py +0 -89
  127. deepecgkit-0.2.0/deepecgkit/cli/commands/info.py +0 -73
  128. deepecgkit-0.2.0/deepecgkit/cli/commands/predict.py +0 -108
  129. deepecgkit-0.2.0/deepecgkit/cli/commands/train.py +0 -279
  130. deepecgkit-0.2.0/deepecgkit/cli/evaluation.py +0 -251
  131. deepecgkit-0.2.0/deepecgkit/cli/logger.py +0 -26
  132. deepecgkit-0.2.0/deepecgkit/cli/main.py +0 -456
  133. deepecgkit-0.2.0/deepecgkit/datasets/af_classification.py +0 -301
  134. deepecgkit-0.2.0/deepecgkit/datasets/base.py +0 -146
  135. deepecgkit-0.2.0/deepecgkit/datasets/ltafdb.py +0 -363
  136. deepecgkit-0.2.0/deepecgkit/datasets/mitbih_afdb.py +0 -287
  137. deepecgkit-0.2.0/deepecgkit/datasets/modules.py +0 -223
  138. deepecgkit-0.2.0/deepecgkit/evaluation/__init__.py +0 -40
  139. deepecgkit-0.2.0/deepecgkit/evaluation/evaluator.py +0 -392
  140. deepecgkit-0.2.0/deepecgkit/evaluation/metrics.py +0 -164
  141. deepecgkit-0.2.0/deepecgkit/evaluation/visualization.py +0 -280
  142. deepecgkit-0.2.0/deepecgkit/registry.py +0 -97
  143. deepecgkit-0.2.0/deepecgkit/utils/download.py +0 -123
  144. deepecgkit-0.2.0/deepecgkit/utils/weights.py +0 -107
  145. deepecgkit-0.2.0/deepecgkit.egg-info/PKG-INFO +0 -103
  146. deepecgkit-0.2.0/deepecgkit.egg-info/requires.txt +0 -25
  147. deepecgkit-0.2.0/tests/test_cli.py +0 -757
  148. deepecgkit-0.2.0/tests/test_datasets.py +0 -636
  149. deepecgkit-0.2.0/tests/test_evaluation.py +0 -370
  150. deepecgkit-0.2.0/tests/test_utils.py +0 -212
  151. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/LICENSE +0 -0
  152. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/__main__.py +0 -0
  153. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/training/__init__.py +0 -0
  154. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit.egg-info/dependency_links.txt +0 -0
  155. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit.egg-info/entry_points.txt +0 -0
  156. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit.egg-info/top_level.txt +0 -0
  157. {deepecgkit-0.2.0 → deepecgkit-0.3.0}/setup.cfg +0 -0
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.4
2
+ Name: deepecgkit
3
+ Version: 0.3.0
4
+ Summary: A deep learning toolkit for ECG analysis
5
+ Author-email: Steven <steven@simula.no>
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Programming Language :: Python :: 3.10
8
+ Classifier: Programming Language :: Python :: 3.11
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Classifier: License :: OSI Approved :: Apache Software License
12
+ Classifier: Operating System :: OS Independent
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: numpy>=1.22.0
17
+ Requires-Dist: scipy>=1.8.0
18
+ Requires-Dist: torch>=2.1.0
19
+ Requires-Dist: pandas>=1.4.0
20
+ Requires-Dist: scikit-learn>=1.1.0
21
+ Requires-Dist: matplotlib>=3.5.0
22
+ Requires-Dist: wfdb>=4.0.0
23
+ Requires-Dist: seaborn>=0.11.2
24
+ Requires-Dist: tqdm>=4.60.0
25
+ Requires-Dist: pyyaml>=6.0.3
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8.3.5; extra == "dev"
28
+ Requires-Dist: pytest-cov>=3.0.0; extra == "dev"
29
+ Requires-Dist: ruff>=0.11.13; extra == "dev"
30
+ Requires-Dist: mypy>=1.10; extra == "dev"
31
+ Provides-Extra: docs
32
+ Requires-Dist: mkdocs>=1.6.0; extra == "docs"
33
+ Requires-Dist: mkdocs-material>=9.5.0; extra == "docs"
34
+ Requires-Dist: mkdocstrings[python]>=0.25.0; extra == "docs"
35
+ Requires-Dist: mkdocs-autorefs>=1.0.0; extra == "docs"
36
+ Dynamic: license-file
37
+
38
+ # DeepECG-Kit
39
+
40
+ A deep learning library for ECG analysis and arrhythmia classification. Provides a CLI for
41
+ training, evaluation, inference, feature extraction, and explainability across 26 model
42
+ architectures and 5 ECG datasets.
43
+
44
+ **Documentation:** https://stevenah.github.io/deepecg-kit/
45
+
46
+ ## Installation
47
+
48
+ ```bash
49
+ pip install deepecgkit
50
+ ```
51
+
52
+ For local development (Python ≥ 3.10):
53
+
54
+ ```bash
55
+ uv sync --group dev # blessed path
56
+ pip install -e ".[dev]" # pip alternative
57
+ ```
58
+
59
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow.
60
+
61
+ ## Usage
62
+
63
+ ```bash
64
+ deepecg train -m kanres -d af-classification # train (downloads the dataset, ~100 MB)
65
+ deepecg evaluate --checkpoint runs/<run> -d af-classification
66
+ deepecg predict --checkpoint runs/<run> --input ecg.npy --sampling-rate 300
67
+ deepecg extract-features --checkpoint runs/<run> -d af-classification -o features.npz
68
+ deepecg explain --checkpoint runs/<run> --input ecg.npy -o explanations/
69
+ deepecg list-models # all 26 architectures
70
+ deepecg list-datasets # sizes, signal shape, local status
71
+ deepecg info -m kanres
72
+ ```
73
+
74
+ Training writes a run directory `runs/{timestamp}-{model}-{dataset}-seed{seed}/` containing
75
+ checkpoints (top 3 by validation loss), training plots, test metrics
76
+ (`classification_metrics.csv`, confusion matrix, calibration curve), and run metadata, and ends
77
+ by printing the best checkpoint. Anywhere a checkpoint is expected, the run directory can be
78
+ passed instead and stands for that best checkpoint.
79
+
80
+ Checkpoints describe themselves: they record the architecture, the class names, and the
81
+ preprocessing the training data went through (sampling rate, segment length, normalisation).
82
+ That is why `--model` is optional above, why `deepecg evaluate` rebuilds the dataset the way
83
+ the model was trained, and why `deepecg predict` can take a raw recording: it is resampled,
84
+ segmented and normalised to match before it reaches the model.
85
+
86
+ Run `deepecg list-models` and `deepecg list-datasets` for the full catalogs — datasets cover
87
+ PhysioNet 2017 (`af-classification`), PTB-XL (`ptbxl`), MIT-BIH AFDB (`mitbih-afdb`),
88
+ Long-Term AFDB (`ltafdb`), and the combined `unified-af`. Each dataset is used at its own
89
+ sampling rate (300 Hz for the AF datasets, 500 Hz for PTB-XL) unless `--sampling-rate` says
90
+ otherwise.
91
+
92
+ Datasets are downloaded on first use into `~/.deepecgkit/datasets`; set `DEEPECGKIT_HOME` to
93
+ keep them somewhere else. Downloads come from PhysioNet's open-data mirror, fall back to
94
+ physionet.org, and resume if interrupted.
95
+
96
+ ## Experiments
97
+
98
+ Experiment sweeps are defined in `experiments.yaml` and run via
99
+ `python scripts/run_experiments.py`. Each experiment trains every configured model on every
100
+ dataset across multiple seeds (default `seeds: [42, 43, 44, 45, 46]` — 5 runs per
101
+ model/dataset pair), saving to `runs/{model}-{dataset}-seed{seed}/`.
102
+
103
+ Aggregate and analyse finished runs with:
104
+
105
+ ```bash
106
+ deepecg aggregate --runs-dir runs --output-dir analysis --emit-latex
107
+ deepecg rank --runs-dir runs --metric f1
108
+ ```
109
+
110
+ ### Default Training Parameters
111
+
112
+ Defined once in `deepecgkit/cli/defaults.py`:
113
+
114
+ | Parameter | Value |
115
+ |-----------|-------|
116
+ | Epochs | 50 |
117
+ | Batch size | 32 |
118
+ | Learning rate | 0.001 |
119
+ | Optimizer | Adam |
120
+ | LR scheduler | ReduceLROnPlateau (factor=0.5, patience=5) |
121
+ | Early stopping patience | 10 |
122
+ | Validation split | 0.2 |
123
+ | Test split | 0.1 |
124
+ | Seed | 42 |
125
+ | Sampling rate | the dataset's own (override with `--sampling-rate`) |
126
+ | Data loader workers | 0 |
127
+
128
+ ## References
129
+
130
+ ### Datasets
131
+
132
+ 1. **af-classification** — Clifford GD, Liu C, Moody B, Li-wei HL, Silva I, Li Q, Johnson AE, Mark RG. "AF Classification from a Short Single Lead ECG Recording: The PhysioNet/Computing in Cardiology Challenge 2017." *Computing in Cardiology (CinC)*, 2017. [[PhysioNet]](https://physionet.org/content/challenge-2017/1.0.0/)
133
+
134
+ 2. **ptbxl** — Wagner P, Strodthoff N, Bousseljot RD, Kreiseler D, Lunze FI, Samek W, Schaeffter T. "PTB-XL, a Large Publicly Available Electrocardiography Dataset." *Scientific Data*, 7(1):154, 2020. [[PhysioNet]](https://physionet.org/content/ptb-xl/1.0.3/)
135
+
136
+ 3. **mitbih-afdb** — Moody GB, Mark RG. "A New Method for Detecting Atrial Fibrillation Using R-R Intervals." *Computers in Cardiology*, 10:227–230, 1983. [[PhysioNet]](https://physionet.org/content/afdb/1.0.0/)
137
+
138
+ 4. **ltafdb** — Petrutiu S, Sahakian AV, Swiryn S. "Abrupt Changes in Fibrillatory Wave Characteristics at the Termination of Paroxysmal Atrial Fibrillation in Humans." *Europace*, 9(7):466–470, 2007. [[PhysioNet]](https://physionet.org/content/ltafdb/1.0.0/)
139
+
140
+ 5. **unified-af** — Combined cross-dataset using af-classification, mitbih-afdb, and ltafdb (see references above).
141
+
142
+ ### Models
143
+
144
+ 6. **convnext-v2** — Woo S, Debnath S, Hu R, Chen X, Liu Z, Kweon IS, Xie S. "ConvNeXt V2: Co-designing and Scaling ConvNets with Masked Autoencoders." *CVPR*, 2023. [[GitHub]](https://github.com/facebookresearch/ConvNeXt-V2)
145
+
146
+ 7. **deep-res-cnn** — Elyamani H. "ECG Classification Using Deep Residual CNN." 2022. [[GitHub]](https://github.com/HaneenElyamani/ECG-classification)
147
+
148
+ 8. **inception-time** — Fawaz HI, Lucas B, Forestier G, Pelletier C, Schmidt DF, Weber J, Webb GI, Idoumghar L, Muller PA, Petitjean F. "InceptionTime: Finding AlexNet for Time Series Classification." *Data Mining and Knowledge Discovery*, 34:1936–1962, 2020. [[GitHub]](https://github.com/hfawaz/InceptionTime)
149
+
150
+ 9. **xresnet / xresnet1d-benchmark** — Strodthoff N, Wagner P, Schaeffter T, Samek W. "Deep Learning for ECG Analysis: Benchmarks and Insights from PTB-XL." *IEEE Journal of Biomedical and Health Informatics*, 25(5):1519–1528, 2021. [[GitHub]](https://github.com/helme/ecg_ptbxl_benchmarking)
@@ -0,0 +1,113 @@
1
+ # DeepECG-Kit
2
+
3
+ A deep learning library for ECG analysis and arrhythmia classification. Provides a CLI for
4
+ training, evaluation, inference, feature extraction, and explainability across 26 model
5
+ architectures and 5 ECG datasets.
6
+
7
+ **Documentation:** https://stevenah.github.io/deepecg-kit/
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install deepecgkit
13
+ ```
14
+
15
+ For local development (Python ≥ 3.10):
16
+
17
+ ```bash
18
+ uv sync --group dev # blessed path
19
+ pip install -e ".[dev]" # pip alternative
20
+ ```
21
+
22
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow.
23
+
24
+ ## Usage
25
+
26
+ ```bash
27
+ deepecg train -m kanres -d af-classification # train (downloads the dataset, ~100 MB)
28
+ deepecg evaluate --checkpoint runs/<run> -d af-classification
29
+ deepecg predict --checkpoint runs/<run> --input ecg.npy --sampling-rate 300
30
+ deepecg extract-features --checkpoint runs/<run> -d af-classification -o features.npz
31
+ deepecg explain --checkpoint runs/<run> --input ecg.npy -o explanations/
32
+ deepecg list-models # all 26 architectures
33
+ deepecg list-datasets # sizes, signal shape, local status
34
+ deepecg info -m kanres
35
+ ```
36
+
37
+ Training writes a run directory `runs/{timestamp}-{model}-{dataset}-seed{seed}/` containing
38
+ checkpoints (top 3 by validation loss), training plots, test metrics
39
+ (`classification_metrics.csv`, confusion matrix, calibration curve), and run metadata, and ends
40
+ by printing the best checkpoint. Anywhere a checkpoint is expected, the run directory can be
41
+ passed instead and stands for that best checkpoint.
42
+
43
+ Checkpoints describe themselves: they record the architecture, the class names, and the
44
+ preprocessing the training data went through (sampling rate, segment length, normalisation).
45
+ That is why `--model` is optional above, why `deepecg evaluate` rebuilds the dataset the way
46
+ the model was trained, and why `deepecg predict` can take a raw recording: it is resampled,
47
+ segmented and normalised to match before it reaches the model.
48
+
49
+ Run `deepecg list-models` and `deepecg list-datasets` for the full catalogs — datasets cover
50
+ PhysioNet 2017 (`af-classification`), PTB-XL (`ptbxl`), MIT-BIH AFDB (`mitbih-afdb`),
51
+ Long-Term AFDB (`ltafdb`), and the combined `unified-af`. Each dataset is used at its own
52
+ sampling rate (300 Hz for the AF datasets, 500 Hz for PTB-XL) unless `--sampling-rate` says
53
+ otherwise.
54
+
55
+ Datasets are downloaded on first use into `~/.deepecgkit/datasets`; set `DEEPECGKIT_HOME` to
56
+ keep them somewhere else. Downloads come from PhysioNet's open-data mirror, fall back to
57
+ physionet.org, and resume if interrupted.
58
+
59
+ ## Experiments
60
+
61
+ Experiment sweeps are defined in `experiments.yaml` and run via
62
+ `python scripts/run_experiments.py`. Each experiment trains every configured model on every
63
+ dataset across multiple seeds (default `seeds: [42, 43, 44, 45, 46]` — 5 runs per
64
+ model/dataset pair), saving to `runs/{model}-{dataset}-seed{seed}/`.
65
+
66
+ Aggregate and analyse finished runs with:
67
+
68
+ ```bash
69
+ deepecg aggregate --runs-dir runs --output-dir analysis --emit-latex
70
+ deepecg rank --runs-dir runs --metric f1
71
+ ```
72
+
73
+ ### Default Training Parameters
74
+
75
+ Defined once in `deepecgkit/cli/defaults.py`:
76
+
77
+ | Parameter | Value |
78
+ |-----------|-------|
79
+ | Epochs | 50 |
80
+ | Batch size | 32 |
81
+ | Learning rate | 0.001 |
82
+ | Optimizer | Adam |
83
+ | LR scheduler | ReduceLROnPlateau (factor=0.5, patience=5) |
84
+ | Early stopping patience | 10 |
85
+ | Validation split | 0.2 |
86
+ | Test split | 0.1 |
87
+ | Seed | 42 |
88
+ | Sampling rate | the dataset's own (override with `--sampling-rate`) |
89
+ | Data loader workers | 0 |
90
+
91
+ ## References
92
+
93
+ ### Datasets
94
+
95
+ 1. **af-classification** — Clifford GD, Liu C, Moody B, Li-wei HL, Silva I, Li Q, Johnson AE, Mark RG. "AF Classification from a Short Single Lead ECG Recording: The PhysioNet/Computing in Cardiology Challenge 2017." *Computing in Cardiology (CinC)*, 2017. [[PhysioNet]](https://physionet.org/content/challenge-2017/1.0.0/)
96
+
97
+ 2. **ptbxl** — Wagner P, Strodthoff N, Bousseljot RD, Kreiseler D, Lunze FI, Samek W, Schaeffter T. "PTB-XL, a Large Publicly Available Electrocardiography Dataset." *Scientific Data*, 7(1):154, 2020. [[PhysioNet]](https://physionet.org/content/ptb-xl/1.0.3/)
98
+
99
+ 3. **mitbih-afdb** — Moody GB, Mark RG. "A New Method for Detecting Atrial Fibrillation Using R-R Intervals." *Computers in Cardiology*, 10:227–230, 1983. [[PhysioNet]](https://physionet.org/content/afdb/1.0.0/)
100
+
101
+ 4. **ltafdb** — Petrutiu S, Sahakian AV, Swiryn S. "Abrupt Changes in Fibrillatory Wave Characteristics at the Termination of Paroxysmal Atrial Fibrillation in Humans." *Europace*, 9(7):466–470, 2007. [[PhysioNet]](https://physionet.org/content/ltafdb/1.0.0/)
102
+
103
+ 5. **unified-af** — Combined cross-dataset using af-classification, mitbih-afdb, and ltafdb (see references above).
104
+
105
+ ### Models
106
+
107
+ 6. **convnext-v2** — Woo S, Debnath S, Hu R, Chen X, Liu Z, Kweon IS, Xie S. "ConvNeXt V2: Co-designing and Scaling ConvNets with Masked Autoencoders." *CVPR*, 2023. [[GitHub]](https://github.com/facebookresearch/ConvNeXt-V2)
108
+
109
+ 7. **deep-res-cnn** — Elyamani H. "ECG Classification Using Deep Residual CNN." 2022. [[GitHub]](https://github.com/HaneenElyamani/ECG-classification)
110
+
111
+ 8. **inception-time** — Fawaz HI, Lucas B, Forestier G, Pelletier C, Schmidt DF, Weber J, Webb GI, Idoumghar L, Muller PA, Petitjean F. "InceptionTime: Finding AlexNet for Time Series Classification." *Data Mining and Knowledge Discovery*, 34:1936–1962, 2020. [[GitHub]](https://github.com/hfawaz/InceptionTime)
112
+
113
+ 9. **xresnet / xresnet1d-benchmark** — Strodthoff N, Wagner P, Schaeffter T, Samek W. "Deep Learning for ECG Analysis: Benchmarks and Insights from PTB-XL." *IEEE Journal of Biomedical and Health Informatics*, 25(5):1519–1528, 2021. [[GitHub]](https://github.com/helme/ecg_ptbxl_benchmarking)
@@ -0,0 +1,104 @@
1
+ """DeepECG-Kit: Deep learning library for ECG analysis and arrhythmia classification."""
2
+
3
+ import importlib
4
+ from importlib.metadata import PackageNotFoundError, version
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ if TYPE_CHECKING:
8
+ from deepecgkit.datasets import ECGDataModule
9
+ from deepecgkit.evaluation import ECGEvaluator
10
+ from deepecgkit.explainability import Attribution, ECGExplainer, explain
11
+ from deepecgkit.features import FeatureExtractor, FeatureSet, extract_features
12
+ from deepecgkit.models import KanResWideX
13
+ from deepecgkit.models.factory import build_model, build_model_from_checkpoint
14
+ from deepecgkit.registry import (
15
+ get_dataset_names,
16
+ get_model_names,
17
+ register_dataset,
18
+ register_model,
19
+ )
20
+ from deepecgkit.training import ECGTrainer
21
+ from deepecgkit.utils import read_csv
22
+ from deepecgkit.utils.device import resolve_device
23
+
24
+ try:
25
+ __version__ = version("deepecgkit")
26
+ except PackageNotFoundError:
27
+ __version__ = "0.0.0.dev0"
28
+
29
+ __author__ = "DeepECG Kit Contributors"
30
+ __license__ = "Apache-2.0"
31
+
32
+ __all__ = [
33
+ "Attribution",
34
+ "ECGDataModule",
35
+ "ECGEvaluator",
36
+ "ECGExplainer",
37
+ "ECGTrainer",
38
+ "FeatureExtractor",
39
+ "FeatureSet",
40
+ "KanResWideX",
41
+ "build_model",
42
+ "build_model_from_checkpoint",
43
+ "explain",
44
+ "extract_features",
45
+ "get_dataset_names",
46
+ "get_model_names",
47
+ "read_csv",
48
+ "register_dataset",
49
+ "register_model",
50
+ "resolve_device",
51
+ ]
52
+
53
+ #: Where each public name lives. A name is imported when it is first asked
54
+ #: for, so `import deepecgkit` and the commands that need little of the
55
+ #: package do not load PyTorch and the rest of the scientific stack.
56
+ _EXPORTS = {
57
+ "Attribution": "deepecgkit.explainability",
58
+ "ECGDataModule": "deepecgkit.datasets",
59
+ "ECGEvaluator": "deepecgkit.evaluation",
60
+ "ECGExplainer": "deepecgkit.explainability",
61
+ "ECGTrainer": "deepecgkit.training",
62
+ "FeatureExtractor": "deepecgkit.features",
63
+ "FeatureSet": "deepecgkit.features",
64
+ "KanResWideX": "deepecgkit.models",
65
+ "build_model": "deepecgkit.models.factory",
66
+ "build_model_from_checkpoint": "deepecgkit.models.factory",
67
+ "explain": "deepecgkit.explainability",
68
+ "extract_features": "deepecgkit.features",
69
+ "get_dataset_names": "deepecgkit.registry",
70
+ "get_model_names": "deepecgkit.registry",
71
+ "read_csv": "deepecgkit.utils",
72
+ "register_dataset": "deepecgkit.registry",
73
+ "register_model": "deepecgkit.registry",
74
+ "resolve_device": "deepecgkit.utils.device",
75
+ }
76
+ _SUBMODULES = frozenset(
77
+ {
78
+ "analysis",
79
+ "cli",
80
+ "datasets",
81
+ "evaluation",
82
+ "explainability",
83
+ "features",
84
+ "models",
85
+ "registry",
86
+ "training",
87
+ "utils",
88
+ }
89
+ )
90
+
91
+
92
+ def __getattr__(name: str) -> Any:
93
+ if name in _EXPORTS:
94
+ value = getattr(importlib.import_module(_EXPORTS[name]), name)
95
+ elif name in _SUBMODULES:
96
+ value = importlib.import_module(f"{__name__}.{name}")
97
+ else:
98
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
99
+ globals()[name] = value
100
+ return value
101
+
102
+
103
+ def __dir__() -> list[str]:
104
+ return sorted({*globals(), *__all__, *_SUBMODULES})
@@ -0,0 +1,115 @@
1
+ """Shared input normalisation for model wrappers (internal).
2
+
3
+ FeatureExtractor and ECGExplainer accept the same family of inputs — arrays,
4
+ tensors, recording files, Datasets, DataLoaders, and ECGDataModules — and both
5
+ need them as a non-shuffling DataLoader so row order stays joinable to external
6
+ metadata. The dispatch lives here once.
7
+ """
8
+
9
+ import warnings
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ import numpy as np
14
+ import torch
15
+ from torch.utils.data import DataLoader, Dataset, RandomSampler, TensorDataset
16
+
17
+ from deepecgkit.datasets.io import as_model_input, read_ecg_recording
18
+ from deepecgkit.datasets.modules import ECGDataModule
19
+
20
+ __all__ = ["as_inference_dataloader", "unpack_batch"]
21
+
22
+
23
+ def as_inference_dataloader(
24
+ data: Any,
25
+ *,
26
+ split: str,
27
+ batch_size: int,
28
+ num_workers: int,
29
+ input_channels: int | None,
30
+ verb: str = "processed",
31
+ ) -> DataLoader:
32
+ """Normalise any supported input to a non-shuffling DataLoader.
33
+
34
+ Args:
35
+ data: An array, tensor, file path, Dataset, DataLoader, or ECGDataModule
36
+ split: Split name, used only for an ECGDataModule; "all" selects the
37
+ full dataset
38
+ batch_size: Batch size for loaders constructed here
39
+ num_workers: Worker count for loaders constructed here
40
+ input_channels: Lead count used to shape raw arrays for the model
41
+ verb: Past-tense verb naming the operation ("extracted", "explained"),
42
+ used in the shuffling warning
43
+
44
+ Returns:
45
+ A loader yielding either signals alone or (signals, labels).
46
+
47
+ Raises:
48
+ TypeError: If the input type is not supported.
49
+ RuntimeError: If a data module split is unavailable.
50
+ """
51
+
52
+ def build_loader(dataset: Dataset) -> DataLoader:
53
+ return DataLoader(
54
+ dataset,
55
+ batch_size=batch_size,
56
+ shuffle=False,
57
+ num_workers=num_workers,
58
+ )
59
+
60
+ if isinstance(data, ECGDataModule):
61
+ # The module's own train_dataloader shuffles, which would scramble row
62
+ # order, so the loader is constructed here instead.
63
+ if data.dataset is None or data.train_dataset is None:
64
+ data.setup()
65
+ subset = data.dataset if split == "all" else getattr(data, f"{split}_dataset")
66
+ if subset is None:
67
+ raise RuntimeError(f"Data module has no '{split}' split available.")
68
+ return build_loader(subset)
69
+
70
+ if isinstance(data, DataLoader):
71
+ if isinstance(data.sampler, RandomSampler):
72
+ warnings.warn(
73
+ f"The supplied DataLoader shuffles, so {verb} rows will not follow "
74
+ "dataset order and cannot be joined to external metadata. Pass "
75
+ "shuffle=False for a reproducible ordering.",
76
+ RuntimeWarning,
77
+ stacklevel=4,
78
+ )
79
+ return data
80
+
81
+ if isinstance(data, Dataset):
82
+ return build_loader(data)
83
+
84
+ if isinstance(data, str | Path):
85
+ array = as_model_input(read_ecg_recording(data), input_channels)
86
+ return build_loader(TensorDataset(torch.as_tensor(array)))
87
+
88
+ if isinstance(data, torch.Tensor):
89
+ array = as_model_input(data.detach().cpu().numpy(), input_channels)
90
+ return build_loader(TensorDataset(torch.as_tensor(array)))
91
+
92
+ if isinstance(data, np.ndarray):
93
+ array = as_model_input(data, input_channels)
94
+ return build_loader(TensorDataset(torch.as_tensor(array)))
95
+
96
+ raise TypeError(
97
+ f"Unsupported input type: {type(data).__name__}. Pass an array, tensor, file path, "
98
+ f"Dataset, DataLoader, or ECGDataModule."
99
+ )
100
+
101
+
102
+ def unpack_batch(batch: Any) -> tuple[torch.Tensor, torch.Tensor | None]:
103
+ """Split a batch into signals and optional labels.
104
+
105
+ Args:
106
+ batch: A tensor, or a sequence whose first element is the signals
107
+
108
+ Returns:
109
+ Tuple of the signals and the labels, which are None when absent.
110
+ """
111
+ if isinstance(batch, list | tuple):
112
+ signals = batch[0]
113
+ labels = batch[1] if len(batch) > 1 else None
114
+ return signals, labels
115
+ return batch, None
@@ -0,0 +1,90 @@
1
+ """Shared model-loading machinery for wrapper classes (internal).
2
+
3
+ FeatureExtractor and ECGExplainer both wrap a model they can load from
4
+ registered pretrained weights, and both need to split caller kwargs between
5
+ their own constructor and the model's. That logic lives here once.
6
+ """
7
+
8
+ import inspect
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from torch import nn
13
+
14
+ from deepecgkit.models.factory import infer_input_channels
15
+ from deepecgkit.models.pretrained import from_pretrained, model_name_for_weights
16
+
17
+ __all__ = [
18
+ "load_pretrained_resolving_name",
19
+ "model_input_channels",
20
+ "split_wrapper_kwargs",
21
+ ]
22
+
23
+
24
+ def model_input_channels(model: nn.Module) -> int | None:
25
+ """Lead count declared by the model, or recovered from its weights.
26
+
27
+ Args:
28
+ model: The wrapped model
29
+
30
+ Returns:
31
+ The lead count, or None when unrecoverable.
32
+ """
33
+ declared = getattr(model, "input_channels", None)
34
+ if isinstance(declared, int):
35
+ return declared
36
+ return infer_input_channels(model.state_dict())
37
+
38
+
39
+ def split_wrapper_kwargs(
40
+ wrapper_cls: type, kwargs: dict[str, Any]
41
+ ) -> tuple[dict[str, Any], dict[str, Any]]:
42
+ """Split kwargs between a wrapper class's constructor and the model's.
43
+
44
+ The wrapper's accepted names are read from its ``__init__`` signature, so
45
+ adding a constructor parameter never requires updating a hand-maintained
46
+ key list.
47
+
48
+ Args:
49
+ wrapper_cls: The wrapper class (FeatureExtractor, ECGExplainer, ...)
50
+ kwargs: Mixed keyword arguments from the caller
51
+
52
+ Returns:
53
+ Tuple of (wrapper kwargs, model kwargs).
54
+ """
55
+ accepted = set(inspect.signature(wrapper_cls.__init__).parameters) - {
56
+ "self",
57
+ "model",
58
+ "model_name",
59
+ }
60
+ wrapper_kwargs = {k: v for k, v in kwargs.items() if k in accepted}
61
+ model_kwargs = {k: v for k, v in kwargs.items() if k not in accepted}
62
+ return wrapper_kwargs, model_kwargs
63
+
64
+
65
+ def load_pretrained_resolving_name(
66
+ weights: str | Path,
67
+ model_name: str | None = None,
68
+ **model_kwargs: Any,
69
+ ) -> tuple[nn.Module, str]:
70
+ """Load pretrained weights, resolving the model name from the registry.
71
+
72
+ Args:
73
+ weights: Name of registered pretrained weights, or a path
74
+ model_name: Registered model name; inferred from the weight registry
75
+ when the weights are registered
76
+ **model_kwargs: Constructor arguments forwarded to the model
77
+
78
+ Returns:
79
+ Tuple of the loaded model and the resolved registry name.
80
+
81
+ Raises:
82
+ ValueError: If the model name is neither given nor inferable.
83
+ """
84
+ resolved = model_name or model_name_for_weights(str(weights))
85
+ if resolved is None:
86
+ raise ValueError(
87
+ f"Could not determine which model '{weights}' belongs to. Pass model_name explicitly."
88
+ )
89
+ model = from_pretrained(resolved, weights, **model_kwargs)
90
+ return model, resolved
@@ -0,0 +1,71 @@
1
+ """Post-hoc analysis of finished training runs.
2
+
3
+ This subpackage aggregates the metrics written by completed runs, summarises
4
+ them across random seeds, and tests whether cross-dataset ranking reversals
5
+ survive that variance. It is distinct from
6
+ :mod:`deepecgkit.evaluation`, which evaluates a live model.
7
+ """
8
+
9
+ from deepecgkit.analysis.aggregation import (
10
+ RunRecord,
11
+ aggregate_runs,
12
+ discover_runs,
13
+ read_run_metrics,
14
+ reconcile_averaging,
15
+ runs_to_frame,
16
+ )
17
+ from deepecgkit.analysis.latex import (
18
+ escape_latex,
19
+ flip_survival_table,
20
+ rank_distribution_table,
21
+ results_table,
22
+ tau_table,
23
+ transfer_matrix_table,
24
+ write_tables,
25
+ )
26
+ from deepecgkit.analysis.ranking import (
27
+ all_flip_tests,
28
+ evaluate_rank_claim,
29
+ rank_distribution,
30
+ rank_within_seed,
31
+ tau_summary,
32
+ )
33
+ from deepecgkit.analysis.statistics import (
34
+ benjamini_yekutieli_adjust,
35
+ holm_adjust,
36
+ is_bimodal,
37
+ kendall_tau,
38
+ min_attainable_kendall_p,
39
+ nadeau_bengio_factor,
40
+ paired_summary,
41
+ summarize_scores,
42
+ )
43
+
44
+ __all__ = [
45
+ "RunRecord",
46
+ "aggregate_runs",
47
+ "all_flip_tests",
48
+ "benjamini_yekutieli_adjust",
49
+ "discover_runs",
50
+ "escape_latex",
51
+ "evaluate_rank_claim",
52
+ "flip_survival_table",
53
+ "holm_adjust",
54
+ "is_bimodal",
55
+ "kendall_tau",
56
+ "min_attainable_kendall_p",
57
+ "nadeau_bengio_factor",
58
+ "paired_summary",
59
+ "rank_distribution",
60
+ "rank_distribution_table",
61
+ "rank_within_seed",
62
+ "read_run_metrics",
63
+ "reconcile_averaging",
64
+ "results_table",
65
+ "runs_to_frame",
66
+ "summarize_scores",
67
+ "tau_summary",
68
+ "tau_table",
69
+ "transfer_matrix_table",
70
+ "write_tables",
71
+ ]