Skip to content

Commit c70cc5a

Browse files
committed
hrv - fixed handling of frequencies
1 parent 0250302 commit c70cc5a

1 file changed

Lines changed: 22 additions & 36 deletions

File tree

src/data_analysis/heart_rate_variability/exploring_heart_rate_variability.clj

Lines changed: 22 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
^{: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]}}}
99
(ns data-analysis.heart-rate-variability.exploring-heart-rate-variability
1010
(:require [tablecloth.api :as tc]
1111
[scicloj.tableplot.v1.plotly :as plotly]
@@ -27,7 +27,6 @@
2727
[scicloj.tableplot.v1.plotly :as plotly]
2828
[java-time.api :as jt]))
2929

30-
3130
;; # Exploring HRV - DRAFT 🛠
3231

3332
(ns data-analysis.heart-rate-variability.exploring-heart-rate-variability
@@ -52,13 +51,10 @@
5251
[com.github.psambit9791.jdsp.filter Butterworth]
5352
[com.github.psambit9791.jdsp.transform DiscreteFourier]))
5453

55-
5654
;; ## My pulse-to-pulse intervals
5755

58-
5956
;; (extracted from PPG data)
6057

61-
6258
(def my-ppi
6359
(-> (tc/dataset "src/data_analysis/heart_rate_variability/ppi-series.csv"
6460
{:key-fn keyword})
@@ -75,10 +71,9 @@
7571
:=height 300 :=width 700})
7672
(plotly/layer-bar {:=y :ppi})))
7773

78-
7974
(def compute-measures
8075
(fn [ppi-ds {:keys [sampling-rate
81-
window-size-in-sec ]}]
76+
window-size-in-sec]}]
8277
(let [spline (interp/interpolation
8378
:cubic
8479
(:t ppi-ds)
@@ -92,7 +87,7 @@
9287
bw (com.github.psambit9791.jdsp.filter.Butterworth.
9388
sampling-rate)
9489
n (tc/row-count resampled-ppi)
95-
window-size (* window-size-in-sec sampling-rate)
90+
window-size (* window-size-in-sec sampling-rate)
9691
hop-size 8
9792
n-windows (int (/ (- n window-size)
9893
hop-size))
@@ -140,17 +135,16 @@
140135
:magnitude (take 40 whole-magnitude)})))
141136
vec)]
142137
{:sampling-rate sampling-rate
138+
:window-size window-size ;; Added: FFT window size needed for freq resolution
143139
:resampled-ppi resampled-ppi
144140
:rmssds rmssds
145141
:spectrograms spectrograms})))
146142

147-
148143
(comment
149144
(compute-measures my-ppi
150145
{:sampling-rate 10
151146
:window-size-in-sec 60}))
152147

153-
154148
;; [An Overview of Heart Rate Variability Metrics and Norms](https://pmc.ncbi.nlm.nih.gov/articles/PMC5624990/)
155149
;; by Fred Shaffer, & J P Ginsberg.
156150
;; doi: [10.3389/fpubh.2017.00258](https://www.frontiersin.org/journals/public-health/articles/10.3389/fpubh.2017.00258/full)
@@ -168,18 +162,22 @@
168162
:s
169163
tcc/sum))))
170164

171-
172165
(defn plot-with-measures [{:keys [sampling-rate
166+
window-size
173167
resampled-ppi
174168
rmssds
175169
spectrograms]}]
176170
(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)
178177
Nyquist-freq (/ sampling-rate 2.0)
179-
freq-resolution (/ Nyquist-freq n)
180178
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)]
183181
{:resampled-ppi (-> resampled-ppi
184182
(plotly/base {:=height 300 :=width 700})
185183
(plotly/layer-bar (merge {:=x :t
@@ -206,7 +204,7 @@
206204
:width 700
207205
:margin {:t 25}
208206
:xaxis {:title {:text "t"}}
209-
:yaxis {:title {:text "freq"}}}})
207+
:yaxis {:title {:text "freq (Hz)"}}}})
210208
:mean-power-spectrum (-> {:freq freqs
211209
:mean-power (-> spectrograms
212210
(->> (map :magnitude))
@@ -227,14 +225,12 @@
227225
(assoc-in [:layout :yaxis :range] [0 4])
228226
(assoc-in [:layout :yaxis :title] {:text "LF/HF"}))})))
229227

230-
231228
(delay
232229
(-> my-ppi
233230
(compute-measures {:sampling-rate 10
234231
:window-size-in-sec 60})
235232
plot-with-measures))
236233

237-
238234
;; ## Analysing ECG data
239235

240236
;; ### The [WESAD](https://dl.acm.org/doi/10.1145/3242969.3242985) dataset
@@ -265,8 +261,8 @@
265261
:ECG (-> ld
266262
(get-in [:signal :chest :ECG])
267263
(py. flatten))
268-
:label (-> ld
269-
(get :label))})))))
264+
:label (-> ld
265+
(get :label))})))))
270266

271267
(delay
272268
(labelled-dataset 5))
@@ -290,7 +286,6 @@
290286
final-peaks (.filterByPeakDistance peak-obj height-filtered distance)]
291287
final-peaks)) ; Returns int[] of peak row-numbers
292288

293-
294289
(delay
295290
(let [bw (com.github.psambit9791.jdsp.filter.Butterworth.
296291
WESAD-sampling-rate)
@@ -328,7 +323,6 @@
328323
(plotly/layer-point {:=y :peak
329324
:=name "peak"}))))
330325

331-
332326
(defn extract-ppi
333327
"Extract peak-to-peak intervals from ECG signal.
334328
Returns dataset with columns: :t (time in seconds), :ppi (interval in seconds)"
@@ -381,7 +375,6 @@
381375
extract-ppi
382376
(compute-measures measures-params)))))
383377

384-
385378
(delay
386379
(-> {:ppi-params {:subject-id 5
387380
:row-interval [0 1000000]}
@@ -413,11 +406,9 @@
413406
(tc/add-column :offset #(cons 0 (reductions + (:n %))))
414407
(tc/select-columns [:offset :n :label])))))
415408

416-
417409
(delay
418410
(label-intervals 5))
419411

420-
421412
(delay
422413
(let [subject 5]
423414
(-> (label-intervals subject)
@@ -434,11 +425,6 @@
434425
plot-with-measures)
435426
(catch Exception e 'unavailable))]))))))
436427

437-
438-
439-
440-
441-
442428
;; ## Conclusion
443429

444430
;; - measures are tricky

0 commit comments

Comments
 (0)