From 916de1dda08cf08f4683aab2011d2884c725767f Mon Sep 17 00:00:00 2001 From: Erikpostt Date: Wed, 2 Oct 2024 15:16:55 +0200 Subject: [PATCH 01/10] Add I/O wrappers to high-level gait functions --- docs/notebooks/gait/gait_analysis.ipynb | 34 ++-- src/paradigma/gait_analysis.py | 155 +++++++++++------- src/paradigma/imu_preprocessing.py | 23 ++- .../gait/arm_swing_values.bin | Bin 272208 -> 272208 bytes .../gait/arm_swing_values.bin | Bin 64 -> 64 bytes tests/test_gait_analysis.py | 16 +- 6 files changed, 134 insertions(+), 94 deletions(-) diff --git a/docs/notebooks/gait/gait_analysis.ipynb b/docs/notebooks/gait/gait_analysis.ipynb index 7cd4880..6ec0b35 100644 --- a/docs/notebooks/gait/gait_analysis.ipynb +++ b/docs/notebooks/gait/gait_analysis.ipynb @@ -9,7 +9,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -19,14 +19,14 @@ "\n", "import os\n", "from paradigma.preprocessing_config import IMUPreprocessingConfig\n", - "from paradigma.gait_analysis import extract_gait_features, detect_gait, extract_arm_swing_features, detect_arm_swing, quantify_arm_swing\n", + "from paradigma.gait_analysis import extract_gait_features_io, detect_gait_io, extract_arm_swing_features_io, detect_arm_swing_io, quantify_arm_swing_io\n", "from paradigma.gait_analysis_config import GaitFeatureExtractionConfig, GaitDetectionConfig, ArmSwingFeatureExtractionConfig, ArmSwingDetectionConfig, ArmSwingQuantificationConfig\n", - "from paradigma.imu_preprocessing import preprocess_imu_data" + "from paradigma.imu_preprocessing import preprocess_imu_data_io" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 5, "metadata": { "tags": [ "parameters" @@ -54,12 +54,12 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "config = IMUPreprocessingConfig()\n", - "preprocess_imu_data(path_to_sensor_data, path_to_preprocessed_data, config)" + "preprocess_imu_data_io(path_to_sensor_data, path_to_preprocessed_data, config)" ] }, { @@ -71,13 +71,13 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "config = GaitFeatureExtractionConfig()\n", "#config.set_sampling_frequency(50)\n", - "extract_gait_features(path_to_preprocessed_data, path_to_extracted_features, config)" + "extract_gait_features_io(path_to_preprocessed_data, path_to_extracted_features, config)" ] }, { @@ -89,12 +89,12 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "config = GaitDetectionConfig()\n", - "detect_gait(path_to_extracted_features, path_to_predictions, path_to_classifier, config)" + "detect_gait_io(path_to_extracted_features, path_to_predictions, path_to_classifier, config)" ] }, { @@ -106,12 +106,12 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "config = ArmSwingFeatureExtractionConfig()\n", - "extract_arm_swing_features(path_to_preprocessed_data, path_to_extracted_features, config)" + "extract_arm_swing_features_io(path_to_preprocessed_data, path_to_extracted_features, config)" ] }, { @@ -123,12 +123,12 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ "config = ArmSwingDetectionConfig()\n", - "detect_arm_swing(path_to_extracted_features, path_to_predictions, path_to_classifier, config)" + "detect_arm_swing_io(path_to_extracted_features, path_to_predictions, path_to_classifier, config)" ] }, { @@ -140,12 +140,12 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 16, "metadata": {}, "outputs": [], "source": [ "config = ArmSwingQuantificationConfig()\n", - "quantify_arm_swing(path_to_extracted_features, path_to_predictions, path_to_quantification, config)" + "quantify_arm_swing_io(path_to_extracted_features, path_to_predictions, path_to_quantification, config)" ] } ], @@ -165,7 +165,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.13" + "version": "3.11.5" } }, "nbformat": 4, diff --git a/src/paradigma/gait_analysis.py b/src/paradigma/gait_analysis.py index 2652c71..5b4c819 100644 --- a/src/paradigma/gait_analysis.py +++ b/src/paradigma/gait_analysis.py @@ -3,6 +3,8 @@ import pandas as pd from pathlib import Path from typing import Union +from sklearn.linear_model import LogisticRegression +from sklearn.ensemble import RandomForestClassifier import tsdf @@ -18,11 +20,7 @@ from paradigma.util import get_end_iso8601, write_data, read_metadata -def extract_gait_features(input_path: Union[str, Path], output_path: Union[str, Path], config: GaitFeatureExtractionConfig) -> None: - # load data - metadata_time, metadata_samples = read_metadata(input_path, config.meta_filename, config.time_filename, config.values_filename) - df = tsdf.load_dataframe_from_binaries([metadata_time, metadata_samples], tsdf.constants.ConcatenationType.columns) - +def extract_gait_features(df: pd.DataFrame, config: GaitFeatureExtractionConfig) -> pd.DataFrame: # group sequences of timestamps into windows df_windowed = tabulate_windows( df=df, @@ -41,6 +39,18 @@ def extract_gait_features(input_path: Union[str, Path], output_path: Union[str, # and extract spectral features df_windowed = extract_spectral_domain_features(config, df_windowed, config.sensor, config.l_accelerometer_cols) + return df_windowed + + +def extract_gait_features_io(input_path: Union[str, Path], output_path: Union[str, Path], config: GaitFeatureExtractionConfig) -> None: + # Load data + metadata_time, metadata_samples = read_metadata(input_path, config.meta_filename, config.time_filename, config.values_filename) + df = tsdf.load_dataframe_from_binaries([metadata_time, metadata_samples], tsdf.constants.ConcatenationType.columns) + + # Extract gait features + df_windowed = extract_gait_features(df, config) + + # Store data end_iso8601 = get_end_iso8601(start_iso8601=metadata_time.start_iso8601, window_length_seconds=int(df_windowed[config.time_colname][-1:].values[0] + config.window_length_s)) @@ -58,12 +68,7 @@ def extract_gait_features(input_path: Union[str, Path], output_path: Union[str, write_data(metadata_time, metadata_samples, output_path, 'gait_meta.json', df_windowed) -def detect_gait(input_path: Union[str, Path], output_path: Union[str, Path], path_to_classifier_input: Union[str, Path], config: GaitDetectionConfig) -> None: - - # Load the data - metadata_time, metadata_samples = read_metadata(input_path, config.meta_filename, config.time_filename, config.values_filename) - df = tsdf.load_dataframe_from_binaries([metadata_time, metadata_samples], tsdf.constants.ConcatenationType.columns) - +def detect_gait(df: pd.DataFrame, config: GaitDetectionConfig, path_to_classifier_input: Union[str, Path]) -> pd.DataFrame: # Initialize the classifier clf = pd.read_pickle(os.path.join(path_to_classifier_input, config.classifier_file_name)) with open(os.path.join(path_to_classifier_input, config.thresholds_file_name), 'r') as f: @@ -80,7 +85,18 @@ def detect_gait(input_path: Union[str, Path], output_path: Union[str, Path], pat # Make prediction df['pred_gait_proba'] = clf.predict_proba(X)[:, 1] - df['pred_gait'] = df['pred_gait_proba'] > threshold + df['pred_gait'] = df['pred_gait_proba'] >= threshold + + return df + + +def detect_gait_io(input_path: Union[str, Path], output_path: Union[str, Path], path_to_classifier_input: Union[str, Path], config: GaitDetectionConfig) -> None: + + # Load the data + metadata_time, metadata_samples = read_metadata(input_path, config.meta_filename, config.time_filename, config.values_filename) + df = tsdf.load_dataframe_from_binaries([metadata_time, metadata_samples], tsdf.constants.ConcatenationType.columns) + + df = detect_gait(df, config, path_to_classifier_input) # Prepare the metadata metadata_samples.file_name = 'gait_values.bin' @@ -95,22 +111,7 @@ def detect_gait(input_path: Union[str, Path], output_path: Union[str, Path], pat write_data(metadata_time, metadata_samples, output_path, 'gait_meta.json', df) -def extract_arm_swing_features(input_path: Union[str, Path], output_path: Union[str, Path], config: ArmSwingFeatureExtractionConfig) -> None: - # load accelerometer and gyroscope data - l_dfs = [] - for sensor in ['accelerometer', 'gyroscope']: - config.set_sensor(sensor) - meta_filename = f'{sensor}_meta.json' - values_filename = f'{sensor}_samples.bin' - time_filename = f'{sensor}_time.bin' - - metadata_dict = tsdf.load_metadata_from_path(os.path.join(input_path, meta_filename)) - metadata_time = metadata_dict[time_filename] - metadata_samples = metadata_dict[values_filename] - l_dfs.append(tsdf.load_dataframe_from_binaries([metadata_time, metadata_samples], tsdf.constants.ConcatenationType.columns)) - - df = pd.merge(l_dfs[0], l_dfs[1], on=config.time_colname) - +def extract_arm_swing_features(df: pd.DataFrame, config: ArmSwingFeatureExtractionConfig) -> pd.DataFrame: # temporary add "random" predictions df[config.pred_gait_colname] = np.concatenate([np.repeat([1], df.shape[0]//3), np.repeat([0], df.shape[0]//3), np.repeat([1], df.shape[0] + 1 - 2*df.shape[0]//3)], axis=0) @@ -257,6 +258,27 @@ def extract_arm_swing_features(input_path: Union[str, Path], output_path: Union[ for sensor, l_sensor_colnames in zip(['accelerometer', 'gyroscope'], [config.l_accelerometer_cols, config.l_gyroscope_cols]): df_windowed = extract_spectral_domain_features(config, df_windowed, sensor, l_sensor_colnames) + return df_windowed + + +def extract_arm_swing_features_io(input_path: Union[str, Path], output_path: Union[str, Path], config: ArmSwingFeatureExtractionConfig) -> None: + # load accelerometer and gyroscope data + l_dfs = [] + for sensor in ['accelerometer', 'gyroscope']: + config.set_sensor(sensor) + meta_filename = f'{sensor}_meta.json' + values_filename = f'{sensor}_samples.bin' + time_filename = f'{sensor}_time.bin' + + metadata_dict = tsdf.load_metadata_from_path(os.path.join(input_path, meta_filename)) + metadata_time = metadata_dict[time_filename] + metadata_samples = metadata_dict[values_filename] + l_dfs.append(tsdf.load_dataframe_from_binaries([metadata_time, metadata_samples], tsdf.constants.ConcatenationType.columns)) + + df = pd.merge(l_dfs[0], l_dfs[1], on=config.time_colname) + + df_windowed = extract_arm_swing_features(df, config) + end_iso8601 = get_end_iso8601(metadata_samples.start_iso8601, df_windowed[config.time_colname][-1:].values[0] + config.window_length_s) @@ -274,13 +296,7 @@ def extract_arm_swing_features(input_path: Union[str, Path], output_path: Union[ write_data(metadata_time, metadata_samples, output_path, 'arm_swing_meta.json', df_windowed) -def detect_arm_swing(input_path: Union[str, Path], output_path: Union[str, Path], path_to_classifier_input: Union[str, Path], config: ArmSwingDetectionConfig) -> None: - # Load the data - metadata_time, metadata_samples = read_metadata(input_path, config.meta_filename, config.time_filename, config.values_filename) - df = tsdf.load_dataframe_from_binaries([metadata_time, metadata_samples], tsdf.constants.ConcatenationType.columns) - - # Initialize the classifier - clf = pd.read_pickle(os.path.join(path_to_classifier_input, config.classifier_file_name)) +def detect_arm_swing(df: pd.DataFrame, config: ArmSwingDetectionConfig, clf: Union[LogisticRegression, RandomForestClassifier]) -> pd.DataFrame: # Prepare the data clf.feature_names_in_ = ['std_norm_acc'] + [f'{x}_power_below_gait' for x in config.l_accelerometer_cols] + \ @@ -292,13 +308,23 @@ def detect_arm_swing(input_path: Union[str, Path], output_path: Union[str, Path] ['range_of_motion', 'forward_peak_ang_vel_mean', 'backward_peak_ang_vel_mean', 'forward_peak_ang_vel_std', 'backward_peak_ang_vel_std', 'angle_perc_power', 'angle_dominant_frequency'] + \ [f'{x}_dominant_frequency' for x in config.l_accelerometer_cols] - X = df.loc[:, clf.feature_names_in_] # Make prediction - # df['pred_arm_swing_proba'] = clf.predict_proba(X)[:, 1] df['pred_arm_swing'] = clf.predict(X) + return df + +def detect_arm_swing_io(input_path: Union[str, Path], output_path: Union[str, Path], path_to_classifier_input: Union[str, Path], config: ArmSwingDetectionConfig) -> None: + # Load the data + metadata_time, metadata_samples = read_metadata(input_path, config.meta_filename, config.time_filename, config.values_filename) + df = tsdf.load_dataframe_from_binaries([metadata_time, metadata_samples], tsdf.constants.ConcatenationType.columns) + + # Load the classifier + clf = pd.read_pickle(os.path.join(path_to_classifier_input, config.classifier_file_name)) + + df = detect_arm_swing(df, config, clf) + # Prepare the metadata metadata_samples.file_name = 'arm_swing_values.bin' metadata_time.file_name = 'arm_swing_time.bin' @@ -312,31 +338,7 @@ def detect_arm_swing(input_path: Union[str, Path], output_path: Union[str, Path] write_data(metadata_time, metadata_samples, output_path, 'arm_swing_meta.json', df) -def quantify_arm_swing(path_to_feature_input: Union[str, Path], path_to_prediction_input: Union[str, Path], output_path: Union[str, Path], config: ArmSwingQuantificationConfig) -> None: - # Load the features & predictions - metadata_time, metadata_samples = read_metadata(path_to_feature_input, config.meta_filename, config.time_filename, config.values_filename) - df_features = tsdf.load_dataframe_from_binaries([metadata_time, metadata_samples], tsdf.constants.ConcatenationType.columns) - - metadata_dict = tsdf.load_metadata_from_path(os.path.join(path_to_prediction_input, config.meta_filename)) - metadata_time = metadata_dict[config.time_filename] - metadata_samples = metadata_dict[config.values_filename] - df_predictions = tsdf.load_dataframe_from_binaries([metadata_time, metadata_samples], tsdf.constants.ConcatenationType.columns) - - # Validate - # dataframes have same length - assert df_features.shape[0] == df_predictions.shape[0] - - # dataframes have same time column - assert df_features['time'].equals(df_predictions['time']) - - # Prepare the data - - # subset features - l_feature_cols = ['time', 'range_of_motion', 'forward_peak_ang_vel_mean', 'backward_peak_ang_vel_mean'] - df_features = df_features[l_feature_cols] - - # concatenate features and predictions - df = pd.concat([df_features, df_predictions[config.pred_arm_swing_colname]], axis=1) +def quantify_arm_swing(df: pd.DataFrame, config: ArmSwingQuantificationConfig) -> pd.DataFrame: # temporarily for testing: manually determine predictions df[config.pred_arm_swing_colname] = np.concatenate([np.repeat([1], df.shape[0]//3), np.repeat([0], df.shape[0]//3), np.repeat([1], df.shape[0] - 2*df.shape[0]//3)], axis=0) @@ -379,6 +381,35 @@ def quantify_arm_swing(path_to_feature_input: Union[str, Path], path_to_predicti df_aggregates['segment_duration_ms'] = df_aggregates['segment_duration_s'] * 1000 df_aggregates = df_aggregates.drop(columns=['segment_nr']) + return df_aggregates + + +def quantify_arm_swing_io(path_to_feature_input: Union[str, Path], path_to_prediction_input: Union[str, Path], output_path: Union[str, Path], config: ArmSwingQuantificationConfig) -> None: + # Load the features & predictions + metadata_time, metadata_samples = read_metadata(path_to_feature_input, config.meta_filename, config.time_filename, config.values_filename) + df_features = tsdf.load_dataframe_from_binaries([metadata_time, metadata_samples], tsdf.constants.ConcatenationType.columns) + + metadata_dict = tsdf.load_metadata_from_path(os.path.join(path_to_prediction_input, config.meta_filename)) + metadata_time = metadata_dict[config.time_filename] + metadata_samples = metadata_dict[config.values_filename] + df_predictions = tsdf.load_dataframe_from_binaries([metadata_time, metadata_samples], tsdf.constants.ConcatenationType.columns) + + # Validate + # Dataframes have same length + assert df_features.shape[0] == df_predictions.shape[0] + + # Dataframes have same time column + assert df_features['time'].equals(df_predictions['time']) + + # Subset features + l_feature_cols = ['time', 'range_of_motion', 'forward_peak_ang_vel_mean', 'backward_peak_ang_vel_mean'] + df_features = df_features[l_feature_cols] + + # Concatenate features and predictions + df = pd.concat([df_features, df_predictions[config.pred_arm_swing_colname]], axis=1) + + df_aggregates = quantify_arm_swing(df, config) + # Store data metadata_samples.file_name = 'arm_swing_values.bin' metadata_time.file_name = 'arm_swing_time.bin' diff --git a/src/paradigma/imu_preprocessing.py b/src/paradigma/imu_preprocessing.py index 812da15..7006399 100644 --- a/src/paradigma/imu_preprocessing.py +++ b/src/paradigma/imu_preprocessing.py @@ -11,12 +11,7 @@ from paradigma.preprocessing_config import IMUPreprocessingConfig -def preprocess_imu_data(input_path: Union[str, Path], output_path: Union[str, Path], config: IMUPreprocessingConfig) -> None: - - # Load data - metadata_time, metadata_samples = read_metadata(str(input_path), str(config.meta_filename), - str(config.time_filename), str(config.values_filename)) - df = tsdf.load_dataframe_from_binaries([metadata_time, metadata_samples], tsdf.constants.ConcatenationType.columns) +def preprocess_imu_data(df: pd.DataFrame, config: IMUPreprocessingConfig, scale_factors: list) -> pd.DataFrame: # Rename columns df = df.rename(columns={f'rotation_{a}': f'gyroscope_{a}' for a in ['x', 'y', 'z']}) @@ -35,7 +30,7 @@ def preprocess_imu_data(input_path: Union[str, Path], output_path: Union[str, Pa time_column=config.time_colname, time_unit_type=TimeUnit.RELATIVE_MS, unscaled_column_names = list(config.d_channels_imu.keys()), - scale_factors=metadata_samples.scale_factors, + scale_factors=scale_factors, resampling_frequency=config.sampling_frequency) if config.side_watch == 'left': @@ -59,6 +54,19 @@ def preprocess_imu_data(input_path: Union[str, Path], output_path: Union[str, Pa df = df.drop(columns=[col]) df = df.rename(columns={f'filt_{col}': col}) + return df + + +def preprocess_imu_data_io(input_path: Union[str, Path], output_path: Union[str, Path], config: IMUPreprocessingConfig) -> None: + + # Load data + metadata_time, metadata_samples = read_metadata(str(input_path), str(config.meta_filename), + str(config.time_filename), str(config.values_filename)) + df = tsdf.load_dataframe_from_binaries([metadata_time, metadata_samples], tsdf.constants.ConcatenationType.columns) + + # Preprocess data + df = preprocess_imu_data(df=df, config=config, scale_factors=metadata_samples.scale_factors) + # Store data for sensor, units in zip(['accelerometer', 'gyroscope'], ['g', config.rotation_units]): df_sensor = df[[config.time_colname] + [x for x in df.columns if sensor in x]] @@ -72,6 +80,7 @@ def preprocess_imu_data(input_path: Union[str, Path], output_path: Union[str, Pa write_data(metadata_time, metadata_samples, output_path, f'{sensor}_meta.json', df_sensor) + def transform_time_array( time_array: pd.Series, scale_factor: float, diff --git a/tests/data/3.extracted_features/gait/arm_swing_values.bin b/tests/data/3.extracted_features/gait/arm_swing_values.bin index a0b08069c8079f3612893a16d7c47058699bcc2c..f72f4a263edbc6df722e5f120c65e51720fd7db4 100644 GIT binary patch delta 16005 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 N$wvO^t4;?){{s^+g>V1> delta 16005 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 H2^ji65ru`s diff --git a/tests/data/5.quantification/gait/arm_swing_values.bin b/tests/data/5.quantification/gait/arm_swing_values.bin index 8e6ecf96b890eb7db3e2b73406296ae5eea56b8c..997859bb279c3597c84d346223574eecbdb2dd4b 100644 GIT binary patch literal 64 zcmZQzfPnM&xP-nx&~Siq8SEDL3QMbaI{clP>$h^Dki)*UK6PK?3>;=Vma(WDP;oGE Lv9wSV^Kt+H5seZ- literal 64 zcmZQzfPk;}xP-nx&~Siq8SEDL3QMbaI{cfN>$h^Dki)jMK6PK?3>;D%%UDzns5q#) LSX!uwc{u<86;%=% diff --git a/tests/test_gait_analysis.py b/tests/test_gait_analysis.py index c1ae115..506fe13 100644 --- a/tests/test_gait_analysis.py +++ b/tests/test_gait_analysis.py @@ -1,8 +1,8 @@ from pathlib import Path -from paradigma.gait_analysis import detect_arm_swing, detect_gait, extract_arm_swing_features, extract_gait_features, quantify_arm_swing +from paradigma.gait_analysis import detect_arm_swing_io, detect_gait_io, extract_arm_swing_features_io, extract_gait_features_io, quantify_arm_swing_io from paradigma.gait_analysis_config import ArmSwingDetectionConfig, ArmSwingFeatureExtractionConfig, ArmSwingQuantificationConfig, GaitDetectionConfig, GaitFeatureExtractionConfig -from paradigma.imu_preprocessing import preprocess_imu_data +from paradigma.imu_preprocessing import preprocess_imu_data_io from paradigma.preprocessing_config import IMUPreprocessingConfig from test_notebooks import compare_data @@ -42,7 +42,7 @@ def test_1_imu_preprocessing_outputs(shared_datadir: Path): tested_output_path = reference_output_path / "test-output" config = IMUPreprocessingConfig() - preprocess_imu_data(input_path, tested_output_path, config) + preprocess_imu_data_io(input_path, tested_output_path, config) compare_data(reference_output_path, tested_output_path, imu_binaries_pairs) @@ -60,7 +60,7 @@ def test_2_extract_features_gait_output(shared_datadir: Path): tested_output_path = reference_output_path / "test-output" config = GaitFeatureExtractionConfig() - extract_gait_features(input_path, tested_output_path, config) + extract_gait_features_io(input_path, tested_output_path, config) compare_data(reference_output_path, tested_output_path, gait_binaries_pairs) @@ -80,7 +80,7 @@ def test_3_gait_detection_output(shared_datadir: Path): tested_output_path = reference_output_path / "test-output" config = GaitDetectionConfig() - detect_gait(input_path, tested_output_path, path_to_classifier_input, config) + detect_gait_io(input_path, tested_output_path, path_to_classifier_input, config) compare_data(reference_output_path, tested_output_path, gait_binaries_pairs) @@ -99,7 +99,7 @@ def test_4_extract_features_arm_swing_output(shared_datadir: Path): tested_output_path = reference_output_path / "test-output" config = ArmSwingFeatureExtractionConfig() - extract_arm_swing_features(input_path, tested_output_path, config) + extract_arm_swing_features_io(input_path, tested_output_path, config) compare_data(reference_output_path, tested_output_path, arm_swing_binaries_pairs) @@ -120,7 +120,7 @@ def test_5_arm_swing_detection_output(shared_datadir: Path): tested_output_path = reference_output_path / "test-output" config = ArmSwingDetectionConfig() - detect_arm_swing(input_path, tested_output_path, path_to_classifier_input, config) + detect_arm_swing_io(input_path, tested_output_path, path_to_classifier_input, config) compare_data(reference_output_path, tested_output_path, arm_swing_binaries_pairs) @@ -141,5 +141,5 @@ def test_6_arm_swing_quantification_output(shared_datadir: Path): tested_output_path = reference_output_path / "test-output" config = ArmSwingQuantificationConfig() - quantify_arm_swing(path_to_feature_input, path_to_prediction_input, tested_output_path, config) + quantify_arm_swing_io(path_to_feature_input, path_to_prediction_input, tested_output_path, config) compare_data(reference_output_path, tested_output_path, arm_swing_binaries_pairs) From 7c85f6de49097125f20225e3b32332dd614c767d Mon Sep 17 00:00:00 2001 From: Erikpostt Date: Fri, 4 Oct 2024 13:27:43 +0200 Subject: [PATCH 02/10] Change rounded prediction to probability --- src/paradigma/gait_analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/paradigma/gait_analysis.py b/src/paradigma/gait_analysis.py index 5b4c819..efea627 100644 --- a/src/paradigma/gait_analysis.py +++ b/src/paradigma/gait_analysis.py @@ -311,7 +311,7 @@ def detect_arm_swing(df: pd.DataFrame, config: ArmSwingDetectionConfig, clf: Uni X = df.loc[:, clf.feature_names_in_] # Make prediction - df['pred_arm_swing'] = clf.predict(X) + df['pred_arm_swing_proba'] = clf.predict_proba(X)[:, 1] return df From b1a9bff717f6298283661f544756252427f66a9f Mon Sep 17 00:00:00 2001 From: Erikpostt Date: Fri, 4 Oct 2024 13:28:05 +0200 Subject: [PATCH 03/10] Change rounded prediction to probability --- src/paradigma/gait_analysis.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/paradigma/gait_analysis.py b/src/paradigma/gait_analysis.py index efea627..4dbe5cd 100644 --- a/src/paradigma/gait_analysis.py +++ b/src/paradigma/gait_analysis.py @@ -329,8 +329,8 @@ def detect_arm_swing_io(input_path: Union[str, Path], output_path: Union[str, Pa metadata_samples.file_name = 'arm_swing_values.bin' metadata_time.file_name = 'arm_swing_time.bin' - metadata_samples.channels = ['pred_arm_swing'] - metadata_samples.units = ['boolean'] + metadata_samples.channels = ['pred_arm_swing_proba'] + metadata_samples.units = ['probability'] metadata_time.channels = ['time'] metadata_time.units = ['relative_time_ms'] From 9c2fcd9ff0ba65c8711022bdcf6384ec85746c14 Mon Sep 17 00:00:00 2001 From: Erikpostt Date: Fri, 4 Oct 2024 13:28:26 +0200 Subject: [PATCH 04/10] Fix constraint --- src/paradigma/windowing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/paradigma/windowing.py b/src/paradigma/windowing.py index 150df90..a90bbf0 100644 --- a/src/paradigma/windowing.py +++ b/src/paradigma/windowing.py @@ -203,7 +203,7 @@ def discard_segments( pd.DataFrame The dataframe with segments that are longer than the specified length """ - segment_length_bool = df.groupby(segment_nr_colname)[time_colname].apply(lambda x: x.max() - x.min()) > minimum_segment_length_s + segment_length_bool = df.groupby(segment_nr_colname)[time_colname].apply(lambda x: x.max() - x.min()) >= minimum_segment_length_s df = df.loc[df[segment_nr_colname].isin(segment_length_bool.loc[segment_length_bool.values].index)] From b28d27702d0767e5ad2f8f3482742f3977a90710 Mon Sep 17 00:00:00 2001 From: Erikpostt Date: Fri, 4 Oct 2024 13:31:35 +0200 Subject: [PATCH 05/10] Change float to int --- src/paradigma/gait_analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/paradigma/gait_analysis.py b/src/paradigma/gait_analysis.py index 4dbe5cd..654465e 100644 --- a/src/paradigma/gait_analysis.py +++ b/src/paradigma/gait_analysis.py @@ -378,7 +378,7 @@ def quantify_arm_swing(df: pd.DataFrame, config: ArmSwingQuantificationConfig) - l_quantiles=[0.95] ) - df_aggregates['segment_duration_ms'] = df_aggregates['segment_duration_s'] * 1000 + df_aggregates['segment_duration_ms'] = (df_aggregates['segment_duration_s'] * 1000).round().astype(int) df_aggregates = df_aggregates.drop(columns=['segment_nr']) return df_aggregates From 21e4d661ba830a97dc3b50a8932e11a05c7dfe6d Mon Sep 17 00:00:00 2001 From: Erikpostt Date: Fri, 4 Oct 2024 13:37:42 +0200 Subject: [PATCH 06/10] Change rounded -> proba --- src/paradigma/gait_analysis.py | 35 ++++++++++++++------------- src/paradigma/gait_analysis_config.py | 2 ++ 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/paradigma/gait_analysis.py b/src/paradigma/gait_analysis.py index 654465e..89788f2 100644 --- a/src/paradigma/gait_analysis.py +++ b/src/paradigma/gait_analysis.py @@ -62,7 +62,7 @@ def extract_gait_features_io(input_path: Union[str, Path], output_path: Union[st metadata_samples.channels = list(config.d_channels_values.keys()) metadata_samples.units = list(config.d_channels_values.values()) - metadata_time.channels = ['time'] + metadata_time.channels = [DataColumns.TIME] metadata_time.units = ['relative_time_ms'] write_data(metadata_time, metadata_samples, output_path, 'gait_meta.json', df_windowed) @@ -144,7 +144,7 @@ def extract_arm_swing_features(df: pd.DataFrame, config: ArmSwingFeatureExtracti df_segments = create_segments( df=df, time_colname=config.time_colname, - segment_nr_colname='segment_nr', + segment_nr_colname=DataColumns.SEGMENT_NR, minimum_gap_s=3 ) @@ -152,7 +152,7 @@ def extract_arm_swing_features(df: pd.DataFrame, config: ArmSwingFeatureExtracti df_segments = discard_segments( df=df_segments, time_colname=config.time_colname, - segment_nr_colname='segment_nr', + segment_nr_colname=DataColumns.SEGMENT_NR, minimum_segment_length_s=3 ) @@ -332,7 +332,7 @@ def detect_arm_swing_io(input_path: Union[str, Path], output_path: Union[str, Pa metadata_samples.channels = ['pred_arm_swing_proba'] metadata_samples.units = ['probability'] - metadata_time.channels = ['time'] + metadata_time.channels = [DataColumns.TIME] metadata_time.units = ['relative_time_ms'] write_data(metadata_time, metadata_samples, output_path, 'arm_swing_meta.json', df) @@ -341,10 +341,11 @@ def detect_arm_swing_io(input_path: Union[str, Path], output_path: Union[str, Pa def quantify_arm_swing(df: pd.DataFrame, config: ArmSwingQuantificationConfig) -> pd.DataFrame: # temporarily for testing: manually determine predictions - df[config.pred_arm_swing_colname] = np.concatenate([np.repeat([1], df.shape[0]//3), np.repeat([0], df.shape[0]//3), np.repeat([1], df.shape[0] - 2*df.shape[0]//3)], axis=0) + df[config.pred_arm_swing_proba_colname] = np.concatenate([np.repeat([1], df.shape[0]//3), np.repeat([0], df.shape[0]//3), np.repeat([1], df.shape[0] - 2*df.shape[0]//3)], axis=0) # keep only predicted arm swing - df_arm_swing = df.loc[df[config.pred_arm_swing_colname]==1].copy().reset_index(drop=True) + # TODO: Aggregate overlapping windows for probabilities + df_arm_swing = df.loc[df[config.pred_arm_swing_colname]>=0.5].copy().reset_index(drop=True) del df @@ -356,22 +357,22 @@ def quantify_arm_swing(df: pd.DataFrame, config: ArmSwingQuantificationConfig) - df_arm_swing = create_segments( df=df_arm_swing, - time_colname='time', - segment_nr_colname='segment_nr', + time_colname=DataColumns.TIME, + segment_nr_colname=DataColumns.SEGMENT_NR, minimum_gap_s=config.segment_gap_s ) df_arm_swing = discard_segments( df=df_arm_swing, - time_colname='time', - segment_nr_colname='segment_nr', + time_colname=DataColumns.TIME, + segment_nr_colname=DataColumns.SEGMENT_NR, minimum_segment_length_s=config.min_segment_length_s ) # Quantify arm swing df_aggregates = aggregate_segments( df=df_arm_swing, - time_colname='time', - segment_nr_colname='segment_nr', + time_colname=DataColumns.TIME, + segment_nr_colname=DataColumns.SEGMENT_NR, window_step_size_s=config.window_step_size, l_metrics=['range_of_motion', 'peak_ang_vel'], l_aggregates=['median'], @@ -379,7 +380,7 @@ def quantify_arm_swing(df: pd.DataFrame, config: ArmSwingQuantificationConfig) - ) df_aggregates['segment_duration_ms'] = (df_aggregates['segment_duration_s'] * 1000).round().astype(int) - df_aggregates = df_aggregates.drop(columns=['segment_nr']) + df_aggregates = df_aggregates.drop(columns=[DataColumns.SEGMENT_NR]) return df_aggregates @@ -399,14 +400,14 @@ def quantify_arm_swing_io(path_to_feature_input: Union[str, Path], path_to_predi assert df_features.shape[0] == df_predictions.shape[0] # Dataframes have same time column - assert df_features['time'].equals(df_predictions['time']) + assert df_features[DataColumns.TIME].equals(df_predictions[DataColumns.TIME]) # Subset features - l_feature_cols = ['time', 'range_of_motion', 'forward_peak_ang_vel_mean', 'backward_peak_ang_vel_mean'] + l_feature_cols = [DataColumns.TIME, 'range_of_motion', 'forward_peak_ang_vel_mean', 'backward_peak_ang_vel_mean'] df_features = df_features[l_feature_cols] # Concatenate features and predictions - df = pd.concat([df_features, df_predictions[config.pred_arm_swing_colname]], axis=1) + df = pd.concat([df_features, df_predictions[config.pred_arm_swing_proba_colname]], axis=1) df_aggregates = quantify_arm_swing(df, config) @@ -418,7 +419,7 @@ def quantify_arm_swing_io(path_to_feature_input: Union[str, Path], path_to_predi 'peak_ang_vel_median', 'peak_ang_vel_quantile_95'] metadata_samples.units = ['deg', 'deg', 'deg/s', 'deg/s'] - metadata_time.channels = ['time', 'segment_duration_ms'] + metadata_time.channels = [DataColumns.TIME, 'segment_duration_ms'] metadata_time.units = ['relative_time_ms', 'ms'] write_data(metadata_time, metadata_samples, output_path, 'arm_swing_meta.json', df_aggregates) diff --git a/src/paradigma/gait_analysis_config.py b/src/paradigma/gait_analysis_config.py index b073969..37d9d2e 100644 --- a/src/paradigma/gait_analysis_config.py +++ b/src/paradigma/gait_analysis_config.py @@ -177,6 +177,7 @@ def initialize_column_names( self ) -> None: + self.pred_gait_proba_colname=DataColumns.PRED_GAIT_PROBA self.pred_gait_colname=DataColumns.PRED_GAIT self.angle_smooth_colname: str = DataColumns.ANGLE_SMOOTH self.angle_colname=DataColumns.ANGLE @@ -258,6 +259,7 @@ def __init__(self) -> None: super().__init__() self.set_filenames_values("arm_swing") + self.pred_arm_swing_proba_colname = DataColumns.PRED_ARM_SWING_PROBA self.pred_arm_swing_colname = DataColumns.PRED_ARM_SWING self.window_length_s = 3 From 51e82c338ff949764ce10644a6e49a0c0dc05d2f Mon Sep 17 00:00:00 2001 From: Erikpostt Date: Fri, 4 Oct 2024 14:33:24 +0200 Subject: [PATCH 07/10] Add proba --- src/paradigma/constants.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/paradigma/constants.py b/src/paradigma/constants.py index 8640214..986591b 100644 --- a/src/paradigma/constants.py +++ b/src/paradigma/constants.py @@ -19,7 +19,9 @@ class DataColumns(): GRAV_ACCELEROMETER_X : str = "grav_accelerometer_x" GRAV_ACCELEROMETER_Y : str = "grav_accelerometer_y" GRAV_ACCELEROMETER_Z : str = "grav_accelerometer_z" + PRED_GAIT_PROBA: str = "pred_gait_proba" PRED_GAIT : str = "pred_gait" + PRED_ARM_SWING_PROBA: str = "pred_arm_swing_proba" PRED_ARM_SWING : str = "pred_arm_swing" ANGLE : str = "angle" ANGLE_SMOOTH : str = "angle_smooth" From fd6c9cf4c3d7876e84c9610f4973323b696acf9f Mon Sep 17 00:00:00 2001 From: Erikpostt Date: Fri, 4 Oct 2024 14:35:39 +0200 Subject: [PATCH 08/10] Fix constants for pred proba --- src/paradigma/gait_analysis.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/paradigma/gait_analysis.py b/src/paradigma/gait_analysis.py index 89788f2..6529776 100644 --- a/src/paradigma/gait_analysis.py +++ b/src/paradigma/gait_analysis.py @@ -311,7 +311,7 @@ def detect_arm_swing(df: pd.DataFrame, config: ArmSwingDetectionConfig, clf: Uni X = df.loc[:, clf.feature_names_in_] # Make prediction - df['pred_arm_swing_proba'] = clf.predict_proba(X)[:, 1] + df[DataColumns.PRED_ARM_SWING_PROBA] = clf.predict_proba(X)[:, 1] return df @@ -329,7 +329,7 @@ def detect_arm_swing_io(input_path: Union[str, Path], output_path: Union[str, Pa metadata_samples.file_name = 'arm_swing_values.bin' metadata_time.file_name = 'arm_swing_time.bin' - metadata_samples.channels = ['pred_arm_swing_proba'] + metadata_samples.channels = [DataColumns.PRED_ARM_SWING_PROBA] metadata_samples.units = ['probability'] metadata_time.channels = [DataColumns.TIME] From d57741048eedb3d8325701d49b007e97fc6e339d Mon Sep 17 00:00:00 2001 From: Erikpostt Date: Fri, 4 Oct 2024 14:48:23 +0200 Subject: [PATCH 09/10] Update test data --- .../4.predictions/gait/arm_swing_meta.json | 7 +++---- .../4.predictions/gait/arm_swing_values.bin | Bin 5136 -> 5136 bytes .../5.quantification/gait/arm_swing_meta.json | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/data/4.predictions/gait/arm_swing_meta.json b/tests/data/4.predictions/gait/arm_swing_meta.json index ee42b0e..db62bf5 100644 --- a/tests/data/4.predictions/gait/arm_swing_meta.json +++ b/tests/data/4.predictions/gait/arm_swing_meta.json @@ -8,10 +8,10 @@ "end_iso8601": "2021-06-27T17:04:30Z", "rows": 642, "endianness": "little", + "data_type": "float", "bits": 64, "sensors": [ { - "data_type": "float", "scale_factors": [ 1 ], @@ -24,13 +24,12 @@ ] }, { - "data_type": "int", "file_name": "arm_swing_values.bin", "channels": [ - "pred_arm_swing" + "pred_arm_swing_proba" ], "units": [ - "boolean" + "probability" ], "scale_factors": [ 0.00469378, diff --git a/tests/data/4.predictions/gait/arm_swing_values.bin b/tests/data/4.predictions/gait/arm_swing_values.bin index 43ed6637967d83acbe79fa1fc5a1542bced24f71..c8fa711a2fcd5ec25161a9134309fda90531e67b 100644 GIT binary patch literal 5136 zcmeIyF%19!2m`P=!@nCO5RgI2v!85D_PQ>lKj?!!aDW3G-~b0WzyS_$fCC)h00%h0 Hf!7WUua)=x literal 5136 zcmeIy!3_W~2m>*)|I!H90=b|5qr<6t@$Uk5(8DaC10CqV9dLjH9N+*4IKTl8aDW3G I;J{Z08b_Z3g8%>k diff --git a/tests/data/5.quantification/gait/arm_swing_meta.json b/tests/data/5.quantification/gait/arm_swing_meta.json index 80150f1..f2ebc40 100644 --- a/tests/data/5.quantification/gait/arm_swing_meta.json +++ b/tests/data/5.quantification/gait/arm_swing_meta.json @@ -8,8 +8,8 @@ "end_iso8601": "2021-06-27T17:04:30Z", "rows": 2, "endianness": "little", - "bits": 64, "data_type": "float", + "bits": 64, "sensors": [ { "scale_factors": [ From 71023556506301588ab548d9f7e3c74139ec7306 Mon Sep 17 00:00:00 2001 From: Erikpostt Date: Fri, 4 Oct 2024 14:52:37 +0200 Subject: [PATCH 10/10] rounded -> proba --- src/paradigma/gait_analysis.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/paradigma/gait_analysis.py b/src/paradigma/gait_analysis.py index 6529776..062de11 100644 --- a/src/paradigma/gait_analysis.py +++ b/src/paradigma/gait_analysis.py @@ -341,11 +341,11 @@ def detect_arm_swing_io(input_path: Union[str, Path], output_path: Union[str, Pa def quantify_arm_swing(df: pd.DataFrame, config: ArmSwingQuantificationConfig) -> pd.DataFrame: # temporarily for testing: manually determine predictions - df[config.pred_arm_swing_proba_colname] = np.concatenate([np.repeat([1], df.shape[0]//3), np.repeat([0], df.shape[0]//3), np.repeat([1], df.shape[0] - 2*df.shape[0]//3)], axis=0) + df[DataColumns.PRED_ARM_SWING_PROBA] = np.concatenate([np.repeat([1], df.shape[0]//3), np.repeat([0], df.shape[0]//3), np.repeat([1], df.shape[0] - 2*df.shape[0]//3)], axis=0) # keep only predicted arm swing # TODO: Aggregate overlapping windows for probabilities - df_arm_swing = df.loc[df[config.pred_arm_swing_colname]>=0.5].copy().reset_index(drop=True) + df_arm_swing = df.loc[df[DataColumns.PRED_ARM_SWING_PROBA]>=0.5].copy().reset_index(drop=True) del df @@ -407,7 +407,7 @@ def quantify_arm_swing_io(path_to_feature_input: Union[str, Path], path_to_predi df_features = df_features[l_feature_cols] # Concatenate features and predictions - df = pd.concat([df_features, df_predictions[config.pred_arm_swing_proba_colname]], axis=1) + df = pd.concat([df_features, df_predictions[DataColumns.PRED_ARM_SWING_PROBA]], axis=1) df_aggregates = quantify_arm_swing(df, config)