library(readxl)
library(dplyr)
##
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
library(janitor)
##
## Attaching package: 'janitor'
## The following objects are masked from 'package:stats':
##
## chisq.test, fisher.test
file_path <- "Clinical data for Proteomics data_original.xlsx"
clinical_raw <- read_excel(file_path, sheet = "Sheet3") %>%
clean_names()
dim(clinical_raw)
## [1] 233 49
The accompanying feature dictionary (Sheet2) lists 60
candidate clinical features. The actual dataset (Sheet3)
contains 49 columns. Comparing the two directly: 2 columns present in
the data are identifiers not covered by the dictionary
(Patient.ID, Linked ID), and 13 features
listed in the dictionary were not present in this extract, predominantly
the HIV sub-panel (CD4%, CD4 count, viral load, CD45+ count), tumour
markers (PSA, CEA), pancreatic enzymes (amylase, lipase), and two
additional infectious disease screens (Hepatitis A, syphilis). Core
panels: full blood count, blood chemistry, and liver function tests are
complete. This is consistent with these being optional/specialised tests
not administered to every patient in the study, rather than a
data-loading error; the count (60 − 13 + 2 = 49) reconciles exactly with
dim(clinical_raw).
clinical_raw %>% tabyl(pathology)
## pathology n percent
## BBP 83 0.35622318
## CP 6 0.02575107
## HC 42 0.18025751
## LAPC 13 0.05579399
## MPC 10 0.04291845
## RPC 79 0.33905579
No whitespace-related duplicate categories were found -
readxl::read_excel() trims whitespace from character
columns by default (trim_ws = TRUE), which already resolved
a spacing inconsistency present in the raw Excel file
("RPC " vs "RPC", "LAPC " vs
"LAPC") before this check ran.
missing_summary <- clinical_raw %>%
summarise(across(everything(), ~ sum(is.na(.)))) %>%
tidyr::pivot_longer(everything(), names_to = "variable", values_to = "n_missing") %>%
mutate(pct_missing = round(100 * n_missing / nrow(clinical_raw), 1)) %>%
arrange(desc(n_missing))
missing_summary
## # A tibble: 49 × 3
## variable n_missing pct_missing
## <chr> <int> <dbl>
## 1 date_of_death 197 84.5
## 2 followup 195 83.7
## 3 hiv_status 193 82.8
## 4 smoking 193 82.8
## 5 drinker 193 82.8
## 6 employment_status 193 82.8
## 7 date_of_last_visit 193 82.8
## 8 hepatitis_b 191 82
## 9 hepatitis_c 191 82
## 10 dead_alive 175 75.1
## # ℹ 39 more rows
clinical_raw %>%
select(patient_id, hiv_status, smoking, drinker, employment_status, date_of_last_visit) %>%
mutate(all_present = if_all(-patient_id, ~ !is.na(.))) %>%
count(all_present)
## # A tibble: 2 × 2
## all_present n
## <lgl> <int>
## 1 FALSE 193
## 2 TRUE 40
class(clinical_raw$date_of_test)
## [1] "character"
clinical_raw %>% count(is.na(date_of_test))
## # A tibble: 2 × 2
## `is.na(date_of_test)` n
## <lgl> <int>
## 1 FALSE 170
## 2 TRUE 63
missing_summary
clinical_raw %>%
filter(!is.na(date_of_test)) %>%
distinct(date_of_test) %>%
slice_head(n = 15)
## # A tibble: 15 × 1
## date_of_test
## <chr>
## 1 43593
## 2 43609
## 3 43655
## 4 43683
## 5 43626
## 6 43634
## 7 43642
## 8 43705
## 9 43662
## 10 43671
## 11 43711
## 12 43714
## 13 43742
## 14 43760
## 15 43851
clinical_clean <- clinical_raw %>%
mutate(
date_of_test_parsed = case_when(
grepl("^[0-9]+$", date_of_test) ~ janitor::excel_numeric_to_date(as.numeric(date_of_test)),
TRUE ~ lubridate::dmy(date_of_test)
)
)
## Warning: There were 2 warnings in `mutate()`.
## The first warning was:
## ℹ In argument: `date_of_test_parsed = case_when(...)`.
## Caused by warning in `janitor::excel_numeric_to_date()`:
## ! NAs introduced by coercion
## ℹ Run `dplyr::last_dplyr_warnings()` to see the 1 remaining warning.
# sanity check: did every non-missing original value get a real date?
clinical_clean %>%
filter(!is.na(date_of_test), is.na(date_of_test_parsed)) %>%
select(patient_id, date_of_test, date_of_test_parsed)
## # A tibble: 0 × 3
## # ℹ 3 variables: patient_id <chr>, date_of_test <chr>,
## # date_of_test_parsed <date>
saveRDS(clinical_clean, "clinical_clean.rds")
list.files(pattern = ".rds")
## [1] "clinical_clean.rds" "demographics_age_summary.rds"
## [3] "demographics_gender_summary.rds" "harmonized_metabolomics.rds"
## [5] "harmonized_multiomics_dataset.rds" "harmonized_proteomics.rds"
## [7] "matched_cohort_ids.rds" "metabolomics_clean.rds"
## [9] "pathology_mismatches.rds" "proteomics_clean.rds"
## [11] "table1_clinical_parameters.rds"
clinical_raw %>% tabyl(bilirubin_index)
## bilirubin_index n percent valid_percent
## 1+ 33 0.14163090 0.3707865
## 2+ 21 0.09012876 0.2359551
## 3+ 10 0.04291845 0.1123596
## none 11 0.04721030 0.1235955
## trace 14 0.06008584 0.1573034
## <NA> 144 0.61802575 NA
clinical_clean <- clinical_clean %>%
mutate(bilirubin_index_clean = trimws(as.character(bilirubin_index)))
# flag any value that isn't a recognised grade
clinical_clean %>%
filter(!bilirubin_index_clean %in% c("none", "trace", "1+", "2+", "3+", NA)) %>%
select(patient_id, bilirubin_index_clean)
## # A tibble: 0 × 2
## # ℹ 2 variables: patient_id <chr>, bilirubin_index_clean <chr>
clinical_raw %>% count(followup == "#VALUE!")
## # A tibble: 2 × 2
## `followup == "#VALUE!"` n
## <lgl> <int>
## 1 FALSE 38
## 2 NA 195
clinical_raw %>%
filter(patient_id %in% c("CHBDK167", "CHBDK174")) %>% # placeholder check, see below
select(patient_id, followup)
## # A tibble: 2 × 2
## patient_id followup
## <chr> <dbl>
## 1 CHBDK167 NA
## 2 CHBDK174 NA
clinical_clean <- clinical_clean %>%
mutate(followup_missing_reason = case_when(
patient_id %in% c("RPC 22", "CP 1") ~ "Excel formula error (#VALUE!) in source file",
is.na(followup) ~ "Not part of follow-up sub-study",
TRUE ~ NA_character_
))
clinical_clean %>% count(followup_missing_reason)
## # A tibble: 3 × 2
## followup_missing_reason n
## <chr> <int>
## 1 Excel formula error (#VALUE!) in source file 2
## 2 Not part of follow-up sub-study 193
## 3 <NA> 38
class(clinical_raw$date_of_death)
## [1] "character"
class(clinical_raw$date_of_last_visit)
## [1] "character"
clinical_raw %>%
filter(!is.na(date_of_death)) %>%
distinct(date_of_death) %>%
slice_head(n = 5)
## # A tibble: 5 × 1
## date_of_death
## <chr>
## 1 43709
## 2 44039
## 3 43656
## 4 43877
## 5 43754
clinical_raw %>%
filter(!is.na(date_of_last_visit)) %>%
distinct(date_of_last_visit) %>%
slice_head(n = 5)
## # A tibble: 5 × 1
## date_of_last_visit
## <chr>
## 1 43488
## 2 44013
## 3 43837
## 4 43655
## 5 43650
clinical_clean <- clinical_clean %>%
mutate(
date_of_death_parsed = case_when(
grepl("^[0-9]+$", date_of_death) ~ janitor::excel_numeric_to_date(as.numeric(date_of_death)),
TRUE ~ lubridate::dmy(date_of_death)
),
date_of_last_visit_parsed = case_when(
grepl("^[0-9]+$", date_of_last_visit) ~ janitor::excel_numeric_to_date(as.numeric(date_of_last_visit)),
TRUE ~ lubridate::dmy(date_of_last_visit)
)
)
## Warning: There were 4 warnings in `mutate()`.
## The first warning was:
## ℹ In argument: `date_of_death_parsed = case_when(...)`.
## Caused by warning in `janitor::excel_numeric_to_date()`:
## ! NAs introduced by coercion
## ℹ Run `dplyr::last_dplyr_warnings()` to see the 3 remaining warnings.
# sanity check both columns again
clinical_clean %>%
filter(!is.na(date_of_death), is.na(date_of_death_parsed)) %>%
select(patient_id, date_of_death, date_of_death_parsed)
## # A tibble: 0 × 3
## # ℹ 3 variables: patient_id <chr>, date_of_death <chr>,
## # date_of_death_parsed <date>
clinical_clean %>%
filter(!is.na(date_of_last_visit), is.na(date_of_last_visit_parsed)) %>%
select(patient_id, date_of_last_visit, date_of_last_visit_parsed)
## # A tibble: 0 × 3
## # ℹ 3 variables: patient_id <chr>, date_of_last_visit <chr>,
## # date_of_last_visit_parsed <date>
saveRDS(clinical_clean, "clinical_clean.rds")
list.files(pattern = ".rds")
## [1] "clinical_clean.rds" "demographics_age_summary.rds"
## [3] "demographics_gender_summary.rds" "harmonized_metabolomics.rds"
## [5] "harmonized_multiomics_dataset.rds" "harmonized_proteomics.rds"
## [7] "matched_cohort_ids.rds" "metabolomics_clean.rds"
## [9] "pathology_mismatches.rds" "proteomics_clean.rds"
## [11] "table1_clinical_parameters.rds"
clinical_clean <- clinical_clean %>%
mutate(date_of_test_parsed = if_else(
patient_id == "CHBDK174",
as.Date("2022-09-30"),
date_of_test_parsed
))
# confirm the fix applied correctly
clinical_clean %>%
filter(patient_id == "CHBDK174") %>%
select(patient_id, date_of_test, date_of_test_parsed)
## # A tibble: 1 × 3
## patient_id date_of_test date_of_test_parsed
## <chr> <chr> <date>
## 1 CHBDK174 30/09/2022 2022-09-30