Scikit-learn
human-activity-recognition
wearable
wrist
time-series
cpu
scikit-learn
WISP / src /wisp /core /data.py
Zipeng365's picture
Add files using upload-large-folder tool
80b01cc verified
Raw History Blame Contribute Delete
6.08 kB
# Paper-scoped implementation; see SOURCE_PROVENANCE.json.
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Literal
import numpy as np
from numpy.typing import NDArray
from sklearn.model_selection import GroupShuffleSplit, StratifiedShuffleSplit, train_test_split
from sklearn.preprocessing import LabelEncoder
@dataclass
class HARData:
X: NDArray[np.float64]
y: NDArray[np.int64]
subject: NDArray[Any] | None = None
dataset: NDArray[Any] | None = None
device: NDArray[Any] | None = None
time_index: NDArray[Any] | None = None
label_names: list[str] | None = None
source: str = 'unknown'
sampling_rate_hz: float | None = None
metadata: dict[str, Any] | None = None
def __post_init__(self) -> None:
X = np.asarray(self.X, dtype=np.float64)
if X.ndim == 2:
X = X[:, :, None]
if X.ndim != 3:
raise ValueError(f'X must have shape (n,T,C) or (n,T); got {X.shape}')
y = np.asarray(self.y)
if y.ndim != 1 or y.shape[0] != X.shape[0]:
raise ValueError('y must be 1D and have same n as X')
self.X = X
self.y = y.astype(np.int64, copy=False)
n = X.shape[0]
for name in ('subject', 'dataset', 'device', 'time_index'):
val = getattr(self, name)
if val is not None and len(val) != n:
raise ValueError(f'{name} must have length n={n}')
@property
def n_classes(self) -> int:
if self.label_names is not None:
return int(len(self.label_names))
return int(np.max(self.y)) + 1 if self.y.size else 0
@property
def shape(self) -> tuple[int, int, int]:
return tuple((int(v) for v in self.X.shape))
def subset(self, idx: NDArray[np.int64] | list[int]) -> 'HARData':
idx_arr = np.asarray(idx, dtype=np.int64)
return HARData(X=self.X[idx_arr], y=self.y[idx_arr], subject=None if self.subject is None else np.asarray(self.subject)[idx_arr], dataset=None if self.dataset is None else np.asarray(self.dataset)[idx_arr], device=None if self.device is None else np.asarray(self.device)[idx_arr], time_index=None if self.time_index is None else np.asarray(self.time_index)[idx_arr], label_names=self.label_names, source=self.source, sampling_rate_hz=self.sampling_rate_hz, metadata=None if self.metadata is None else dict(self.metadata))
def _label_encode(y: NDArray[Any]) -> tuple[NDArray[np.int64], list[str]]:
if np.issubdtype(np.asarray(y).dtype, np.integer):
y_int = np.asarray(y, dtype=np.int64)
classes = [str(c) for c in sorted(np.unique(y_int).tolist())]
le = LabelEncoder().fit(y_int)
return (le.transform(y_int).astype(np.int64), [str(c) for c in le.classes_.tolist()])
le = LabelEncoder().fit(np.asarray(y).astype(str))
return (le.transform(np.asarray(y).astype(str)).astype(np.int64), [str(c) for c in le.classes_.tolist()])
def load_dataset(data: str | Path, *, synthetic_seed: int=42) -> HARData:
path = Path(data)
if not path.exists():
raise FileNotFoundError(path)
if path.suffix.lower() != '.npz':
raise ValueError('Only .npz is supported by the native loader. Export WristHARBench to npz first.')
obj = np.load(path, allow_pickle=True)
if 'X' not in obj or 'y' not in obj:
raise ValueError('NPZ file must contain keys X and y')
raw_y = np.asarray(obj['y'])
if 'label_names' in obj and np.issubdtype(raw_y.dtype, np.integer):
y = raw_y.astype(np.int64, copy=False)
names = [str(x) for x in np.asarray(obj['label_names'], dtype=object).tolist()]
if y.size and (int(np.min(y)) < 0 or int(np.max(y)) >= len(names)):
raise ValueError('Integer y is incompatible with the supplied label_names vocabulary')
else:
y, names = _label_encode(raw_y)
def opt(key: str) -> NDArray[Any] | None:
return obj[key] if key in obj else None
return HARData(X=np.asarray(obj['X'], dtype=np.float64), y=y, subject=opt('subject'), dataset=opt('dataset'), device=opt('device'), time_index=opt('time_index'), label_names=names, source=str(path), sampling_rate_hz=float(np.asarray(obj['sampling_rate_hz']).reshape(-1)[0]) if 'sampling_rate_hz' in obj else None, metadata=dict(np.asarray(obj['metadata'], dtype=object).reshape(-1)[0]) if 'metadata' in obj else None)
def concatenate_har(*parts: HARData, source: str | None=None) -> HARData:
if not parts:
raise ValueError('At least one HARData object is required')
shape_tail = parts[0].X.shape[1:]
n_classes = parts[0].n_classes
label_names = parts[0].label_names
for part in parts[1:]:
if part.X.shape[1:] != shape_tail:
raise ValueError(f'Incompatible time/channel shape {part.X.shape[1:]} != {shape_tail}')
if part.n_classes != n_classes:
raise ValueError(f'Incompatible class vocabulary {part.n_classes} != {n_classes}')
if label_names is not None and part.label_names is not None and (part.label_names != label_names):
raise ValueError('label_names differ across HARData objects')
def cat_optional(name: str) -> NDArray[Any] | None:
vals = [getattr(part, name) for part in parts]
if all((v is None for v in vals)):
return None
if any((v is None for v in vals)):
raise ValueError(f'Cannot concatenate partially missing {name}')
return np.concatenate([np.asarray(v) for v in vals], axis=0)
metadata = dict(parts[0].metadata or {})
metadata['concatenated_sources'] = [part.source for part in parts]
return HARData(X=np.concatenate([part.X for part in parts], axis=0), y=np.concatenate([part.y for part in parts], axis=0), subject=cat_optional('subject'), dataset=cat_optional('dataset'), device=cat_optional('device'), time_index=cat_optional('time_index'), label_names=label_names, source=source or '+'.join((part.source for part in parts)), sampling_rate_hz=parts[0].sampling_rate_hz, metadata=metadata)