paradigma 1.0.4__py3-none-any.whl → 1.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.
@@ -1,3 +1,5 @@
1
+ import json
2
+ import logging
1
3
  from pathlib import Path
2
4
 
3
5
  import numpy as np
@@ -5,7 +7,7 @@ import pandas as pd
5
7
  from scipy import signal
6
8
 
7
9
  from paradigma.classification import ClassifierPackage
8
- from paradigma.config import TremorConfig
10
+ from paradigma.config import IMUConfig, TremorConfig
9
11
  from paradigma.constants import DataColumns
10
12
  from paradigma.feature_extraction import (
11
13
  compute_mfccs,
@@ -14,6 +16,7 @@ from paradigma.feature_extraction import (
14
16
  extract_frequency_peak,
15
17
  extract_tremor_power,
16
18
  )
19
+ from paradigma.preprocessing import preprocess_imu_data
17
20
  from paradigma.segmenting import WindowedDataExtractor, tabulate_windows
18
21
  from paradigma.util import aggregate_parameter
19
22
 
@@ -26,27 +29,33 @@ def extract_tremor_features(df: pd.DataFrame, config: TremorConfig) -> pd.DataFr
26
29
  Parameters
27
30
  ----------
28
31
  df : pd.DataFrame
29
- The input DataFrame containing sensor data, which includes time and gyroscope data. The data should be
30
- structured with the necessary columns as specified in the `config`.
32
+ The input DataFrame containing sensor data, which includes time and
33
+ gyroscope data. The data should be structured with the necessary
34
+ columns as specified in the `config`.
31
35
 
32
36
  config : TremorConfig
33
- Configuration object containing parameters for feature extraction, including column names for time, gyroscope data,
34
- as well as settings for windowing, and feature computation.
37
+ Configuration object containing parameters for feature extraction,
38
+ including column names for time, gyroscope data, as well as settings
39
+ for windowing, and feature computation.
35
40
 
36
41
  Returns
37
42
  -------
38
43
  pd.DataFrame
39
- A DataFrame containing extracted tremor features and a column corresponding to time.
44
+ A DataFrame containing extracted tremor features and a column
45
+ corresponding to time.
40
46
 
41
47
  Notes
42
48
  -----
43
49
  - This function groups the data into windows based on timestamps.
44
- - The input DataFrame must include columns as specified in the `config` object for proper feature extraction.
50
+ - The input DataFrame must include columns as specified in the
51
+ `config` object for proper feature extraction.
45
52
 
46
53
  Raises
47
54
  ------
48
55
  ValueError
49
- If the input DataFrame does not contain the required columns as specified in the configuration or if any step in the feature extraction fails.
56
+ If the input DataFrame does not contain the required columns as
57
+ specified in the configuration or if any step in the feature
58
+ extraction fails.
50
59
  """
51
60
  # group sequences of timestamps into windows
52
61
  windowed_colnames = [config.time_colname] + config.gyroscope_colnames
@@ -70,7 +79,8 @@ def extract_tremor_features(df: pd.DataFrame, config: TremorConfig) -> pd.DataFr
70
79
 
71
80
  df_features = pd.DataFrame(start_time, columns=[config.time_colname])
72
81
 
73
- # transform the signals from the temporal domain to the spectral domain and extract tremor features
82
+ # Transform the signals from the temporal domain to the spectral domain
83
+ # and extract tremor features
74
84
  df_spectral_features = extract_spectral_domain_features(windowed_gyro, config)
75
85
 
76
86
  # Combine spectral features with the start time
@@ -83,48 +93,68 @@ def detect_tremor(
83
93
  df: pd.DataFrame, config: TremorConfig, full_path_to_classifier_package: str | Path
84
94
  ) -> pd.DataFrame:
85
95
  """
86
- Detects tremor in the input DataFrame using a pre-trained classifier and applies a threshold to the predicted probabilities.
96
+ Detects tremor in the input DataFrame using a pre-trained classifier and
97
+ applies a threshold to the predicted probabilities.
87
98
 
88
99
  This function performs the following steps:
89
- 1. Loads the pre-trained classifier and scaling parameters from the provided directory.
90
- 2. Scales the relevant features in the input DataFrame (`df`) using the loaded scaling parameters.
91
- 3. Makes predictions using the classifier to estimate the probability of tremor.
92
- 4. Applies a threshold to the predicted probabilities to classify whether tremor is detected or not.
93
- 5. Checks for rest tremor by verifying the frequency of the peak and below tremor power.
94
- 6. Adds the predicted probabilities and the classification result to the DataFrame.
100
+ 1. Loads the pre-trained classifier and scaling parameters from the
101
+ provided directory.
102
+ 2. Scales the relevant features in the input DataFrame (`df`) using the
103
+ loaded scaling parameters.
104
+ 3. Makes predictions using the classifier to estimate the probability of
105
+ tremor.
106
+ 4. Applies a threshold to the predicted probabilities to classify whether
107
+ tremor is detected or not.
108
+ 5. Checks for rest tremor by verifying the frequency of the peak and
109
+ below tremor power.
110
+ 6. Adds the predicted probabilities and the classification result to the
111
+ DataFrame.
95
112
 
96
113
  Parameters
97
114
  ----------
98
115
  df : pd.DataFrame
99
- The input DataFrame containing extracted tremor features. The DataFrame must include
100
- the necessary columns as specified in the classifier's feature names.
116
+ The input DataFrame containing extracted tremor features. The
117
+ DataFrame must include the necessary columns as specified in the
118
+ classifier's feature names.
101
119
 
102
120
  config : TremorConfig
103
- Configuration object containing settings for tremor detection, including the frequency range for rest tremor.
121
+ Configuration object containing settings for tremor detection,
122
+ including the frequency range for rest tremor.
104
123
 
105
124
  full_path_to_classifier_package : str | Path
106
- The path to the directory containing the classifier file, threshold value, scaler parameters, and other necessary input
107
- files for tremor detection.
125
+ The path to the directory containing the classifier file, threshold
126
+ value, scaler parameters, and other necessary input files for tremor
127
+ detection.
108
128
 
109
129
  Returns
110
130
  -------
111
131
  pd.DataFrame
112
132
  The input DataFrame (`df`) with two additional columns:
113
- - `PRED_TREMOR_PROBA`: Predicted probability of tremor based on the classifier.
114
- - `PRED_TREMOR_LOGREG`: Binary classification result (True for tremor, False for no tremor), based on the threshold applied to `PRED_TREMOR_PROBA`.
115
- - `PRED_TREMOR_CHECKED`: Binary classification result (True for tremor, False for no tremor), after performing extra checks for rest tremor on `PRED_TREMOR_LOGREG`.
116
- - `PRED_ARM_AT_REST`: Binary classification result (True for arm at rest or stable posture, False for significant arm movement), based on the power below tremor.
133
+ - `PRED_TREMOR_PROBA`: Predicted probability of tremor based on the
134
+ classifier.
135
+ - `PRED_TREMOR_LOGREG`: Binary classification result (True for tremor,
136
+ False for no tremor), based on the threshold applied to
137
+ `PRED_TREMOR_PROBA`.
138
+ - `PRED_TREMOR_CHECKED`: Binary classification result (True for
139
+ tremor, False for no tremor), after performing extra checks for
140
+ rest tremor on `PRED_TREMOR_LOGREG`.
141
+ - `PRED_ARM_AT_REST`: Binary classification result (True for arm at
142
+ rest or stable posture, False for significant arm movement), based
143
+ on the power below tremor.
117
144
 
118
145
  Notes
119
146
  -----
120
- - The threshold used to classify tremor is loaded from a file and applied to the predicted probabilities.
147
+ - The threshold used to classify tremor is loaded from a file and
148
+ applied to the predicted probabilities.
121
149
 
122
150
  Raises
123
151
  ------
124
152
  FileNotFoundError
125
- If the classifier, scaler, or threshold files are not found at the specified paths.
153
+ If the classifier, scaler, or threshold files are not found at the
154
+ specified paths.
126
155
  ValueError
127
- If the DataFrame does not contain the expected features for prediction or if the prediction fails.
156
+ If the DataFrame does not contain the expected features for
157
+ prediction or if the prediction fails.
128
158
 
129
159
  """
130
160
 
@@ -140,11 +170,11 @@ def detect_tremor(
140
170
  scaled_features = clf_package.transform_features(df.loc[:, feature_names_scaling])
141
171
 
142
172
  # Replace scaled features in a copy of the relevant features for prediction
143
- X = df.loc[:, feature_names_predictions].copy()
144
- X.loc[:, feature_names_scaling] = scaled_features
173
+ x_features = df.loc[:, feature_names_predictions].copy()
174
+ x_features.loc[:, feature_names_scaling] = scaled_features
145
175
 
146
176
  # Get the tremor probability
147
- df[DataColumns.PRED_TREMOR_PROBA] = clf_package.predict_proba(X)
177
+ df[DataColumns.PRED_TREMOR_PROBA] = clf_package.predict_proba(x_features)
148
178
 
149
179
  # Make prediction based on pre-defined threshold
150
180
  df[DataColumns.PRED_TREMOR_LOGREG] = (
@@ -152,11 +182,11 @@ def detect_tremor(
152
182
  ).astype(int)
153
183
 
154
184
  # Perform extra checks for rest tremor
155
- peak_check = (df["freq_peak"] >= config.fmin_rest_tremor) & (
156
- df["freq_peak"] <= config.fmax_rest_tremor
185
+ peak_check = (df[DataColumns.FREQ_PEAK] >= config.fmin_rest_tremor) & (
186
+ df[DataColumns.FREQ_PEAK] <= config.fmax_rest_tremor
157
187
  ) # peak within 3-7 Hz
158
188
  df[DataColumns.PRED_ARM_AT_REST] = (
159
- df["below_tremor_power"] <= config.movement_threshold
189
+ df[DataColumns.BELOW_TREMOR_POWER] <= config.movement_threshold
160
190
  ).astype(
161
191
  int
162
192
  ) # arm at rest or in stable posture
@@ -171,31 +201,39 @@ def detect_tremor(
171
201
 
172
202
  def aggregate_tremor(df: pd.DataFrame, config: TremorConfig):
173
203
  """
174
- Quantifies the amount of tremor time and tremor power, aggregated over all windows in the input dataframe.
175
- Tremor time is calculated as the number of the detected tremor windows, as percentage of the number of windows
176
- without significant non-tremor movement (at rest). For tremor power the following aggregates are derived:
177
- the median, mode and percentile of tremor power specified in the configuration object.
204
+ Quantifies the amount of tremor time and tremor power, aggregated over
205
+ all windows in the input dataframe. Tremor time is calculated as the
206
+ number of the detected tremor windows, as percentage of the number of
207
+ windows without significant non-tremor movement (at rest). For tremor
208
+ power the following aggregates are derived: the median, mode and
209
+ percentile of tremor power specified in the configuration object.
178
210
 
179
211
  Parameters
180
212
  ----------
181
213
  df : pd.DataFrame
182
- The input DataFrame containing the tremor predictions and computed tremor power.
183
- The DataFrame must also contain a datatime column ('time_dt').
214
+ The input DataFrame containing the tremor predictions and computed
215
+ tremor power. The DataFrame must also contain a datatime column
216
+ ('time_dt').
184
217
 
185
218
  config : TremorConfig
186
- Configuration object containing the percentile for aggregating tremor power.
219
+ Configuration object containing the percentile for aggregating tremor
220
+ power.
187
221
 
188
222
  Returns
189
223
  -------
190
224
  dict
191
- A dictionary with the aggregated tremor time and tremor power measures, as well as the number of valid days,
192
- the total number of windows, and the number of windows at rest available in the input dataframe.
225
+ A dictionary with the aggregated tremor time and tremor power
226
+ measures, as well as the number of valid days, the total number of
227
+ windows, and the number of windows at rest available in the input
228
+ dataframe.
193
229
 
194
230
  Notes
195
231
  -----
196
- - Tremor power is converted to log scale, after adding a constant of 1, so that zero tremor power
197
- corresponds to a value of 0 in log scale.
198
- - The modal tremor power is computed based on gaussian kernel density estimation.
232
+ - Tremor power is converted to log scale, after adding a constant of
233
+ 1, so that zero tremor power corresponds to a value of 0 in log
234
+ scale.
235
+ - The modal tremor power is computed based on gaussian kernel density
236
+ estimation.
199
237
 
200
238
  """
201
239
  nr_valid_days = (
@@ -203,7 +241,8 @@ def aggregate_tremor(df: pd.DataFrame, config: TremorConfig):
203
241
  ) # number of valid days in the input dataframe
204
242
  nr_windows_total = df.shape[0] # number of windows in the input dataframe
205
243
 
206
- # remove windows with detected non-tremor arm movements to control for the amount of arm activities performed
244
+ # Remove windows with detected non-tremor arm movements to control for
245
+ # the amount of arm activities performed
207
246
  df_filtered = df.loc[df.pred_arm_at_rest == 1]
208
247
  nr_windows_rest = df_filtered.shape[
209
248
  0
@@ -214,20 +253,16 @@ def aggregate_tremor(df: pd.DataFrame, config: TremorConfig):
214
253
  ): # if no windows without non-tremor arm movement are detected
215
254
  raise Warning("No windows without non-tremor arm movement are detected.")
216
255
 
217
- # calculate tremor time
218
- n_windows_tremor = np.sum(df_filtered["pred_tremor_checked"])
219
- perc_windows_tremor = (
220
- n_windows_tremor / nr_windows_rest * 100
221
- ) # as percentage of total measured time without non-tremor arm movement
256
+ # Calculate tremor time
257
+ n_windows_tremor = np.sum(df_filtered[DataColumns.PRED_TREMOR_CHECKED])
258
+ # As percentage of total measured time without non-tremor arm movement
259
+ perc_windows_tremor = n_windows_tremor / nr_windows_rest * 100
222
260
 
223
- aggregated_tremor_power = (
224
- {}
225
- ) # initialize dictionary to store aggregated tremor power measures
226
-
227
- if (
228
- n_windows_tremor == 0
229
- ): # if no tremor is detected, the tremor power measures are set to NaN
261
+ # Initialize dictionary to store aggregated tremor power measures
262
+ aggregated_tremor_power = {}
230
263
 
264
+ # If no tremor is detected, the tremor power measures are set to NaN
265
+ if n_windows_tremor == 0:
231
266
  aggregated_tremor_power["median_tremor_power"] = np.nan
232
267
  aggregated_tremor_power["mode_binned_tremor_power"] = np.nan
233
268
  aggregated_tremor_power["90p_tremor_power"] = np.nan
@@ -236,7 +271,7 @@ def aggregate_tremor(df: pd.DataFrame, config: TremorConfig):
236
271
 
237
272
  # calculate aggregated tremor power measures
238
273
  tremor_power = df_filtered.loc[
239
- df_filtered["pred_tremor_checked"] == 1, "tremor_power"
274
+ df_filtered[DataColumns.PRED_TREMOR_CHECKED] == 1, DataColumns.TREMOR_POWER
240
275
  ]
241
276
  tremor_power = np.log10(tremor_power + 1) # convert to log scale
242
277
 
@@ -268,22 +303,25 @@ def extract_spectral_domain_features(data: np.ndarray, config) -> pd.DataFrame:
268
303
  """
269
304
  Compute spectral domain features from the gyroscope data.
270
305
 
271
- This function computes Mel-frequency cepstral coefficients (MFCCs), the frequency of the peak,
272
- the tremor power, and the below tremor power based on the total power spectral density of the windowed gyroscope data.
306
+ This function computes Mel-frequency cepstral coefficients (MFCCs), the
307
+ frequency of the peak, the tremor power, and the below tremor power
308
+ based on the total power spectral density of the windowed gyroscope
309
+ data.
273
310
 
274
311
  Parameters
275
312
  ----------
276
313
  data : numpy.ndarray
277
314
  A 2D numpy array where each row corresponds to a window of gyroscope data.
278
315
  config : object
279
- Configuration object containing settings such as sampling frequency, window type,
280
- and MFCC parameters.
316
+ Configuration object containing settings such as sampling frequency,
317
+ window type, and MFCC parameters.
281
318
 
282
319
  Returns
283
320
  -------
284
321
  pd.DataFrame
285
- The feature dataframe containing the extracted spectral features, including
286
- MFCCs, the frequency of the peak, the tremor power and below tremor power for each window.
322
+ The feature dataframe containing the extracted spectral features,
323
+ including MFCCs, the frequency of the peak, the tremor power and
324
+ below tremor power for each window.
287
325
 
288
326
  """
289
327
 
@@ -321,7 +359,7 @@ def extract_spectral_domain_features(data: np.ndarray, config) -> pd.DataFrame:
321
359
  overlap_n = segment_length_n * overlap_fraction
322
360
  window = signal.get_window(window_type, segment_length_n)
323
361
 
324
- f, t, S1 = signal.stft(
362
+ f, t, stft_result = signal.stft(
325
363
  x=data,
326
364
  fs=sampling_frequency,
327
365
  window=window,
@@ -331,9 +369,10 @@ def extract_spectral_domain_features(data: np.ndarray, config) -> pd.DataFrame:
331
369
  axis=1,
332
370
  )
333
371
 
334
- # Compute total power in the PSD and the total spectrogram (summed over the three axes)
372
+ # Compute total power in the PSD and the total spectrogram (summed over
373
+ # the three axes)
335
374
  total_psd = compute_total_power(psd)
336
- total_spectrogram = np.sum(np.abs(S1) * sampling_frequency, axis=2)
375
+ total_spectrogram = np.sum(np.abs(stft_result) * sampling_frequency, axis=2)
337
376
 
338
377
  # Compute the MFCC's
339
378
  config.mfcc_low_frequency = config.fmin_mfcc
@@ -355,10 +394,10 @@ def extract_spectral_domain_features(data: np.ndarray, config) -> pd.DataFrame:
355
394
  feature_dict[colname] = mfccs[:, i]
356
395
 
357
396
  # Compute the frequency of the peak, non-tremor power and tremor power
358
- feature_dict["freq_peak"] = extract_frequency_peak(
397
+ feature_dict[DataColumns.FREQ_PEAK] = extract_frequency_peak(
359
398
  freqs, total_psd, config.fmin_peak_search, config.fmax_peak_search
360
399
  )
361
- feature_dict["below_tremor_power"] = compute_power_in_bandwidth(
400
+ feature_dict[DataColumns.BELOW_TREMOR_POWER] = compute_power_in_bandwidth(
362
401
  freqs,
363
402
  total_psd,
364
403
  config.fmin_below_rest_tremor,
@@ -367,8 +406,189 @@ def extract_spectral_domain_features(data: np.ndarray, config) -> pd.DataFrame:
367
406
  spectral_resolution=config.spectral_resolution,
368
407
  cumulative_sum_method="sum",
369
408
  )
370
- feature_dict["tremor_power"] = extract_tremor_power(
409
+ feature_dict[DataColumns.TREMOR_POWER] = extract_tremor_power(
371
410
  freqs, total_psd, config.fmin_rest_tremor, config.fmax_rest_tremor
372
411
  )
373
412
 
374
413
  return pd.DataFrame(feature_dict)
414
+
415
+
416
+ def run_tremor_pipeline(
417
+ df_prepared: pd.DataFrame,
418
+ output_dir: str | Path,
419
+ store_intermediate: list[str] = [],
420
+ tremor_config: TremorConfig | None = None,
421
+ imu_config: IMUConfig | None = None,
422
+ logging_level: int = logging.INFO,
423
+ custom_logger: logging.Logger | None = None,
424
+ ) -> pd.DataFrame:
425
+ """
426
+ High-level tremor analysis pipeline for a single segment.
427
+
428
+ This function implements the complete tremor analysis workflow from the
429
+ tremor tutorial:
430
+ 1. Preprocess gyroscope data
431
+ 2. Extract tremor features
432
+ 3. Detect tremor
433
+ 4. Quantify tremor (select relevant columns)
434
+
435
+ Parameters
436
+ ----------
437
+ df_prepared : pd.DataFrame
438
+ Prepared sensor data with time and gyroscope columns
439
+ output_dir : str or Path
440
+ Output directory for intermediate results (required)
441
+ store_intermediate : list of str, default []
442
+ Which intermediate results to store
443
+ tremor_config : TremorConfig, optional
444
+ Tremor analysis configuration
445
+ imu_config : IMUConfig, optional
446
+ IMU preprocessing configuration
447
+ logging_level : int, default logging.INFO
448
+ Logging level using standard logging constants
449
+ custom_logger : logging.Logger, optional
450
+ Custom logger instance
451
+
452
+ Returns
453
+ -------
454
+ pd.DataFrame
455
+ Quantified tremor data with columns:
456
+ - time: timestamp
457
+ - pred_arm_at_rest: arm at rest prediction
458
+ - pred_tremor_checked: tremor detection result
459
+ - tremor_power: tremor power measure
460
+
461
+ """
462
+ # Setup logger
463
+ active_logger = (
464
+ custom_logger if custom_logger is not None else logging.getLogger(__name__)
465
+ )
466
+ if custom_logger is None:
467
+ active_logger.setLevel(logging_level)
468
+
469
+ if tremor_config is None:
470
+ tremor_config = TremorConfig()
471
+ if imu_config is None:
472
+ imu_config = IMUConfig()
473
+
474
+ output_dir = Path(output_dir)
475
+
476
+ # Validate input data columns
477
+ required_columns = [
478
+ DataColumns.TIME,
479
+ DataColumns.GYROSCOPE_X,
480
+ DataColumns.GYROSCOPE_Y,
481
+ DataColumns.GYROSCOPE_Z,
482
+ ]
483
+ missing_columns = [
484
+ col for col in required_columns if col not in df_prepared.columns
485
+ ]
486
+ if missing_columns:
487
+ active_logger.warning(
488
+ f"Missing required columns for tremor pipeline: " f"{missing_columns}"
489
+ )
490
+ return pd.DataFrame()
491
+
492
+ # Step 1: Preprocess gyroscope data (following tutorial)
493
+ active_logger.info("Step 1: Preprocessing gyroscope data")
494
+ df_preprocessed = preprocess_imu_data(
495
+ df_prepared,
496
+ imu_config,
497
+ sensor="gyroscope",
498
+ watch_side="left", # Watch side is unimportant for tremor detection
499
+ verbose=1 if logging_level <= logging.INFO else 0,
500
+ )
501
+
502
+ if "preprocessing" in store_intermediate:
503
+ preprocessing_dir = output_dir / "preprocessing"
504
+ preprocessing_dir.mkdir(exist_ok=True)
505
+ df_preprocessed.to_parquet(preprocessing_dir / "tremor_preprocessed.parquet")
506
+ active_logger.info(f"Saved preprocessed data to {preprocessing_dir}")
507
+
508
+ # Step 2: Extract tremor features
509
+ active_logger.info("Step 2: Extracting tremor features")
510
+ df_features = extract_tremor_features(df_preprocessed, tremor_config)
511
+
512
+ if "tremor" in store_intermediate:
513
+ tremor_dir = output_dir / "tremor"
514
+ tremor_dir.mkdir(exist_ok=True)
515
+ df_features.to_parquet(tremor_dir / "tremor_features.parquet")
516
+ active_logger.info(f"Saved tremor features to {tremor_dir}")
517
+
518
+ # Step 3: Detect tremor
519
+ active_logger.info("Step 3: Detecting tremor")
520
+ try:
521
+ from importlib.resources import files
522
+
523
+ classifier_path = files("paradigma.assets") / "tremor_detection_clf_package.pkl"
524
+ df_predictions = detect_tremor(df_features, tremor_config, classifier_path)
525
+ except Exception as e:
526
+ active_logger.error(f"Tremor detection failed: {e}")
527
+ return pd.DataFrame()
528
+
529
+ # Step 4: Quantify tremor (following tutorial pattern)
530
+ active_logger.info("Step 4: Quantifying tremor")
531
+
532
+ # Select quantification columns as in the tutorial
533
+ quantification_columns = [
534
+ tremor_config.time_colname,
535
+ DataColumns.PRED_ARM_AT_REST,
536
+ DataColumns.PRED_TREMOR_CHECKED,
537
+ DataColumns.TREMOR_POWER,
538
+ ]
539
+
540
+ # Check if all required columns exist
541
+ available_columns = [
542
+ col for col in quantification_columns if col in df_predictions.columns
543
+ ]
544
+ if len(available_columns) != len(quantification_columns):
545
+ missing = set(quantification_columns) - set(available_columns)
546
+ active_logger.warning(f"Missing quantification columns: {missing}")
547
+ # Use available columns
548
+ quantification_columns = available_columns
549
+
550
+ df_quantification = df_predictions[quantification_columns].copy()
551
+
552
+ # Set tremor power to None for non-tremor windows (following tutorial)
553
+ if (
554
+ DataColumns.TREMOR_POWER in df_quantification.columns
555
+ and DataColumns.PRED_TREMOR_CHECKED in df_quantification.columns
556
+ ):
557
+ df_quantification.loc[
558
+ df_quantification[DataColumns.PRED_TREMOR_CHECKED] == 0,
559
+ DataColumns.TREMOR_POWER,
560
+ ] = None
561
+
562
+ if "quantification" in store_intermediate:
563
+ quantification_dir = output_dir / "quantification"
564
+ quantification_dir.mkdir(exist_ok=True)
565
+ df_quantification.to_parquet(
566
+ quantification_dir / "tremor_quantification.parquet"
567
+ )
568
+
569
+ # Save quantification metadata
570
+ quantification_meta = {
571
+ "total_windows": len(df_quantification),
572
+ "tremor_windows": (
573
+ int(df_quantification[DataColumns.PRED_TREMOR_CHECKED].sum())
574
+ if DataColumns.PRED_TREMOR_CHECKED in df_quantification.columns
575
+ else 0
576
+ ),
577
+ "columns": list(df_quantification.columns),
578
+ }
579
+ with open(quantification_dir / "tremor_quantification_meta.json", "w") as f:
580
+ json.dump(quantification_meta, f, indent=2)
581
+
582
+ active_logger.debug(f"Saved tremor quantification to {quantification_dir}")
583
+
584
+ tremor_windows = (
585
+ int(df_quantification[DataColumns.PRED_TREMOR_CHECKED].sum())
586
+ if DataColumns.PRED_TREMOR_CHECKED in df_quantification.columns
587
+ else 0
588
+ )
589
+ active_logger.info(
590
+ f"Tremor analysis completed: {tremor_windows} tremor windows "
591
+ f"detected from {len(df_quantification)} total windows"
592
+ )
593
+
594
+ return df_quantification