Source code for biopsykit.signals.pep._pep_extraction

import warnings
from typing import Literal

import pandas as pd
from typing_extensions import Self

__all__ = ["PepExtraction"]

from biopsykit.signals._base_extraction import HANDLE_MISSING_EVENTS, BaseExtraction, CanHandleMissingEventsMixin
from biopsykit.utils.dtypes import (
    BPointDataFrame,
    HeartbeatSegmentationDataFrame,
    QPeakDataFrame,
    is_b_point_dataframe,
    is_heartbeat_segmentation_dataframe,
    is_pep_result_dataframe,
    is_q_peak_dataframe,
)
from biopsykit.utils.exceptions import EventExtractionError

NEGATIVE_PEP_HANDLING = Literal["nan", "zero", "keep"]


[docs]class PepExtraction(BaseExtraction, CanHandleMissingEventsMixin): _action_methods = "extract" handle_negative_pep: NEGATIVE_PEP_HANDLING pep_results_: pd.DataFrame def __init__( self, handle_negative_pep: NEGATIVE_PEP_HANDLING = "nan", handle_missing_events: HANDLE_MISSING_EVENTS = "warn", ) -> None: self.handle_negative_pep = handle_negative_pep super().__init__(handle_missing_events=handle_missing_events)
[docs] def extract( self, *, heartbeats: HeartbeatSegmentationDataFrame, q_peak_samples: QPeakDataFrame, b_point_samples: BPointDataFrame, sampling_rate_hz: float, ) -> Self: """Compute PEP from Q-peak samples and B-point locations. Args: heartbeats: Heartbeat locations as DataFrame q_peak_samples: ECG signal as DataFrame b_point_samples: ICG signal as DataFrame sampling_rate_hz: Sampling rate of the signals in Hz Returns ------- self """ is_heartbeat_segmentation_dataframe(heartbeats) is_b_point_dataframe(b_point_samples) is_q_peak_dataframe(q_peak_samples) self._check_valid_missing_handling() self._check_valid_handle_pep() # do something pep_results = pd.DataFrame( index=heartbeats.index, columns=[ "heartbeat_start_sample", "heartbeat_end_sample", "r_peak_sample", "rr_interval_sample", "rr_interval_ms", "heart_rate_bpm", "q_peak_sample", "b_point_sample", "pep_sample", "pep_ms", "nan_reason", ], ) if heartbeats.empty: missing_str = "No heartbeats found, no PEP can be extracted!" self.pep_results_ = pep_results if self.handle_missing_events == "warn": warnings.warn(missing_str) elif self.handle_missing_events == "raise": raise EventExtractionError(missing_str) return self pep_results = pep_results.assign( heartbeat_start_time=heartbeats["start_time"], heartbeat_start_sample=pd.to_numeric(heartbeats["start_sample"]), heartbeat_end_sample=pd.to_numeric(heartbeats["end_sample"]), r_peak_sample=pd.to_numeric(heartbeats["r_peak_sample"]), rr_interval_sample=pd.to_numeric(heartbeats["rr_interval_sample"]), rr_interval_ms=pd.to_numeric(heartbeats["rr_interval_sample"] / sampling_rate_hz * 1000), heart_rate_bpm=pd.to_numeric(60 / (heartbeats["rr_interval_sample"] / sampling_rate_hz)), q_peak_sample=pd.to_numeric(q_peak_samples["q_peak_sample"]), b_point_sample=pd.to_numeric(b_point_samples["b_point_sample"]), pep_sample=pd.to_numeric(b_point_samples["b_point_sample"] - q_peak_samples["q_peak_sample"]), ) pep_results = pep_results.assign( pep_ms=pep_results["pep_sample"] / sampling_rate_hz * 1000, ) pep_results = self._add_invalid_pep_reason(pep_results, q_peak_samples, b_point_samples) pep_results = pep_results.astype( { "heartbeat_start_sample": "Int64", "heartbeat_end_sample": "Int64", "r_peak_sample": "Int64", "rr_interval_sample": "Int64", "rr_interval_ms": "Float64", "heart_rate_bpm": "Float64", "q_peak_sample": "Int64", "b_point_sample": "Int64", "pep_sample": "Int64", "pep_ms": "Float64", "nan_reason": "object", } ) is_pep_result_dataframe(pep_results) self.pep_results_ = pep_results return self
def _add_invalid_pep_reason( self, pep_results: pd.DataFrame, q_peaks: QPeakDataFrame, b_points: BPointDataFrame, ) -> pd.DataFrame: # extract nan_reason from q_peak_samples and add to pep_results pep_results = pep_results.assign(nan_reason=q_peaks["nan_reason"]) # TODO add option to store multiple nan_reasons in one column? # extract nan_reason from b_point_samples nan_reason_b_point = b_points["nan_reason"].loc[~b_points["nan_reason"].isna()] # add nan_reason to pep_results if not nan_reason_b_point.empty: pep_results.loc[nan_reason_b_point.index, "nan_reason"] = nan_reason_b_point neg_pep_idx = pep_results["pep_ms"] < 0 if self.handle_negative_pep == "zero": pep_results.loc[neg_pep_idx, ["pep_sample", "pep_ms"]] = 0 pep_results.loc[neg_pep_idx, "nan_reason"] = "negative_pep" elif self.handle_negative_pep == "nan": pep_results.loc[neg_pep_idx, ["pep_sample", "pep_ms"]] = pd.NA pep_results.loc[neg_pep_idx, "nan_reason"] = "negative_pep" return pep_results def _check_valid_handle_pep(self): if self.handle_negative_pep not in ["nan", "zero", "keep"]: raise ValueError(f"Invalid value for 'handle_negative_pep': {self.handle_negative_pep}")