-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathreadme.Rmd
More file actions
623 lines (426 loc) · 25.2 KB
/
Copy pathreadme.Rmd
File metadata and controls
623 lines (426 loc) · 25.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
---
title: "Project Description and Data Processing Workflow"
author: "Group 2 Data Science Spring School 2022 (Anders Isaksen, Hugo Fitipaldi, Sam Ghatan, Sedrah Butt)"
output:
html_document:
toc: true
keep_md: yes
code_folding: hide
---
```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = TRUE)
```
# Preface:
This is the repository for ~~Group *Anders*~~ **Group 2** of the Data Science Spring School & Challenge, notorious Kahoot quiz winners!

Update: and **BEST CHALLENGE AWARD WINNERS!**!

This document is best viewed on [GitHub Pages](https://pandapowell.github.io/ECG_DDA/index.html).
This project is fully open-source from raw data to output and presentation, so please join and help us improve the model. Sources are contained in the [GitHub repository](https://github.com/PandaPowell/ECG_DDA).
A questionable model unfit for deployment is deployed [here](https://mybinder.org/v2/gh/Aastedet/ecg_neuropathy_app/main?urlpath=%2Fvoila%2Frender%2Fecg_neuropathy_app.ipynb).
This repository contains the *R* code used to process data, and also contains four datasets from PhysioNet with data on diabetes and neuropathy status. While ECG data is not contained in the repository, it is available for download on PhysioNet for all datasets, although a substantial proportion of participants are missing ECG or tabular data.
Throughout this document, *R* code to reproduce data processing and population flow is provided in folded chunks below the section of the text where these are mentioned.
The final processing of ECG data and neural network training is performed using Python in a Google Colab Notebook [here](https://colab.research.google.com/drive/1yezNX6pxlHbcWRo08Zk5r23IShMEXZQN?usp=sharing) and [here](https://colab.research.google.com/drive/1_b-j3hDzYTYbbqGdyUnoaGiPZyRMbcJE?usp=sharing). The filtered ECG data output from the `readme.Rmd` *R* script and used in the notebook can be found on [Google Drive](https://drive.google.com/drive/folders/17AX33KuzP2nZwCQ9sWErY_0bSGI-ON1R?usp=sharing) (or locally in `/ecg_data/` after running the `readme.Rmd` script).
To generate/update the GitHub Pages landing page, run `index.R` after knitting `readme.Rmd`.
# Aims and summary
- This project aimed to train a neural network to be able to predict the risk of an individual having prevalent diabetic neuropathy, using nothing but a ~~standard 12-lead~~ 10 second ECG of two non-standard V1/V2 and V5/V6 leads.
- By combining four publicly available PhysioNet datasets, data is available on a total of roughly 100 ECGs from 90 individuals with diabetes, who have provided data on neuropathy status. However, ECG data from two of these datasets (a third of the total ECG records) is recorded during vasoregulatory stress testing experiments, and were deemed inappropriate for our use. Thus, the final dataset consisted of 60 individuals with diabetes from two PhysioNet datasets, with 24 cases of prevalent diabetic neuropathy among these individuals.
- Data augmentation was mainly done in the way of splitting the ECG signals into many images of 10-second snippets, in addition to item and batch transforming operations (random resizing and cropping, etc.).
-We trained the same model on different training/validation splits to account for potential data leakage on the individual and experiment dataset level.
- A ResNet18 computer vision model was transferred and trained on these data, but model performance was poor, and very prone to over-fitting, with a validation loss > 0.80 and increasing with every epoch of training beyond the first. Validation loss was highest in the model with training/validation split on experiment dataset level, indicating that data leakage only this level may be worth accounting for.
- Further work: Particularly challenging was the high level of noise in the ECG data. Had more time been available, the next logical step to improve model performance would be to filter out noisy ECG snippets, or even better, train the model as a 3-label classifier (neuropathy, healthy, noise).
Poster presentation:

# Data sources
## Overview
- **Cerebromicrovascular Disease in Elderly with Diabetes**
- Abbreviated: **CDED**
- Link: <https://physionet.org/content/cded/1.0.0/>
- Folder: `/raw_csv_data/GE-79/`
- Contents: 69 participants age 55-75 with or without diabetes
- **Cerebral perfusion and cognitive decline in type 2 diabetes**
- Abbreviated: **CPD**
- Link: <https://physionet.org/content/cerebral-perfusion-diabetes/1.0.0/>
- Folder: `/raw_csv_data/GE-75/`
- Contents: 140 participants age 50-85 years, 70 with type 2 diabetes + 70 without.
- **Cerebral Vasoregulation in Elderly with Stroke**
- Abbreviated: **CVES**
- Link: <https://www.physionet.org/content/cves/1.0.0/>
- Folder: `/raw_csv_data/GE-72/`
- Contents: 120 participants, 60 with stroke, 60 without. Very few with diabetes.
- **Cerebral Vasoregulation in Diabetes**
- Abbreviated: **CVD**
- Link: <https://physionet.org/content/cerebral-vasoreg-diabetes/1.0.0/>
- Folder: `/raw_csv_data/GE-71/`
- Contents: 86 participants age 55-75 years, 37 with type 2 diabetes + 49 without.
```{r, message=FALSE}
# Required packages:
library(dplyr)
library(data.table)
library(here)
library(snakecase)
library(stringr)
library(fs)
# Load tabular data:
# From "Cerebromicrovascular Disease in Elderly with Diabetes" ("GE-79"):
# https://physionet.org/content/cded/1.0.0/
cded_data <-
lapply(list.files(here("raw_csv_data", "GE-79"), full.names = T), fread, stringsAsFactors = F)
# From "Cerebral perfusion and cognitive decline in type 2 diabetes" ("GE-75"):
# https://physionet.org/content/cerebral-perfusion-diabetes/1.0.0/
cpd_data <-
lapply(list.files(here("raw_csv_data", "GE-75"), full.names = T), fread, stringsAsFactors = F)
# From "Cerebral Vasoregulation in Diabetes" ("GE-71"):
# https://physionet.org/content/cerebral-vasoreg-diabetes/1.0.0/
cvd_data <-
lapply(list.files(here("raw_csv_data", "GE-71"), full.names = T), fread, stringsAsFactors = F)
# From: "Cerebral Vasoregulation in Elderly with Stroke" ("GE-72"):
# https://www.physionet.org/content/cves/1.0.0/
cves_data <-
fread(list.files(here("raw_csv_data", "GE-72"), full.names = T), stringsAsFactors = F)
```
## Actual size of usable data
The above contents are what the documentation describes. That does not match the size of the tabular data actually in the datasets, and some individuals may be present in more than one dataset.
### Unique subjects in each dataset and in a combined dataset:
- CDED: 82
- CPD: 88
- CVES: 172
- CVD: 86
- Combined: 391
```{r, results='hide'}
# Unique subjects in each dataset:
# CDED:
length(unique(cded_data[[3]]$`Subject ID`))
# CPD:
length(unique(cpd_data[[4]]$`Subject ID`))
# CVES:
length(unique(cves_data$subject_number))
# CVD:
length(unique(cvd_data[[4]]$`Subject ID`))
# Unique subjects in total:
length(unique(toupper(
c(
cded_data[[3]]$`Subject ID`,
cpd_data[[4]]$`Subject ID`,
cves_data$subject_number,
cvd_data[[4]]$`Subject ID`
)
)))
```
### Unique subjects with ECG data available
All four datasets include data on whether ECG data is missing or not. In the CDED and CPD datasets, this is described with an explicit variable. In the CVES and CVD datasets, we're making a qualified guess based on whether that person completed the visit where ECGs were performed:
- CDED: 47
- CPD: 51
- CVES: 91
- CVD: 57
- Combined: 220
```{r, results='hide'}
# Unique subjects in each dataset with ECG data:
# CDED:
length(unique(cded_data[[3]][ECG == 1]$`Subject ID`))
# CPD:
length(unique(cpd_data[[4]][ECG == 1]$`Subject ID`))
# CVES:
length(unique(cves_data[completed_visit_status == "COMPLETED"]$subject_number))
# CVD:
length(unique(cvd_data[[4]][`Head Up Tilt D2` == 1]$`Subject ID`))
# Unique subjects in total:
length(unique(toupper(
c(
cded_data[[3]][ECG == 1]$`Subject ID`,
cpd_data[[4]][ECG == 1]$`Subject ID`,
cves_data[completed_visit_status == "COMPLETED"]$subject_number,
cvd_data[[4]][`Head Up Tilt D2` == 1]$`Subject ID`
)
)))
```
### Unique subjects in each dataset with ECG data, who have diabetes
All four datasets provide data on diabetes status. Note that these individuals may provide more than one ECG, e.g. if ECGs are performed at baseline and at follow-up:
- CDED: 22
- CPD: 45
- CVES: 2
- CVD: 29
- Combined: 90
```{r, results='hide'}
# Unique subjects in each dataset with ECG data, who have diabetes:
# CDED:
length(unique(cded_data[[3]][ECG == 1 & toupper(`Subject ID`) %in% toupper(cded_data[[6]][`DM PATIENT MEDICAL HISTORY` == "YES"]$`patient ID`)]$`Subject ID`))
# CPD:
length(unique(cpd_data[[4]][ECG == 1 & Group == "DM"]$`Subject ID`))
# CVES:
length(unique(cves_data[completed_visit_status == "COMPLETED" & `DM PATIENT MEDICAL HISTORY` %in% c("yes", "YES")]$subject_number))
# CVD:
length(unique(cvd_data[[4]][`Head Up Tilt D2` == 1 & Group %in% c("DM", "DMOH")]$`Subject ID`))
# Unique subjects in total:
length(unique(toupper(
c(
cded_data[[3]][ECG == 1 &
toupper(`Subject ID`) %in% toupper(cded_data[[6]][`DM PATIENT MEDICAL HISTORY` == "YES"]$`patient ID`)]$`Subject ID`,
cpd_data[[4]][ECG == 1 & Group == "DM"]$`Subject ID`,
cves_data[completed_visit_status == "COMPLETED" &
`DM PATIENT MEDICAL HISTORY` %in% c("yes", "YES")]$subject_number,
cvd_data[[4]][`Head Up Tilt D2` == 1 &
Group %in% c("DM", "DMOH")]$`Subject ID`
)
)))
```
### Participant overlap between datasets
Overlap in participants (with ECG data and diabetes) between the datasets is limited to 8 participants in CDED, who are also present in CPD (7) and CVD (1).
```{r, results='hide'}
### Overlap between cded and cpd/cves/cvd:
# This could have been done more elegant, but bear with me)
# cded vs cpd: 7: ("S0296" "S0301" "S0308" "S0314" "S0318" "S0372" "S0430"):
cded_data[[3]][ECG == 1 &
toupper(`Subject ID`) %in% toupper(cded_data[[6]][`DM PATIENT MEDICAL HISTORY` == "YES"]$`patient ID`) &
toupper(`Subject ID`) %in% toupper(cpd_data[[4]][ECG == 1 &
Group == "DM"]$`Subject ID`)]
# cded vs. cves: 0:
nrow(cded_data[[3]][ECG == 1 &
toupper(`Subject ID`) %in% toupper(cded_data[[6]][`DM PATIENT MEDICAL HISTORY` == "YES"]$`patient ID`) &
toupper(`Subject ID`) %in% toupper(cves_data[completed_visit_status == "COMPLETED" &
`DM PATIENT MEDICAL HISTORY` %in% c("yes", "YES")]$subject_number)])
# cded vs. cvd: 1 ("S0105"):
cded_data[[3]][ECG == 1 &
toupper(`Subject ID`) %in% toupper(cded_data[[6]][`DM PATIENT MEDICAL HISTORY` == "YES"]$`patient ID`) &
toupper(`Subject ID`) %in% toupper(cvd_data[[4]][`Head Up Tilt D2` == 1 &
Group %in% c("DM", "DMOH")]$`Subject ID`)]
### No overlap between cpd and cves/cvd
# cpd vs. cves: 0:
nrow(cpd_data[[4]][ECG == 1 &
Group == "DM" &
toupper(`Subject ID`) %in% toupper(cves_data[completed_visit_status == "COMPLETED" &
`DM PATIENT MEDICAL HISTORY` %in% c("yes", "YES")]$subject_number)])
# cpd vs. cvd: 0
nrow(cpd_data[[4]][ECG == 1 &
Group == "DM" &
toupper(`Subject ID`) %in% toupper(cvd_data[[4]][`Head Up Tilt D2` == 1 &
Group %in% c("DM", "DMOH")]$`Subject ID`)])
### No overlap between cves and cvd: 0
nrow(cves_data[completed_visit_status == "COMPLETED" &
`DM PATIENT MEDICAL HISTORY` %in% c("yes", "YES") &
toupper(subject_number) %in% toupper(cvd_data[[4]][`Head Up Tilt D2` == 1 &
Group %in% c("DM", "DMOH")]$`Subject ID`)])
```
# Study dataset: Individuals in CDED and CPD with diabetes, and data on ECG and neuropathy
For the models, we combined CDED (data from baseline visit) and CPD datasets, excluding records in the CPD data from the 7 individuals already present in the CDED dataset and leaving a final study population of 60 individuals.
## Prevalence of diabetic neuropathy
The protocol states that neuropathy in the CDED dataset was diagnosed at some point using the validated symptom scale [neuropathy total symptom score-6](https://doi.org/10.1016/j.clinthera.2005.08.002), but the available variables do not correspond to this.
Both CDED and CPD contain questionnaire data on numbness and painful sensations of the feet. The CPD dataset also contains an item on autonomic neuropathy symptoms, although it is unclear what specific symptoms this item covers.
We defined diabetic neuropathy as a binary variable on the individual level as the presence of at least one of these symptoms. Individuals with missing data on all neuropathy items were excluded, while cases with missing data on only some items were interpreted as having no symptoms of these types.
### Final dataset
Using the above method, 24 cases of neuropathy were identified among the 60 individuals in the study population, corresponding to a prevalence around 40% in both datasets (6 of 15 individuals from CDED, 19 of 45 from CPD).
```{r, results='hide'}
## Define nephropathy in each dataset:
### CPD:
# Clean column names and subject ID's:
names(cpd_data[[2]]) <- to_snake_case(names(cpd_data[[2]]))
cpd_data[[2]]$patient_id <- toupper(cpd_data[[2]]$patient_id)
# Filtering to only id, diabetes status and the three neuropathy variables on numbness and pain :
cpd_data_vars <-
cpd_data[[2]][, .(
patient_id,
dm_patient_medical_history,
neuropathy_autonomic_symptoms,
numbness_autonomic_symptoms,
painful_feet_autonomic_symptoms
)]
# Recode string data to binary and NAs:
binary_converter_function <- function(x) {
case_when(x == "N/A" ~ NA,
x == "YES" | x == "yes" | x == "Yes" ~ TRUE,
x == "NO" | x == "no" | x == "No" ~ FALSE)
}
# Recode string data to binary and NAs:
mod_cols = names(cpd_data_vars)[2:5]
cpd_data_vars[, (mod_cols) := lapply(.SD, binary_converter_function), .SDcols = mod_cols]
# Create the simpler neuropathy outcome variable:
# neuropathy is defined as the presence of either neuropathy, or numbness or pain in the feet:
cpd_data_vars[, neuropathy_outcome := apply(cpd_data_vars[, 3:5], 1, function(x)
sum(x, na.rm = T)) >= 1]
# Set individuals with completely missing data to NA:
cpd_data_vars[, no_neuropathy_data := apply(cpd_data_vars[, 3:5], 1, function(x)
sum(is.na(x))) == 3]
cpd_data_vars[, neuropathy_outcome := fifelse(no_neuropathy_data == T, NA, neuropathy_outcome)]
# rename diabetes variable and dataset variable for convenience:
names(cpd_data_vars)[2] <- "diabetes"
cpd_data_vars[, dataset := "cpd"]
# Clean CPD dataset (individuals with diabetes, and ECG/neuropathy-data):
cpd_clean <-
cpd_data_vars[diabetes == T &
!is.na(neuropathy_outcome) &
patient_id %in% toupper(cpd_data[[4]][ECG == 1]$`Subject ID`), c(1, 6, 8)]
### CDED:
#### Make column names prettier for future use and clean case inconsistency in ID variable:
names(cded_data[[6]]) <- to_snake_case(names(cded_data[[6]]))
cded_data[[6]]$patient_id <- toupper(cded_data[[6]]$patient_id)
# Filter to variables needed:
cded_survey <-
cded_data[[6]][, .(
patient_id,
visit,
dm_patient_medical_history,
numbness_autonomic_symptoms,
painful_feet_autonomic_symptoms
)]
# Select columns to be modified
mod_cols = names(cded_survey)[3:5]
cded_survey[, (mod_cols) := lapply(.SD, binary_converter_function), .SDcols = mod_cols]
# Create a simple neuropathy variable:
# neuropathy is defined as the presence of either numbness or pain in the feet:
# The few cases of missing data in a symptom variable is treated as no symptom of this kind.
cded_survey[, neuropathy_outcome := apply(cded_survey[, 4:5], 1, function(x)
sum(x, na.rm = T)) >= 1]
# Set individuals with completely missing data to NA:
cded_survey[, no_neuropathy_data := apply(cded_survey[, 4:5], 1, function(x)
sum(is.na(x))) == 2]
cded_survey[, neuropathy_outcome := fifelse(no_neuropathy_data == T, NA, neuropathy_outcome)]
# Rename diabetes variable for convenience:
names(cded_survey)[3] <- "diabetes"
# Add variable to keep track of which dataset overlapping individuals came from:
cded_survey[, dataset := "cded"]
# Clean CDED dataset (visit 2 data from individuals with diabetes and ECG/neuropathy-data, not in CPD):
cded_clean <-
cded_survey[visit == 2 &
diabetes == T &
!is.na(neuropathy_outcome) &
patient_id %in% toupper(cded_data[[3]][ECG == 1]$`Subject ID`) &
!patient_id %in% cpd_clean$patient_id, c(1, 6, 8)]
# Merge to one dataset and count neuropathy cases:
neuropathy_final <- rbind(cded_clean, cpd_clean)
nrow(neuropathy_final[dataset == "cded"])
nrow(neuropathy_final[dataset == "cded" & neuropathy_outcome == T])
nrow(neuropathy_final[dataset == "cpd"])
nrow(neuropathy_final[dataset == "cpd" & neuropathy_outcome == T])
```
# Export tabular data and ECG data
## Final cleaning of tabular data:
Append patient id variable to match ECG data file names: 'S' + ID + 'ECG'
The final dataset looks like this before exporting to a csv file:
```{r}
# Append ID's:
study_dataset <-
neuropathy_final[, .(
patient_id = paste0(patient_id, "ECG"),
dataset = factor(dataset),
neuropathy_outcome
)]
# Export dataset
fwrite(study_dataset, file = here("output_data", "study_dataset.csv"))
# Summary and contents:
summary(study_dataset)
study_dataset
```
## Filter, split and export ECG files:
To save space and computation time, we filter the ECGs to only the ones we need, and export them to different folders for labelling purposes. We'll also split the ECGs into training and validation parent folders, so ECGs from the same experiment or individual cannot be present in both training a validation datasets (we'll be splitting the ECGs into small snippets later, so each individual will contribute multiple ECGs). Otherwise we risk [data leakage](https://en.wikipedia.org/wiki/Leakage_(machine_learning)) between the training and validation datasets, and the model might learn to identify *individuals* or *experiment*, rather than signals of *neuropathy*, which would erode model performance on external data. A somewhat famous example of this mistake being [Andrew Ng's random split of 112,120 x-ray images from 30,805 individuals](https://twitter.com/nizkroberts/status/931121395748270080) which was subsequently corrected.
Fortunately, the CPD and CDED datasets are similarly balanced in terms of neuropathy prevalence, and their relative sizes are suitable for use as a training/validation split (the CDED participants make up 25% of the study population). Due to the limited data available, we do not set aside a test dataset, but expect performance on external datasets to be relatively stable due to the different sources of training and validation set.
```{r}
# Specify local source folder of CDED and CPD ECG data:
cded_ecg_folder <-
"C:/physionet/cded/cerebromicrovascular-disease-in-elderly-with-diabetes-1.0.0/Data/ECG/"
cpd_ecg_folder <- "C:/physionet/cpd/data/ecg"
# List ECG files of all patients in study population
cded_files <- list.files(cded_ecg_folder,
full.names = T)
cpd_files <- list.files(cpd_ecg_folder,
full.names = T)
# Filter files of each dataset to only subjects in study population and split into groups based on neuropathy status:
# CDED:
cded_healthy <-
cded_files[str_sub(cded_files, -12, -5) %in% study_dataset[dataset == "cded" &
neuropathy_outcome == FALSE]$patient_id]
cded_neuropathy <-
cded_files[str_sub(cded_files, -12, -5) %in% study_dataset[dataset == "cded" &
neuropathy_outcome == TRUE]$patient_id]
# CPD:
cpd_healthy <-
cpd_files[str_sub(cpd_files, -12, -5) %in% study_dataset[dataset == "cpd" &
neuropathy_outcome == FALSE]$patient_id]
cpd_neuropathy <-
cpd_files[str_sub(cpd_files, -12, -5) %in% study_dataset[dataset == "cpd" &
neuropathy_outcome == TRUE]$patient_id]
```
In this fashion, we end up with a training set containing 45 individuals, and a validation set containing 15 individuals.
Training set:
```{r}
unique(str_sub(
c(
cpd_healthy,
cpd_neuropathy
),
-12,
-5
))
```
Validation set:
```{r}
unique(str_sub(
c(
cded_healthy,
cded_neuropathy
),
-12,
-5
))
cded_healthy
```
Since the proportion of neuropathy is balanced between the two datasets, we also train a model on a random 20/80 split for comparison (due to the balanced proportions, the risk of data leakage inflating performance when training the model across both datasets shouldn't be critical).
```{r}
# Alternative random split:
# Sample 1 in 5 of all participants to validation dataset:
all_participant_files <- c(cded_healthy, cded_neuropathy, cpd_healthy, cpd_neuropathy)
all_healthy <- all_participant_files[str_sub(all_participant_files, -12, -5) %in% study_dataset[neuropathy_outcome == FALSE]$patient_id]
all_neuropathy <- all_participant_files[str_sub(all_participant_files, -12, -5) %in% study_dataset[neuropathy_outcome == TRUE]$patient_id]
# Set group 2 seed for reproducibility:
set.seed(2)
valid_healthy <- all_participant_files[str_sub(all_participant_files, -12, -5) %in% sample(study_dataset[neuropathy_outcome == FALSE]$patient_id, 0.20 * nrow(study_dataset[neuropathy_outcome == FALSE]))]
valid_neuropathy <- all_participant_files[str_sub(all_participant_files, -12, -5) %in% sample(study_dataset[neuropathy_outcome == TRUE]$patient_id, 0.20 * nrow(study_dataset[neuropathy_outcome == TRUE]))]
# And remove the validation individuals from the training set:
train_healthy <- all_healthy[!all_healthy %in% valid_healthy]
train_neuropathy <- all_neuropathy[!all_neuropathy %in% valid_neuropathy]
```
For labeling purposes, training set ECGs from individuals with neuropathy go to the `/ecg_wfdb/train/neuropathy/` folder, and those without neuropathy go to the `/ecg_wfdb/train/healthy/` folder. Conversely, validation set ECGs go to their respective `/ecg_wfdb/valid/neuropathy/` and `/ecg_wfdb/valid/healthy/` folders. Like so:
```
/ecg_wfdb
├── /train
│ ├── /healthy/
│ └── /neuropathy/
└── /valid
├── /healthy/
└── /neuropathy/
```
Note that the ECG data files aren't tracked in Git, so you'll have to download the datasets from PhysioNet to reproduce this.
```{r, results='hide'}
# Copy these files to either /healthy/ or /neuropathy/ folders based on neuropathy status:
# Create the folders
ecg_folders <- c("ecg_wfdb", "ecg_wfdb_randomsplit")
split_folders <- c(rep("train", 2), rep("valid", 2))
label_folders <- c(rep("healthy", 4), rep("neuropathy", 4))
path <- here(ecg_folders, split_folders, label_folders)
dir_create(path)
# Split by source dataset:
# Training set:
# Healthy:
file.copy(from = cpd_healthy, to = here("ecg_wfdb", "train", "healthy"))
# Neuropathy:
file.copy(from = cpd_neuropathy, to = here("ecg_wfdb", "train", "neuropathy"))
# Validation set:
# Healthy:
file.copy(from = cded_healthy, to = here("ecg_wfdb", "valid", "healthy"))
# Neuropathy:
file.copy(from = cded_neuropathy, to = here("ecg_wfdb", "valid", "neuropathy"))
# Random split:
# Training set:
# Healthy:
file.copy(from = train_healthy, to = here("ecg_wfdb_randomsplit", "train", "healthy"))
# Neuropathy:
file.copy(from = train_neuropathy, to = here("ecg_wfdb_randomsplit", "train", "neuropathy"))
# Validation set:
# Healthy:
file.copy(from = valid_healthy, to = here("ecg_wfdb_randomsplit", "valid", "healthy"))
# Neuropathy:
file.copy(from = valid_neuropathy, to = here("ecg_wfdb_randomsplit", "valid", "neuropathy"))
```
# Off to Python and Google Colab!
Now, we have no further use of the tabular data file, since all the information needed to run the model is contained in the filename and path of the ECG data itself at this point (neuropathy label in the folder name, ECG ID in the file name).
The rest of the data processing is carried out in Python on Google Colab. [Preprocessing here](https://drive.google.com/file/d/1L9HoCpwvcAg84XY1d-mqiYqTXxO004uY/view?usp=sharing) ([GitHub copy here](https://github.com/PandaPowell/ECG_DDA/commit/6fca794c8af1c795940e54035a5c97377d7ecea3)), [transfer learning here](https://colab.research.google.com/drive/1_b-j3hDzYTYbbqGdyUnoaGiPZyRMbcJE?usp=sharing) ([GitHub copy here](https://github.com/PandaPowell/ECG_DDA/commit/a610c3064cb493c10bcb307813418244d6eb81d4)), and involves reading the ECG data's waveform signals, extracting the two ECG leads and splitting them into hundreds of 10 second snippets saved as separate image files, which are then loaded into fastai DataLoader objects to train a ResNet model.
**See you on the other side!**