|
1 | 1 | ^{:kindly/hide-code true |
2 | | - :clay {:title "Exploring Heart Rate Variability" |
3 | | - :external-requirements ["WESAD dataset at /workspace/datasets/WESAD/"] |
4 | | - :quarto {:author :ludgersolbach |
5 | | - :draft true |
6 | | - :type :post |
7 | | - :date "2025-10-17" |
8 | | - :tags [:data-analysis :noj]}}} |
| 2 | + :clay {:title "Exploring Heart Rate Variability" |
| 3 | + :external-requirements ["WESAD dataset at /workspace/datasets/WESAD/"] |
| 4 | + :quarto {:author :ludgersolbach |
| 5 | + :draft true |
| 6 | + :type :post |
| 7 | + :date "2025-10-17" |
| 8 | + :tags [:data-analysis :noj]}}} |
9 | 9 | (ns data-analysis.heart-rate-variability.exploring-heart-rate-variability |
10 | 10 | (:require [tablecloth.api :as tc] |
11 | 11 | [scicloj.tableplot.v1.plotly :as plotly] |
|
27 | 27 | [scicloj.tableplot.v1.plotly :as plotly] |
28 | 28 | [java-time.api :as jt])) |
29 | 29 |
|
30 | | - |
31 | 30 | ;; # Exploring HRV - DRAFT 🛠 |
32 | 31 |
|
33 | 32 | (ns data-analysis.heart-rate-variability.exploring-heart-rate-variability |
|
52 | 51 | [com.github.psambit9791.jdsp.filter Butterworth] |
53 | 52 | [com.github.psambit9791.jdsp.transform DiscreteFourier])) |
54 | 53 |
|
55 | | - |
56 | 54 | ;; ## My pulse-to-pulse intervals |
57 | 55 |
|
58 | | - |
59 | 56 | ;; (extracted from PPG data) |
60 | 57 |
|
61 | | - |
62 | 58 | (def my-ppi |
63 | 59 | (-> (tc/dataset "src/data_analysis/heart_rate_variability/ppi-series.csv" |
64 | 60 | {:key-fn keyword}) |
|
75 | 71 | :=height 300 :=width 700}) |
76 | 72 | (plotly/layer-bar {:=y :ppi}))) |
77 | 73 |
|
78 | | - |
79 | 74 | (def compute-measures |
80 | 75 | (fn [ppi-ds {:keys [sampling-rate |
81 | | - window-size-in-sec ]}] |
| 76 | + window-size-in-sec]}] |
82 | 77 | (let [spline (interp/interpolation |
83 | 78 | :cubic |
84 | 79 | (:t ppi-ds) |
|
92 | 87 | bw (com.github.psambit9791.jdsp.filter.Butterworth. |
93 | 88 | sampling-rate) |
94 | 89 | n (tc/row-count resampled-ppi) |
95 | | - window-size (* window-size-in-sec sampling-rate) |
| 90 | + window-size (* window-size-in-sec sampling-rate) |
96 | 91 | hop-size 8 |
97 | 92 | n-windows (int (/ (- n window-size) |
98 | 93 | hop-size)) |
|
140 | 135 | :magnitude (take 40 whole-magnitude)}))) |
141 | 136 | vec)] |
142 | 137 | {:sampling-rate sampling-rate |
| 138 | + :window-size window-size ;; Added: FFT window size needed for freq resolution |
143 | 139 | :resampled-ppi resampled-ppi |
144 | 140 | :rmssds rmssds |
145 | 141 | :spectrograms spectrograms}))) |
146 | 142 |
|
147 | | - |
148 | 143 | (comment |
149 | 144 | (compute-measures my-ppi |
150 | 145 | {:sampling-rate 10 |
151 | 146 | :window-size-in-sec 60})) |
152 | 147 |
|
153 | | - |
154 | 148 | ;; [An Overview of Heart Rate Variability Metrics and Norms](https://pmc.ncbi.nlm.nih.gov/articles/PMC5624990/) |
155 | 149 | ;; by Fred Shaffer, & J P Ginsberg. |
156 | 150 | ;; doi: [10.3389/fpubh.2017.00258](https://www.frontiersin.org/journals/public-health/articles/10.3389/fpubh.2017.00258/full) |
|
168 | 162 | :s |
169 | 163 | tcc/sum)))) |
170 | 164 |
|
171 | | - |
172 | 165 | (defn plot-with-measures [{:keys [sampling-rate |
| 166 | + window-size |
173 | 167 | resampled-ppi |
174 | 168 | rmssds |
175 | 169 | spectrograms]}] |
176 | 170 | (when spectrograms |
177 | | - (let [n (-> spectrograms first :magnitude count) |
| 171 | + (let [;; Number of frequency bins we're displaying (truncated spectrum) |
| 172 | + n (-> spectrograms first :magnitude count) |
| 173 | + ;; Correct frequency resolution based on FFT window size |
| 174 | + ;; freq_resolution = sampling_rate / FFT_size |
| 175 | + freq-resolution (/ sampling-rate window-size) |
| 176 | + ;; Nyquist frequency (maximum representable frequency) |
178 | 177 | Nyquist-freq (/ sampling-rate 2.0) |
179 | | - freq-resolution (/ Nyquist-freq n) |
180 | 178 | times (map (comp str :t) spectrograms) |
181 | | - freqs (tcc/* (range n) |
182 | | - freq-resolution)] |
| 179 | + ;; Generate frequency values for the n bins we're displaying |
| 180 | + freqs (tcc/* (range n) freq-resolution)] |
183 | 181 | {:resampled-ppi (-> resampled-ppi |
184 | 182 | (plotly/base {:=height 300 :=width 700}) |
185 | 183 | (plotly/layer-bar (merge {:=x :t |
|
206 | 204 | :width 700 |
207 | 205 | :margin {:t 25} |
208 | 206 | :xaxis {:title {:text "t"}} |
209 | | - :yaxis {:title {:text "freq"}}}}) |
| 207 | + :yaxis {:title {:text "freq (Hz)"}}}}) |
210 | 208 | :mean-power-spectrum (-> {:freq freqs |
211 | 209 | :mean-power (-> spectrograms |
212 | 210 | (->> (map :magnitude)) |
|
227 | 225 | (assoc-in [:layout :yaxis :range] [0 4]) |
228 | 226 | (assoc-in [:layout :yaxis :title] {:text "LF/HF"}))}))) |
229 | 227 |
|
230 | | - |
231 | 228 | (delay |
232 | 229 | (-> my-ppi |
233 | 230 | (compute-measures {:sampling-rate 10 |
234 | 231 | :window-size-in-sec 60}) |
235 | 232 | plot-with-measures)) |
236 | 233 |
|
237 | | - |
238 | 234 | ;; ## Analysing ECG data |
239 | 235 |
|
240 | 236 | ;; ### The [WESAD](https://dl.acm.org/doi/10.1145/3242969.3242985) dataset |
|
265 | 261 | :ECG (-> ld |
266 | 262 | (get-in [:signal :chest :ECG]) |
267 | 263 | (py. flatten)) |
268 | | - :label (-> ld |
269 | | - (get :label))}))))) |
| 264 | + :label (-> ld |
| 265 | + (get :label))}))))) |
270 | 266 |
|
271 | 267 | (delay |
272 | 268 | (labelled-dataset 5)) |
|
290 | 286 | final-peaks (.filterByPeakDistance peak-obj height-filtered distance)] |
291 | 287 | final-peaks)) ; Returns int[] of peak row-numbers |
292 | 288 |
|
293 | | - |
294 | 289 | (delay |
295 | 290 | (let [bw (com.github.psambit9791.jdsp.filter.Butterworth. |
296 | 291 | WESAD-sampling-rate) |
|
328 | 323 | (plotly/layer-point {:=y :peak |
329 | 324 | :=name "peak"})))) |
330 | 325 |
|
331 | | - |
332 | 326 | (defn extract-ppi |
333 | 327 | "Extract peak-to-peak intervals from ECG signal. |
334 | 328 | Returns dataset with columns: :t (time in seconds), :ppi (interval in seconds)" |
|
381 | 375 | extract-ppi |
382 | 376 | (compute-measures measures-params))))) |
383 | 377 |
|
384 | | - |
385 | 378 | (delay |
386 | 379 | (-> {:ppi-params {:subject-id 5 |
387 | 380 | :row-interval [0 1000000]} |
|
413 | 406 | (tc/add-column :offset #(cons 0 (reductions + (:n %)))) |
414 | 407 | (tc/select-columns [:offset :n :label]))))) |
415 | 408 |
|
416 | | - |
417 | 409 | (delay |
418 | 410 | (label-intervals 5)) |
419 | 411 |
|
420 | | - |
421 | 412 | (delay |
422 | 413 | (let [subject 5] |
423 | 414 | (-> (label-intervals subject) |
|
434 | 425 | plot-with-measures) |
435 | 426 | (catch Exception e 'unavailable))])))))) |
436 | 427 |
|
437 | | - |
438 | | - |
439 | | - |
440 | | - |
441 | | - |
442 | 428 | ;; ## Conclusion |
443 | 429 |
|
444 | 430 | ;; - measures are tricky |
|
0 commit comments