Создание и обработка медицинской базы данных с помощью python/R
Идея
Реализация
Результат
Идея: в медицинском учреждении выписные эпикризы (информация из истории болезни) пациентов хранятся в общегоспитальной локальной сети.
Необходимо сформировать базу данных пациентов с перенесенным заболеванием COVID-19 (один выписной эпикриз ДО заболевания COVID-19, один выписной эпикриз во время заболевания и один ПОСЛЕ заболевания).
Вот как это выглядит
Реализация:
Сформированы папки с файлами
Формирование базы:
с помощью модуля docx на Python можно перевести *.docx файл в обычный текст, называем этот скрипт readDocx.py (решение найдено на просторах интернета):
import docx
def getText(filename):
doc = docx.Document(filename)
fullText = []
for para in doc.paragraphs:
fullText.append((' ' + para.text))
return '\n'.join(fullText)
Проблемы с которыми я столкнулся: некоторые файлы были в старом формате *.doc и *.rtf. для решения пришлось установить Libre Office и применить следующие команды в Терминале:
/Applications/LibreOffice.app/Contents/MacOS/soffice --headless --convert-to docx --outdir ~/Desktop/output ~/Desktop/COVID-2019/**/*.doc
/Applications/LibreOffice.app/Contents/MacOS/soffice --headless --convert-to docx --outdir ~/Desktop/output ~/Desktop/COVID-2019/**/*.rtf
Для получения базы каждый файл в директории переводится в текстовый формат, затем с помощью регулярных выражений требуемые показатели находятся и формируется общая таблица в формате *.xlsx (Exel):
import os, readDocx, re, pandas as pd
ROOT_DIR = r'/Users/insomnia/Documents/disser/COVID-2019'
docx_files = []
for root, dirs, files in os.walk(ROOT_DIR):
for file in files:
if file.endswith(".docx"):
docx_files.append(os.path.join(root, file))
print(docx_files)
setoftuples = []
for i in docx_files:
x = readDocx.getText(i)
name = [i]
if re.search(r'\d\d[.]\d\d[.]\d{4}', x):
birthdate = re.search(r'\d\d[.]\d\d[.]\d{4}', x).group()
else:
birthdate = "NA"
if len(re.findall(r'\d\d[.]\d\d[.]\d{4}', x)) > 2:
admission = re.findall(r'\d\d[.]\d\d[.]\d{4}', x)[1]
discharge = re.findall(r'\d\d[.]\d\d[.]\d{4}', x)[2]
else:
admission = "NA"
discharge = "NA"
if re.findall(r'[Кк]орон[а]?[о]?вирусная инфекция', x):
COVID = re.findall(r'[Кк]орон[а]?[о]?вирусная инфекция', x)
else:
COVID = "NA"
if re.findall(r'КТ.?[0-4]', x):
CT = re.findall(r'КТ.?[0-4]', x)
else:
CT = "NA"
if re.findall(r'ПОСМЕРТНЫЙ|'
r'\bумер\b|'
r'\bсмерть\b', x):
Death = re.findall(r'ПОСМЕРТНЫЙ|'
r'\bумер\b|'
r'\bсмерть\b', x)
else:
Death = "NA"
if re.findall(r'фибрил\w+', x):
AF = re.findall(r'фибрил\w+', x)
else:
AF = "NA"
if re.findall(r'гемоглобин\s\S?\s?\d\d\d?|'
r'Hb\D?\D?\D?\d\d\d?', x):
hb = re.findall(r'гемоглобин\s\S?\s?\d\d\d?|'
r'Hb\D?\D?\D?\d\d\d?', x)
else:
hb = "NA"
if re.findall(r'Эр\w*\s?\S?\s?[0-9][.,][0-9]?[0-9]?|'
r'эр\w*\s?\S?\s?[0-9][.,][0-9]?[0-9]?', x):
RBC = re.findall(r'Эр\w*\s?\S?\s?[0-9][.,][0-9]?[0-9]?|'
r'эр\w*\s?\S?\s?[0-9][.,][0-9]?[0-9]?', x)
else:
RBC = "NA"
if re.findall(r'лейк\w+\s\S?\s?\d\d?[,.]\d?\d?|'
r'Л – \d\d?,?\.?\d?|'
r'Л-\d\d?,?\.?\d?|'
r'Le \d\d?,?\.?\d?', x):
leu = re.findall(r'лейк\w+\s\S?\s?\d\d?[,.]\d?\d?|'
r'Л – \d\d?,?\.?\d?|'
r'Л-\d\d?,?\.?\d?|'
r'Le \d\d?,?\.?\d?', x)
else:
leu = "NA"
if re.findall(r'лимф\w+\s?\S?\s?[0-9][.,][0-9]?[0-9?]?[0-9?]?', x):
limf = re.findall(r'лимф\w+\s?\S?\s?[0-9][.,][0-9]?[0-9?]?[0-9?]?', x)
else:
limf = "NA"
if re.findall(r'С.?реактивный белок\D*\d?\d?[.]?\d?\d?[.]?\d?\d?\d?\d?\D*\d\d?\d?[.,]\d?\d?|'
r'СРБ \D?\D? ?\d?\d.\d\d.\d\d\d?\d?\D*[0-9][0-9]?[0-9]?[.,][0-9]?[0-9]?|'
r'СРБ\D?\D?\D?\d\d?\d?\S?\d?\d?|'
r'С-реактивный белок – \d\d.\d\d.\d\d\d?\d? г. – \d\d?\d?\W?\d?\d?', x):
CRP = re.findall(r'С.?реактивный белок\D*\d?\d?[.]?\d?\d?[.]?\d?\d?\d?\d?\D*\d\d?\d?[.,]\d?\d?|'
r'СРБ \D?\D? ?\d?\d.\d\d.\d\d\d?\d?\D*[0-9][0-9]?[0-9]?[.,][0-9]?[0-9]?|'
r'СРБ\D?\D?\D?\d\d?\d?\S?\d?\d?|'
r'С-реактивный белок – \d\d.\d\d.\d\d\d?\d? г. – \d\d?\d?\W?\d?\d?', x)
else:
CRP = "NA"
if re.findall(r'[Хх]олестерин\D?\D?\D?\d\S?\d?\d?|'
r'[Хх]олестерин общий\D?\D?\D?\d\S?\d?\d?|'
r'[Cc]hol|CHOL\D?\D?\D?\d\S?\d?\d?', x):
chol = re.findall(r'[Хх]олестерин\D?\D?\D?\d\S?\d?\d?|'
r'[Хх]олестерин общий\D?\D?\D?\d\S?\d?\d?|'
r'[Cc]hol|CHOL\D?\D?\D?\d\S?\d?\d?', x)
else:
chol = "NA"
if re.findall(r'[Тт]риглицериды\D?\D?\D?\d\S?\d?\d?|'
r'TRIG[L]?\D?\D?\D?\d\S?\d?\d?|'
r'Триглицериды\D*\d\S?\d?\d?', x):
TG = re.findall(r'триглицериды\D?\D?\D?\d\S?\d?\d?|'
r'TRIG[L]?\D?\D?\D?\d\S?\d?\d?|'
r'Триглицериды\D*\d\S?\d?\d?', x)
else:
TG = "NA"
if re.findall(r'UHDL\D?\D?\D?\d\S?\d?\d?|'
r'ЛПВП\D?\D?\D?\d\S?\d?\d?', x):
UHDL = re.findall(r'UHDL\D?\D?\D?\d\S?\d?\d?|'
r'ЛПВП\D?\D?\D?\d\S?\d?\d?', x)
else:
UHDL = "NA"
if re.findall(r'DLDL\D?\D?\D?\d\S?\d?\d?|'
r'ЛПНП\D?\D?\D?\d\S?\d?\d?', x):
DLDL = re.findall(r'DLDL\D?\D?\D?\d\S?\d?\d?|'
r'ЛПНП\D?\D?\D?\d\S?\d?\d?', x)
else:
DLDL = "NA"
if re.findall(r'креатинин\D?\D?\D?\d\d?\d?\S?\d?', x):
crea = re.findall(r'креатинин\D?\D?\D?\d\d?\d?\S?\d?', x)
else:
crea = "NA"
if re.findall(r'КДРЛЖ\w?\s?\S?\s?\d\d\d?', x):
LVEDD = re.findall(r'КДРЛЖ\w?\s?\S?\s?\d\d\d?', x)
else:
LVEDD = "NA"
if re.findall(r'КС[Р]?ЛЖ\w?\s?\S?\s?\d\d\d?', x):
LVESD = re.findall(r'КС[Р]?ЛЖ\w?\s?\S?\s?\d\d\d?', x)
else:
LVESD = "NA"
if re.findall(r'КДОЛЖ\w?\s?\S?\s?\d\d\d?\d?', x):
LVEDV = re.findall(r'КДОЛЖ\w?\s?\S?\s?\d\d\d?\d?', x)
else:
LVEDV = "NA"
if re.findall(r'ИММЛЖ\w?\s?\S?\s?\d\d\d?|'
r'индекс массы\s?\S?\s?\d\d\d?', x):
LVMI = re.findall(r'ИММЛЖ\w?\s?\S?\s?\d\d\d?|'
r'индекс массы\s?\S?\s?\d\d\d?', x)
else:
LVMI = "NA"
if re.findall(r'ФВ[в]?[м?]?\s?\W?\s?[0-9]{2}', x):
EF = re.findall(r'ФВ[в]?[м?]?\s?\W?\s?[0-9]{2}', x)
else:
EF = "NA"
if re.findall(r'Систол\D?\s?ДЛА\s?\S?\s?[0-9]{2}|'
r'СДЛА\s?\S?\s?[0-9]{2}|'
r'Сист.ДЛА\s?\S?\s?[0-9]{2}', x):
PASP = re.findall(r'Систол\D?\s?ДЛА\s?\S?\s?[0-9]{2}|'
r'СДЛА\s?\S?\s?[0-9]{2}|'
r'Сист.ДЛА\s?\S?\s?[0-9]{2}', x)
else:
PASP = "NA"
if re.findall(r'ИОЛП.\s?\S?\s?[0-9]{2}', x):
LAVI = re.findall(r'ИОЛП.\s?\S?\s?[0-9]{2}', x)
else:
LAVI = "NA"
if re.findall(r'ИБС|'
r'[Ии]шемическая болезнь сердца', x):
IHD = re.findall(r'ИБС|'
r'[Ии]шемическая болезнь сердца', x)
else:
IHD = "NA"
if re.findall(r'[Ии]нфаркт миокарда|'
r'[Пп]остинфарктный', x):
MI = re.findall(r'[Ии]нфаркт миокарда|'
r'[Пп]остинфарктный', x)
else:
MI = "NA"
if re.findall(r'[Cc]ахарный диабет', x):
Diabetus = re.findall(r'[Cc]ахарный диабет', x)
else:
Diabetus = "NA"
if re.findall(r'[Оо]жирение', x):
Obesity = re.findall(r'[Оо]жирение', x)
else:
Obesity = "NA"
if re.findall(r'ХОБЛ', x):
COPD = re.findall(r'ХОБЛ', x)
else:
COPD = "NA"
if re.findall(r'[Бб]ронхиальная астма', x):
asthma = re.findall(r'[Бб]ронхиальная астма', x)
else:
asthma = "NA"
my_list = (name, birthdate, admission, discharge, COVID, CT, Death, hb, RBC, leu,
limf, CRP, chol, TG, UHDL, DLDL, crea, LVEDD,
LVESD, LVEDV, LVMI, EF, PASP, LAVI, AF, IHD, MI, Diabetus, Obesity,
COPD, asthma)
setoftuples.append(my_list)
print(setoftuples)
df = pd.DataFrame(list(setoftuples),
columns=['name','birthdate','admission', 'discharge', 'COVID',
'CT', 'Death', 'hb', 'RBC',
'leu', 'limf', 'CRP', 'chol', 'TG', 'UHDL', 'DLDL',
'crea', 'LVEDD', 'LVESD', 'LVEDV',
'LVMI', 'EF', 'PASP', 'LAVI', 'AF', 'IHD',
'MI', 'Diabetus', 'Obesity', 'COPD', 'asthma'])
print(df)
df.to_excel(r'/Users/insomnia/Documents/disser/dataframe.xlsx', index=False)
Промежуточный результат:
количество строк — 934, столбцов — 31
Полученные данные необходимо привести в формат с которым можно работать — очистить от мусора, задать каждой колонке тип данных, и проч. Этот процесс называется очисткой данных (Data cleaning).
В общих словах определение таково: Data cleaning is the process of fixing or removing incorrect, corrupted, incorrectly formatted, duplicate, or incomplete data within a dataset.
Для этого процесса я воспользовался программой RStudio IDE (язык R), можно было и на Python сделать, но R для меня удобнее. Постарался оставить комментарии на каждом из этапов:
library(openxlsx)
data <- read.xlsx('/Users/insomnia/Documents/disser/dataframe.xlsx')
library(tidyverse)
# initial data
glimpse(data)
### data cleaning process:
# name column
name <- data$name |> str_split("/")
name_unlisted <- unlist(lapply(name, '[[', 7)) # This returns a vector with the seven element
name_unlisted <- as.factor(name_unlisted) ###
lvls <- levels(name_unlisted) ###
levels(name_unlisted) <- seq_along(lvls) ###
data$name <- name_unlisted
data$name <- data$name |> as.factor()
data <- rename(data, patient_ID = name)
# 2,3,4 (time) columns
data <- data |> mutate(birthdate = dmy(birthdate),
admission = dmy(admission),
discharge = dmy(discharge))
data$admission[1]-data$birthdate[1]
#COVID column
covid <- data$COVID
covid <- lapply(covid, function(x) replace(x,!is.na(x),1))
covid <- lapply(covid, function(x) replace(x,is.na(x),0))
covid <- as.numeric(covid)
covid <- as.factor(covid)
data$COVID <- covid
# CT column
matches <- regmatches(data$CT, gregexpr("[[:digit:]]+", data$CT))
data$CT <- matches
data <- data |> rowwise() |> mutate(CT = max(CT)) |> ungroup()
data$CT <- as.numeric(data$CT)
#Death column
death <- data$Death
death <- lapply(death, function(x) replace(x,!is.na(x),1))
death <- lapply(death, function(x) replace(x,is.na(x),0))
death <- as.numeric(death)
data$Death <- death
# hb column
extracted_hb <- str_replace_all(data$hb,fixed(","), fixed("."))
extracted_hb <- str_extract_all(extracted_hb, pattern = "[0-9][0-9][0-9]?")
extracted_hb <- lapply(extracted_hb,as.numeric)
extracted_hb <- sapply(extracted_hb, mean, 0-20)
extracted_hb[906:913]
data$hb <- extracted_hb
# RBC column
extracted_RBC <- str_replace_all(data$RBC,fixed(","), fixed("."))
extracted_RBC <- str_extract_all(extracted_RBC, pattern = "[0-9][.]?[0-9]?[0-9]?")
extracted_RBC <- lapply(extracted_RBC,as.numeric)
extracted_RBC <- sapply(extracted_RBC, mean, 0-20)
extracted_RBC[906:913]
data$RBC <- extracted_RBC
# leu column
str(data)
extracted_leu <- str_replace_all(data$leu,fixed(","), fixed("."))
extracted_leu <- str_extract_all(extracted_leu, pattern = "[2-9][.]?[0-9]?|[1-3][0-9]?[.]?[0-9]?") # spread 2-22
extracted_leu <- lapply(extracted_leu,as.numeric)
extracted_leu <- sapply(extracted_leu, mean, 0-20)
extracted_leu[906:913]
data$leu <- extracted_leu
# limf column
extracted_limf <- str_replace_all(data$limf,fixed(","), fixed("."))
extracted_limf <- str_extract_all(extracted_limf, pattern = "[0-9][.]?[0-9]?") # spread 0-9
extracted_limf<- lapply(extracted_limf,as.numeric)
extracted_limf <- sapply(extracted_limf, min)
extracted_limf[906:913]
data$limf <- extracted_limf
data <- rename(data, limf_min = limf)
# CRP column
data$CRP[7]
extracted_CRP<- str_replace_all(data$CRP,fixed(","), fixed("."))
# Removing date from CRP data:
extracted_CRP <- str_remove_all(extracted_CRP,
pattern = "[0-9][0-9]?[.][0-9][0-9][.][0-9][0-9][0-9]?[0-9]?")
extracted_CRP <- str_extract_all(extracted_CRP, pattern = "[0-9][0-9]?[0-9]?[.][0-9]?[0-9]?[0-9]?") # spread 0-999
extracted_CRP<- lapply(extracted_CRP,as.numeric)
# getting CRP max value which is more valuable in this case then mean()
extracted_CRP <- sapply(extracted_CRP, max)
extracted_CRP
data$CRP <- extracted_CRP
data <- rename(data, CRP_max = CRP)
# Chol column
extracted_chol <- str_replace_all(data$chol,fixed(","), fixed("."))
extracted_chol <- str_extract_all(extracted_chol, pattern = "[0-9][.][0-9]?[0-9]?") # spread 0-19
extracted_chol<- lapply(extracted_chol,as.numeric)
extracted_chol <- sapply(extracted_chol, mean)
extracted_chol[2]
data$chol <- extracted_chol
# TG column
extracted_TG <- str_replace_all(data$TG,fixed(","), fixed("."))
extracted_TG <- str_extract_all(extracted_TG, pattern = "[0-9][.]?[0-9]?[0-9]?") # spread 0-9
extracted_TG<- lapply(extracted_TG,as.numeric)
extracted_TG <- sapply(extracted_TG, mean)
extracted_TG[15]
data$TG <- extracted_TG
# UHDL colunm
extracted_UHDL <- str_replace_all(data$UHDL,fixed(","), fixed("."))
extracted_UHDL <- str_extract_all(extracted_UHDL, pattern = "[0-9][.]?[0-9]?[0-9]?") # spread 0-9
extracted_UHDL<- lapply(extracted_UHDL,as.numeric)
extracted_UHDL <- sapply(extracted_UHDL, mean, 0-20)
extracted_UHDL[906:913]
data$UHDL <- extracted_UHDL
# DLDL colunm
extracted_DLDL <- str_replace_all(data$DLDL,fixed(","), fixed("."))
extracted_DLDL <- str_extract_all(extracted_DLDL, pattern = "[0-9][.]?[0-9]?[0-9]?") # spread 0-9
extracted_DLDL<- lapply(extracted_DLDL,as.numeric)
extracted_DLDL <- sapply(extracted_DLDL, mean, 0-20)
extracted_DLDL[906:913]
data$DLDL <- extracted_DLDL
# crea column
extracted_crea <- str_replace_all(data$crea,fixed(","), fixed("."))
extracted_crea <- str_extract_all(extracted_crea, pattern = "[0-9][0-9][0-9]?[.]?[0-9]?") # spread 0-9
extracted_crea<- lapply(extracted_crea,as.numeric)
extracted_crea <- sapply(extracted_crea, mean, 0-20)
extracted_crea[906:913]
data$crea <- extracted_crea
#creating age column
x = year(data$admission)
y = year(data$birthdate)
data <- data |> mutate(age = x-y)
data <- data |> relocate(age, .before = admission)
#creating LOS (length of stay(days of hospitalization)) column
x_LOS = (data$admission)
y_LOS = (data$discharge)
data <- data |> mutate(LOS = y_LOS-x_LOS)
data <- data |> relocate(LOS, .before = COVID)
data$LOS <- as.numeric(data$LOS)
# filtering data with 2 or more cases of hospitalization
matches <- data |> group_by(patient_ID) |> summarise(n=n()) |> filter(n>2)
matches <- matches$patient_ID
matched_data <- data |> filter(patient_ID %in% matches)
class(matched_data$COVID)
matched_data <- matched_data |> filter(patient_ID != "****")
matched_data |>group_by(patient_ID) |> summarise(n=n())
# admission/covid plot
matched_data |> ggplot(aes(x = admission, y = patient_ID))+
geom_point(aes(color = COVID))+
scale_color_manual(values = c("blue", "red"))
# запись обновленного файла:
write.xlsx(data, file = "structured_output.xlsx", colNames = T, borders = "columns")
Итоговый результат:
Итоговая таблица
Общее время затраченное всё = 3–4 месяца. Из них 40% — поиск, сбор файлов (вручную). 30% — написание кода. 30% — проверка на ошибки, их исправление.
Буду рад услышать Ваши комментарии и замечания по выполненной работе!