deepecgkit 0.1.0__py3-none-any.whl
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.
- deepecgkit/__init__.py +42 -0
- deepecgkit/__main__.py +6 -0
- deepecgkit/cli/__init__.py +61 -0
- deepecgkit/cli/commands/__init__.py +16 -0
- deepecgkit/cli/commands/evaluate.py +83 -0
- deepecgkit/cli/commands/info.py +73 -0
- deepecgkit/cli/commands/predict.py +94 -0
- deepecgkit/cli/commands/train.py +331 -0
- deepecgkit/cli/config.py +23 -0
- deepecgkit/cli/evaluation.py +251 -0
- deepecgkit/cli/logger.py +26 -0
- deepecgkit/cli/main.py +429 -0
- deepecgkit/cli/registry.py +32 -0
- deepecgkit/datasets/__init__.py +33 -0
- deepecgkit/datasets/af_classification.py +301 -0
- deepecgkit/datasets/base.py +146 -0
- deepecgkit/datasets/ltafdb.py +363 -0
- deepecgkit/datasets/mitbih_afdb.py +287 -0
- deepecgkit/datasets/modules.py +222 -0
- deepecgkit/datasets/preprocessing.py +208 -0
- deepecgkit/datasets/ptbxl.py +567 -0
- deepecgkit/datasets/splitting.py +225 -0
- deepecgkit/datasets/unified_af.py +316 -0
- deepecgkit/evaluation/__init__.py +40 -0
- deepecgkit/evaluation/evaluator.py +398 -0
- deepecgkit/evaluation/metrics.py +164 -0
- deepecgkit/evaluation/visualization.py +285 -0
- deepecgkit/models/__init__.py +51 -0
- deepecgkit/models/af_classifier.py +177 -0
- deepecgkit/models/convnext_v2_1d.py +183 -0
- deepecgkit/models/crnn.py +117 -0
- deepecgkit/models/deep_res_cnn.py +215 -0
- deepecgkit/models/ecg_dualnet.py +230 -0
- deepecgkit/models/fcn_wang.py +88 -0
- deepecgkit/models/gru_model.py +99 -0
- deepecgkit/models/inception_time.py +197 -0
- deepecgkit/models/kanres_wide_x.py +150 -0
- deepecgkit/models/kanres_x.py +126 -0
- deepecgkit/models/lstm_model.py +96 -0
- deepecgkit/models/mamba1d.py +232 -0
- deepecgkit/models/medformer.py +229 -0
- deepecgkit/models/resnet1d.py +189 -0
- deepecgkit/models/resnet1d_wang.py +182 -0
- deepecgkit/models/se_resnet1d.py +232 -0
- deepecgkit/models/simple_cnn.py +82 -0
- deepecgkit/models/tcn.py +178 -0
- deepecgkit/models/transformer_ecg.py +127 -0
- deepecgkit/models/xresnet1d.py +265 -0
- deepecgkit/models/xresnet1d_benchmark.py +227 -0
- deepecgkit/registry.py +97 -0
- deepecgkit/training/__init__.py +9 -0
- deepecgkit/training/train.py +171 -0
- deepecgkit/utils/__init__.py +72 -0
- deepecgkit/utils/download.py +123 -0
- deepecgkit/utils/weights.py +107 -0
- deepecgkit-0.1.0.dist-info/METADATA +99 -0
- deepecgkit-0.1.0.dist-info/RECORD +61 -0
- deepecgkit-0.1.0.dist-info/WHEEL +5 -0
- deepecgkit-0.1.0.dist-info/entry_points.txt +2 -0
- deepecgkit-0.1.0.dist-info/licenses/LICENSE +201 -0
- deepecgkit-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import zipfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import ClassVar, Dict, List, Optional, Tuple, Union
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import scipy.io
|
|
10
|
+
import torch
|
|
11
|
+
from tqdm import tqdm
|
|
12
|
+
|
|
13
|
+
from deepecgkit.registry import register_dataset
|
|
14
|
+
|
|
15
|
+
from ..utils.download import download_file
|
|
16
|
+
from .base import BaseECGDataset
|
|
17
|
+
from .preprocessing import ECGSegmenter, ECGStandardizer, convert_to_tensor
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@register_dataset(
|
|
21
|
+
name="af-classification",
|
|
22
|
+
input_channels=1,
|
|
23
|
+
description="PhysioNet 2017 AF Classification (4 classes, single-lead)",
|
|
24
|
+
)
|
|
25
|
+
class AFClassificationDataset(BaseECGDataset):
|
|
26
|
+
"""PhysioNet/Computing in Cardiology Challenge 2017 AF Classification Dataset.
|
|
27
|
+
|
|
28
|
+
This dataset contains over 10,000 single-lead ECG recordings of 30-60 seconds duration
|
|
29
|
+
for atrial fibrillation (AF) classification. Each recording is labeled as one of four
|
|
30
|
+
categories: Normal (N), Atrial Fibrillation (A), Other rhythm (O), or Noisy (~).
|
|
31
|
+
|
|
32
|
+
The recordings are from AliveCor device and represent patient-initiated recordings.
|
|
33
|
+
|
|
34
|
+
Reference:
|
|
35
|
+
Clifford GD, Liu C, Moody B, Li-wei HL, Silva I, Li Q, Johnson AE, Mark RG.
|
|
36
|
+
AF classification from a short single lead ECG recording: The PhysioNet/computing
|
|
37
|
+
in cardiology challenge 2017. In 2017 Computing in Cardiology (CinC) 2017 Sep 24 (pp. 1-4). IEEE.
|
|
38
|
+
|
|
39
|
+
URL:
|
|
40
|
+
https://physionet.org/content/challenge-2017/1.0.0/
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
CLASS_LABELS: ClassVar[List[str]] = [
|
|
44
|
+
"Normal",
|
|
45
|
+
"AF",
|
|
46
|
+
"Other",
|
|
47
|
+
"Noisy",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
REFERENCE_FILE: ClassVar[str] = "REFERENCE-v3.csv"
|
|
51
|
+
|
|
52
|
+
LABEL_MAPPING: ClassVar[Dict[str, int]] = {
|
|
53
|
+
"N": 0,
|
|
54
|
+
"A": 1,
|
|
55
|
+
"O": 2,
|
|
56
|
+
"~": 3,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
LEADS: ClassVar[List[str]] = ["ECG"]
|
|
60
|
+
DOWNLOAD_URL: ClassVar[str] = "https://physionet.org/content/challenge-2017/get-zip/1.0.0/"
|
|
61
|
+
NATIVE_SAMPLING_RATE: ClassVar[int] = 300
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
data_dir: Optional[Union[str, Path]] = None,
|
|
66
|
+
sampling_rate: int = 300,
|
|
67
|
+
segment_duration_seconds: float = 10.0,
|
|
68
|
+
segment_overlap: float = 0.0,
|
|
69
|
+
normalization: str = "zscore",
|
|
70
|
+
subset: str = "training",
|
|
71
|
+
transform: Optional[callable] = None,
|
|
72
|
+
target_transform: Optional[callable] = None,
|
|
73
|
+
download: bool = False,
|
|
74
|
+
force_download: bool = False,
|
|
75
|
+
version: str = "1.0.0",
|
|
76
|
+
verbose: bool = True,
|
|
77
|
+
):
|
|
78
|
+
if subset not in ["training", "validation"]:
|
|
79
|
+
raise ValueError("subset must be either 'training' or 'validation'")
|
|
80
|
+
|
|
81
|
+
self.version = version
|
|
82
|
+
self.subset = subset
|
|
83
|
+
self.verbose = verbose
|
|
84
|
+
self.segment_duration_seconds = segment_duration_seconds
|
|
85
|
+
self.segment_overlap = segment_overlap
|
|
86
|
+
|
|
87
|
+
self.standardizer = ECGStandardizer(
|
|
88
|
+
target_sampling_rate=sampling_rate,
|
|
89
|
+
normalization=normalization,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
self.segmenter = ECGSegmenter(
|
|
93
|
+
segment_duration_seconds=segment_duration_seconds,
|
|
94
|
+
sampling_rate=sampling_rate,
|
|
95
|
+
overlap=segment_overlap,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
self.signals: List[np.ndarray] = []
|
|
99
|
+
self.labels: List[int] = []
|
|
100
|
+
self.record_names: List[str] = []
|
|
101
|
+
self.reference_data: Optional[pd.DataFrame] = None
|
|
102
|
+
|
|
103
|
+
super().__init__(
|
|
104
|
+
data_dir=data_dir,
|
|
105
|
+
sampling_rate=sampling_rate,
|
|
106
|
+
leads=self.LEADS,
|
|
107
|
+
transform=transform,
|
|
108
|
+
target_transform=target_transform,
|
|
109
|
+
download=download,
|
|
110
|
+
force_download=force_download,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def download(self):
|
|
114
|
+
"""Download the AF Classification dataset from PhysioNet as a single ZIP."""
|
|
115
|
+
if self.verbose:
|
|
116
|
+
print(f"Downloading PhysioNet Challenge 2017 dataset to {self.data_dir}")
|
|
117
|
+
print("Note: This is a large dataset (~1.4GB). Download may take a while.")
|
|
118
|
+
|
|
119
|
+
os.makedirs(self.data_dir, exist_ok=True)
|
|
120
|
+
|
|
121
|
+
zip_path = self.data_dir / "challenge-2017-1.0.0.zip"
|
|
122
|
+
download_file(
|
|
123
|
+
self.DOWNLOAD_URL,
|
|
124
|
+
zip_path,
|
|
125
|
+
desc="Downloading PhysioNet Challenge 2017",
|
|
126
|
+
max_retries=5,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if self.verbose:
|
|
130
|
+
print("Extracting dataset...")
|
|
131
|
+
|
|
132
|
+
with zipfile.ZipFile(zip_path, "r") as zf:
|
|
133
|
+
zf.extractall(self.data_dir)
|
|
134
|
+
|
|
135
|
+
self._flatten_nested_dir()
|
|
136
|
+
self._extract_inner_zips()
|
|
137
|
+
|
|
138
|
+
zip_path.unlink(missing_ok=True)
|
|
139
|
+
|
|
140
|
+
if self.verbose:
|
|
141
|
+
print("Download complete!")
|
|
142
|
+
|
|
143
|
+
def _flatten_nested_dir(self):
|
|
144
|
+
"""Flatten a nested extraction directory if present from a previous download."""
|
|
145
|
+
for child in self.data_dir.iterdir():
|
|
146
|
+
if not child.is_dir() or child.name in ("__MACOSX", "training2017", "sample2017"):
|
|
147
|
+
continue
|
|
148
|
+
if self.verbose:
|
|
149
|
+
print(f"Flattening nested directory {child.name}...")
|
|
150
|
+
for item in child.iterdir():
|
|
151
|
+
dest = self.data_dir / item.name
|
|
152
|
+
if dest.exists():
|
|
153
|
+
if dest.is_dir():
|
|
154
|
+
shutil.rmtree(dest)
|
|
155
|
+
else:
|
|
156
|
+
dest.unlink()
|
|
157
|
+
shutil.move(str(item), str(dest))
|
|
158
|
+
child.rmdir()
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
def _extract_inner_zips(self):
|
|
162
|
+
"""Extract inner zip files like training2017.zip."""
|
|
163
|
+
for zf_path in self.data_dir.glob("*.zip"):
|
|
164
|
+
folder_name = zf_path.stem
|
|
165
|
+
target_dir = self.data_dir / folder_name
|
|
166
|
+
if target_dir.exists():
|
|
167
|
+
continue
|
|
168
|
+
if self.verbose:
|
|
169
|
+
print(f"Extracting {zf_path.name}...")
|
|
170
|
+
with zipfile.ZipFile(zf_path, "r") as zf:
|
|
171
|
+
zf.extractall(self.data_dir)
|
|
172
|
+
zf_path.unlink()
|
|
173
|
+
|
|
174
|
+
def _load_data(self):
|
|
175
|
+
"""Load the AF Classification dataset into memory."""
|
|
176
|
+
if self.verbose:
|
|
177
|
+
print(f"Loading AF Classification dataset ({self.subset} subset)...")
|
|
178
|
+
|
|
179
|
+
self._flatten_nested_dir()
|
|
180
|
+
self._extract_inner_zips()
|
|
181
|
+
|
|
182
|
+
reference_path = self.data_dir / self.REFERENCE_FILE
|
|
183
|
+
if not reference_path.exists():
|
|
184
|
+
for ref_file in ["REFERENCE.csv", "REFERENCE-v2.csv", "REFERENCE-v1.csv"]:
|
|
185
|
+
alt_path = self.data_dir / ref_file
|
|
186
|
+
if alt_path.exists():
|
|
187
|
+
reference_path = alt_path
|
|
188
|
+
break
|
|
189
|
+
|
|
190
|
+
if not reference_path.exists():
|
|
191
|
+
raise FileNotFoundError(f"Reference file not found in {self.data_dir}")
|
|
192
|
+
|
|
193
|
+
self.reference_data = pd.read_csv(
|
|
194
|
+
reference_path, header=None, names=["record_name", "label"]
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
if self.subset == "training":
|
|
198
|
+
data_folder = self.data_dir / "training2017"
|
|
199
|
+
else:
|
|
200
|
+
data_folder = self.data_dir / "sample2017"
|
|
201
|
+
|
|
202
|
+
if not data_folder.exists():
|
|
203
|
+
raise FileNotFoundError(
|
|
204
|
+
f"Data folder {data_folder} not found. Please download the dataset first."
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
iterator = (
|
|
208
|
+
tqdm(self.reference_data.iterrows(), desc="Loading records")
|
|
209
|
+
if self.verbose
|
|
210
|
+
else self.reference_data.iterrows()
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
for _, row in iterator:
|
|
214
|
+
record_name = row["record_name"]
|
|
215
|
+
label = row["label"]
|
|
216
|
+
|
|
217
|
+
mat_file = data_folder / f"{record_name}.mat"
|
|
218
|
+
|
|
219
|
+
if mat_file.exists():
|
|
220
|
+
try:
|
|
221
|
+
mat_data = scipy.io.loadmat(str(mat_file))
|
|
222
|
+
|
|
223
|
+
if "val" in mat_data:
|
|
224
|
+
signal = mat_data["val"].flatten()
|
|
225
|
+
elif "ecg" in mat_data:
|
|
226
|
+
signal = mat_data["ecg"].flatten()
|
|
227
|
+
elif "data" in mat_data:
|
|
228
|
+
signal = mat_data["data"].flatten()
|
|
229
|
+
else:
|
|
230
|
+
signal_key = [k for k in mat_data.keys() if not k.startswith("__")][0]
|
|
231
|
+
signal = mat_data[signal_key].flatten()
|
|
232
|
+
|
|
233
|
+
if label not in self.LABEL_MAPPING:
|
|
234
|
+
if self.verbose:
|
|
235
|
+
print(f"Warning: Unknown label '{label}' for record {record_name}")
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
label_idx = self.LABEL_MAPPING[label]
|
|
239
|
+
|
|
240
|
+
signal = signal[np.newaxis, :]
|
|
241
|
+
resampled = self.standardizer.resample(signal, self.NATIVE_SAMPLING_RATE)
|
|
242
|
+
|
|
243
|
+
segments, _ = self.segmenter.segment(resampled)
|
|
244
|
+
if len(segments) == 0:
|
|
245
|
+
continue
|
|
246
|
+
|
|
247
|
+
for segment in segments:
|
|
248
|
+
normalized = self.standardizer.normalize(segment)
|
|
249
|
+
self.signals.append(normalized)
|
|
250
|
+
self.labels.append(label_idx)
|
|
251
|
+
self.record_names.append(record_name)
|
|
252
|
+
|
|
253
|
+
except Exception as e:
|
|
254
|
+
if self.verbose:
|
|
255
|
+
print(f"Error loading record {record_name}: {e}")
|
|
256
|
+
continue
|
|
257
|
+
|
|
258
|
+
if self.verbose:
|
|
259
|
+
print(f"Successfully loaded {len(self.signals)} segments from {self.subset} subset")
|
|
260
|
+
|
|
261
|
+
def __getitem__(self, idx: int) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
262
|
+
signal = self.signals[idx]
|
|
263
|
+
label = self.labels[idx]
|
|
264
|
+
|
|
265
|
+
signal = convert_to_tensor(signal, dtype=torch.float32)
|
|
266
|
+
label = torch.tensor(label, dtype=torch.long)
|
|
267
|
+
|
|
268
|
+
if self.transform is not None:
|
|
269
|
+
signal = self.transform(signal)
|
|
270
|
+
if self.target_transform is not None:
|
|
271
|
+
label = self.target_transform(label)
|
|
272
|
+
|
|
273
|
+
return signal, label
|
|
274
|
+
|
|
275
|
+
def __len__(self) -> int:
|
|
276
|
+
return len(self.labels)
|
|
277
|
+
|
|
278
|
+
@property
|
|
279
|
+
def num_classes(self) -> int:
|
|
280
|
+
"""Get the number of classes in the dataset."""
|
|
281
|
+
return len(self.CLASS_LABELS)
|
|
282
|
+
|
|
283
|
+
@property
|
|
284
|
+
def class_names(self) -> List[str]:
|
|
285
|
+
"""Get the names of the classes in the dataset."""
|
|
286
|
+
return self.CLASS_LABELS
|
|
287
|
+
|
|
288
|
+
def get_record_info(self, idx: int) -> Dict:
|
|
289
|
+
record_name = self.record_names[idx]
|
|
290
|
+
label_idx = self.labels[idx]
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
"record_name": record_name,
|
|
294
|
+
"label": label_idx,
|
|
295
|
+
"class_name": self.CLASS_LABELS[label_idx],
|
|
296
|
+
"signal_shape": self.signals[idx].shape,
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
def get_class_distribution(self) -> Dict[str, int]:
|
|
300
|
+
unique, counts = np.unique(self.labels, return_counts=True)
|
|
301
|
+
return {self.class_names[int(label)]: int(count) for label, count in zip(unique, counts)}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Dict, List, Optional, Tuple, Union
|
|
5
|
+
|
|
6
|
+
import torch
|
|
7
|
+
from torch.utils.data import Dataset
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseECGDataset(Dataset, ABC):
|
|
11
|
+
"""Base class for all ECG datasets in deepecg-kit.
|
|
12
|
+
|
|
13
|
+
This class defines the common interface and functionality for all ECG datasets.
|
|
14
|
+
Each specific dataset implementation should inherit from this class and implement
|
|
15
|
+
the required methods.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def get_default_data_dir(cls) -> Path:
|
|
20
|
+
"""Get the default data directory for this dataset.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Path to the default data directory
|
|
24
|
+
"""
|
|
25
|
+
return Path.home() / ".deepecgkit" / "datasets" / cls.__name__.lower()
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
data_dir: Optional[Union[str, Path]] = None,
|
|
30
|
+
sampling_rate: int = 500,
|
|
31
|
+
leads: Optional[List[str]] = None,
|
|
32
|
+
transform: Optional[callable] = None,
|
|
33
|
+
target_transform: Optional[callable] = None,
|
|
34
|
+
download: bool = False,
|
|
35
|
+
force_download: bool = False,
|
|
36
|
+
):
|
|
37
|
+
"""Initialize the base ECG dataset.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
data_dir: Directory where the dataset is stored or will be downloaded.
|
|
41
|
+
If None, uses the default data directory.
|
|
42
|
+
sampling_rate: Sampling rate of the ECG signals (Hz)
|
|
43
|
+
leads: List of leads to use (e.g., ['I', 'II', 'III'] for standard leads)
|
|
44
|
+
transform: Optional transform to be applied to the ECG signals
|
|
45
|
+
target_transform: Optional transform to be applied to the labels
|
|
46
|
+
download: Whether to download the dataset if it doesn't exist locally.
|
|
47
|
+
force_download: Whether to force re-download even if the dataset exists
|
|
48
|
+
"""
|
|
49
|
+
self.data_dir = Path(data_dir) if data_dir is not None else self.get_default_data_dir()
|
|
50
|
+
self.sampling_rate = sampling_rate
|
|
51
|
+
self.leads = leads
|
|
52
|
+
self.transform = transform
|
|
53
|
+
self.target_transform = target_transform
|
|
54
|
+
self.home_dir = Path.home()
|
|
55
|
+
|
|
56
|
+
if force_download:
|
|
57
|
+
self._clear_dataset()
|
|
58
|
+
self.download()
|
|
59
|
+
elif not self.data_dir.exists():
|
|
60
|
+
if download:
|
|
61
|
+
self.download()
|
|
62
|
+
|
|
63
|
+
self._load_data()
|
|
64
|
+
|
|
65
|
+
def _clear_dataset(self):
|
|
66
|
+
"""Clear the dataset directory for re-download."""
|
|
67
|
+
if self.data_dir.exists():
|
|
68
|
+
print(f"Clearing existing dataset at {self.data_dir}...")
|
|
69
|
+
shutil.rmtree(self.data_dir)
|
|
70
|
+
self.data_dir.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
|
|
72
|
+
@abstractmethod
|
|
73
|
+
def download(self):
|
|
74
|
+
"""Download the dataset if it doesn't exist."""
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
def _load_data(self):
|
|
79
|
+
"""Load the dataset into memory."""
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
@abstractmethod
|
|
83
|
+
def __getitem__(self, idx: int) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
84
|
+
"""Get a sample from the dataset.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
idx: Index of the sample to get
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Tuple of (ecg_signal, label) where:
|
|
91
|
+
- ecg_signal: Tensor of shape (num_leads, signal_length)
|
|
92
|
+
- label: Tensor of shape (num_classes,) for classification or (num_beats,) for segmentation
|
|
93
|
+
"""
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
@abstractmethod
|
|
97
|
+
def __len__(self) -> int:
|
|
98
|
+
"""Get the number of samples in the dataset."""
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
@abstractmethod
|
|
103
|
+
def num_classes(self) -> int:
|
|
104
|
+
"""Get the number of classes in the dataset."""
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
@abstractmethod
|
|
109
|
+
def class_names(self) -> List[str]:
|
|
110
|
+
"""Get the names of the classes in the dataset."""
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
def get_class_distribution(self) -> Dict[str, int]:
|
|
114
|
+
"""Get the distribution of classes in the dataset."""
|
|
115
|
+
return {}
|
|
116
|
+
|
|
117
|
+
def _print_class_distribution(self):
|
|
118
|
+
"""Print class distribution statistics."""
|
|
119
|
+
distribution = self.get_class_distribution()
|
|
120
|
+
if not distribution:
|
|
121
|
+
return
|
|
122
|
+
print("\nClass distribution:")
|
|
123
|
+
for class_name, count in distribution.items():
|
|
124
|
+
print(f" {class_name}: {count}")
|
|
125
|
+
|
|
126
|
+
def get_metadata(self) -> Dict:
|
|
127
|
+
"""Get metadata about the dataset.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Dictionary containing metadata such as:
|
|
131
|
+
- sampling_rate: Sampling rate of the signals
|
|
132
|
+
- num_leads: Number of leads
|
|
133
|
+
- lead_names: Names of the leads
|
|
134
|
+
- num_classes: Number of classes
|
|
135
|
+
- class_names: Names of the classes
|
|
136
|
+
- dataset_size: Number of samples
|
|
137
|
+
- signal_length: Length of each signal
|
|
138
|
+
"""
|
|
139
|
+
return {
|
|
140
|
+
"sampling_rate": self.sampling_rate,
|
|
141
|
+
"num_leads": len(self.leads) if self.leads else None,
|
|
142
|
+
"lead_names": self.leads,
|
|
143
|
+
"num_classes": self.num_classes,
|
|
144
|
+
"class_names": self.class_names,
|
|
145
|
+
"dataset_size": len(self),
|
|
146
|
+
}
|