Идея
Реализация
Результат
Идея: в медицинском учреждении выписные эпикризы (информация из истории болезни) пациентов хранятся в общегоспитальной локальной сети.
Необходимо сформировать базу данных пациентов с перенесенным заболеванием 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)
Промежуточный результат:
Полученные данные необходимо привести в формат с которым можно работать - очистить от мусора, задать каждой колонке тип данных, и проч. Этот процесс называется очисткой данных (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% - проверка на ошибки, их исправление.
Буду рад услышать Ваши комментарии и замечания по выполненной работе!
Комментарии (11)
Andrey_Solomatin
17.08.2024 17:42+1if 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"
Если документ не создан людьми, то простая регулярка может пропускать часть случаев, когда дата написанна по другому. Например 22, вместо 2022.
Так можно потерять часть данных. Для особо важных полей можно собрать все пропущенные данные и попробовать глянуть на статистику.
Такой подход уже отчищает данные, хотя отчистку вы делаете следующим шагом.Andrey_Solomatin
17.08.2024 17:42+1re.findall(r'[Тт]...
Гляньте на флаг для регуоярок, вроде он должен работать с русским. https://docs.python.org/3/library/re.html#re.IGNORECASE
Andrey_Solomatin
17.08.2024 17:42+1re.findall(r'\D?\D?\D?
Можно упростить повторяющиеся символыre.findall(r'\D{0,3}
Andrey_Solomatin
17.08.2024 17:42+1if re.findall(r'ИБС|' r'[Ии]шемическая болезнь сердца', x): IHD = re.findall(r'ИБС|' r'[Ии]шемическая болезнь сердца', x)
Повторение регулярок, путь к ошибкам.
Можно вот так упростить, а еще лучше при сохраненни заменить на NA или оставить это на шаг отчистки.IHD = re.findall(r'ИБС|[Ии]шемическая болезнь сердца', x) IHD = IHD if IHD else "NA"
Andrey_Solomatin
17.08.2024 17:42+1if 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 напишите цикл.
expressions = { "LAVI": re.compile(r'ИОЛП.\s?\S?\s?[0-9]{2}')} data = {} for name, exr in expressions.items(): data[name] = exr.findall(x)
И тогда columns для DataFrame возьмёте из data.
Такой подход спасёт вас от copy-paste ошибок при добавлении новых проверок.pogozhy Автор
17.08.2024 17:42Спасибо большое за все Ваши замечания, я подозревал, но не знал как это можно было сделать. Эти решения сильно сократят количество кода и повысят его качество. Буду учиться)
Tom_Rench
17.08.2024 17:42Почитайте про библиотеку yargy, хорошо подходит для поиска сущностей в тексте:
https://github.com/natasha/yargy
Я делал похожую задачу для диплома в магистратуре , может полезно будет:
https://github.com/ezhkovskii/graduate_work_nlp_in_ehr
LunarBirdMYT
Вы провели большую и отличную работу, запарсили кучу документов, очистили их и сделали красивый свод. Можно большое количество if-else заменить на цикл и словарь, возможно. Так будет проще поддерживать код или менять его. Целью, видимо, было продемонстрировать очистку? А дальше этот свод просто для анализа или вы планируете сделать модельку с прогнозами?
pogozhy Автор
спасибо большое за похвалу, это моя первая работа подобного характера, я врач-кардиолог, код наверняка не без ошибок новичка, буду стараться и учиться) У меня есть вторая часть работы, где я провожу моделирование, визуализацию, статистические тесты с полученными данными, к примеру разбиваю пациентов на группы и сравниваю уровень холестерина до и после заболевания. Не стал выкладывать сразу все, чтобы не получилась слишком объемная статья. Если кому то будет интересно могу выложить
jbourne
Очень интересно. Выкладывайте.
И такой вот вопрос: не думали как вычищать/исправлять нетехнический мусор - ошибки ввода данных, противоречивые показания, халатности, подтасовки, халатности записанные на ковид, не ковидные смерти записанные как ковидные и наоборот, и т.д.?
Сам сталкивался с такой проблемой ранее, и тогда ничего лучше не придумали, чем проводить периодические аудиты случайных ситуаций с мед специалистом + закодили часть кросс-проверок простых.
Но это было до ковида. Случаи были редкие, но довольно уникальные, поэтому тогда не получилось нормально автоматизировать их выявление.
pogozhy Автор
К сожалению пришлось проводить вручную много проверок и перепроверок. За время работы в каждый файл пришлось зайти как минимум 2 раза, плюс при добавлении файла изначально проверялись и исправлялись некоторые моменты. К примеру: 1. первый три даты dd.dd.dddd во всех файлах вручную проверялись и исправлялись. 2. Таблицы внутри word не переводятся в текст, каждый файл проверялся вручную, таблицы удалялись 3. Некоторые файлы содержат несколько выписных эпикризов разных пациентов - лишнее удалялось вручную. 4. Лимфоциты крови и лимфоциты мочи могут подтягиваться регуляркой как одно и то же
И таких ошибок очень много, все и не перечислить. Я не представляю как это автоматизировать/закодить, мне кажется это невозможно. Единственный вариант который я посчитал наиболее правильным - вручную перепроверять каждый показатель по несколько раз, глядя на то что первично получается (до data tydying). И сомневаюсь что специалист без медицинского образования качественно разберётся в этих помоях)