W Polsce dane zdrowotne Polaków zbiera wiele różnych instytucji. Poniżej - w dużym uproszczeniu - rozpiska kto co zbiera i jakie to dane.
Jakie dane?
informacje o świadczeniach refundowanych:
wizyty u lekarzy POZ i specjalistów (AOS)
hospitalizacje (szpital)
zabiegi
badania diagnostyczne
refundowane leki i wyroby medyczne
dane identyfikujące pacjenta (PESEL, wiek, płeć, adres – w zakresie potrzebnym do rozliczenia)
dane o rozpoznaniach (kody ICD-10), procedurach (ICD-9, katalogi NFZ) itd.
Do czego?
rozliczanie świadczeń z placówkami (świadczeniodawcami)
statystyka i planowanie finansowania
kontrola nadużyć
analiza kosztów
NFZ ma ogromny obraz tego, co i za ile było za osobę refundowane, ale niekoniecznie wszystko, co pacjent robił prywatnie
To instytucja odpowiedzialna za systemy e-zdrowia (P1, IKP itd.).
Jakie dane tam trafiają?
e-recepty – kto wystawił, komu (PESEL), jakie leki, dawki, realizacja w aptece
e-skierowania – na jakie świadczenia, przez kogo, dla kogo
e-zwolnienia (ZUS ZLA) – info o niezdolności do pracy (wraz z ZUS)
dokumenty elektroniczne EDM (gdzie są przechowywane, kto ma dostęp)
Same dokumenty EDM przechowywane są najczęściej w systemach szpitali/przychodni, ale P1 wie, że istnieją i gdzie.
To interfejs dla Ciebie, ale dane stoją za nim na P1/NFZ:
podgląd e-recept, e-skierowań, historii świadczeń refundowanych
często dane z programów profilaktycznych, szczepień itd.
Każdy podmiot leczniczy prowadzi dokumentację medyczną pacjenta.
Jakie dane?
wywiad zdrowotny, rozpoznania, wyniki badań
opisy zabiegów, wypisy ze szpitala
historie chorób, pomiary (ciśnienie, waga), obrazowanie (RTG, TK, MRI)
zgody pacjenta, informacje o alergiach, lekach przyjmowanych itd.
Gdzie?
w systemach informatycznych danej placówki (HIS, gabinetowe, RIS/PACS) 1
częściowo w formie papierowej (archiwa)
Część z tych informacji może być udostępniana jako EDM i integrowana z P1, ale nie wszystko jest jeszcze w pełni zintegrowane i ujednolicone
Jakie dane?
zgłoszenia chorób zakaźnych i zakażeń (np. COVID, gruźlica, WZW, HIV itd.)
dane o szczepieniach ochronnych (zwłaszcza obowiązkowych)
dane epidemiologiczne z laboratoriów i placówek medycznych (np. ogniska epidemiczne, zatrucia)
Te dane są bardziej zbiorcze, ale przy zgłoszeniach bywają dane identyfikujące (PESEL, adres) – potrzebne do dochodzenia epidemiologicznego.
GUS nie leczy, ale zbiera statystyki zdrowotne:
dane o zgonach i przyczynach zgonu (na podstawie kart zgonu)
dane o hospitalizacjach, zachorowaniach na wybrane choroby
dane z badań statystycznych (np. ankietowe zdrowie populacji, styl życia)
GUS stara się anonimizować/zbiorczo prezentować dane – interesuje go bardziej „ile osób na 100 tys. zachorowało”, niż konkretny pacjent
Jakie dane zdrowotne ma ZUS?
informacje o niezdolności do pracy (e-ZLA)
okres niezdolności
kod jednostki chorobowej (często skrócony, np. F32)
dane z orzecznictwa – komisje ZUS do spraw niezdolności do pracy, renty
dane dot. wypadków przy pracy, chorób zawodowych (częściowo z PIP/pracodawcami)
Nie ma pełnej dokumentacji medycznej, ale ma sporo danych dotyczących zdolności do pracy i przyczyn.
Istnieją różne rejestry chorób i programów:
Krajowy Rejestr Nowotworów
rejestry kardiologiczne, diabetologiczne, transplantacyjne itd.
badania prowadzone przez NFZ, MZ, instytuty (np. NIZP-PZH, IMP itd.)
Zwykle:
dane są pseudonimizowane/anonymizowane, ale na etapie zbierania często są dane osobowe
celem jest monitorowanie jakości leczenia, przeżywalności, skuteczności terapii
Gdzie i jakie dane?
jednostki medycyny pracy, z którymi pracodawca ma umowę, mają:
orzeczenia o zdolności/niezdolności do pracy
wyniki badań profilaktycznych (w zakresie potrzebnym do orzeczenia)
Pracodawca nie powinien widzieć pełnych danych medycznych – dostaje tylko orzeczenie (zdolny/niezdolny, ewentualne ograniczenia), a dokumentacja jest u lekarza medycyny pracy.
Coraz więcej danych zdrowotnych ląduje też u podmiotów niepublicznych:
prywatne przychodnie i sieci ( Luxmed, MediCover, Enel-Med itd.) – mają pełną dokumentację swoich pacjentów
prywatni ubezpieczyciele – dane z wniosków, ankiet medycznych, rozliczanych świadczeń
aplikacje mobilne / wearables:
kroki, sen, tętno, EKG, poziom stresu
dzienniczki glikemii, cyklu miesięcznego, diety, treningów
Tu zakres zależy od regulaminów, zgód marketingowych, RODO – często użytkownik akceptuje dość szerokie przetwarzanie.
Literatura:
Johnson A, Pollard T, Mark R. MIMIC-III Clinical Database Demo (version 1.4). physionet.org. 2019. RRID:SCR_007345. Available from: https://doi.org/10.13026/C2HM2Q
Po ściągnięciu pliku .zip na dysk można z poziomu skryptu wypakować wszystkie pliki:
# utils::unzip(zipfile = "dane/mimic-iii-clinical-database-demo-1.4.zip")
Lub wczytać pojedynczo ściągnięte pliki .csv:
# Pacjenci
PATIENTS <- read.csv(file = "dane/PATIENTS.csv")
# Hospitalizacje
ADMISSIONS <- read.csv(file = "dane/ADMISSIONS.csv")
dplyr::glimpse(PATIENTS)
## Rows: 100
## Columns: 8
## $ row_id <int> 9467, 9472, 9474, 9478, 9479, 9486, 9487, 9489, 9491, 9492…
## $ subject_id <int> 10006, 10011, 10013, 10017, 10019, 10026, 10027, 10029, 10…
## $ gender <chr> "F", "F", "F", "F", "M", "F", "F", "M", "M", "F", "M", "F"…
## $ dob <chr> "2094-03-05 00:00:00", "2090-06-05 00:00:00", "2038-09-03 …
## $ dod <chr> "2165-08-12 00:00:00", "2126-08-28 00:00:00", "2125-10-07 …
## $ dod_hosp <chr> "2165-08-12 00:00:00", "2126-08-28 00:00:00", "2125-10-07 …
## $ dod_ssn <chr> "2165-08-12 00:00:00", "", "2125-10-07 00:00:00", "2152-09…
## $ expire_flag <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
dplyr::glimpse(ADMISSIONS)
## Rows: 129
## Columns: 19
## $ row_id <int> 12258, 12263, 12265, 12269, 12270, 12277, 12278, …
## $ subject_id <int> 10006, 10011, 10013, 10017, 10019, 10026, 10027, …
## $ hadm_id <int> 142345, 105331, 165520, 199207, 177759, 103770, 1…
## $ admittime <chr> "2164-10-23 21:09:00", "2126-08-14 22:32:00", "21…
## $ dischtime <chr> "2164-11-01 17:15:00", "2126-08-28 18:59:00", "21…
## $ deathtime <chr> "", "2126-08-28 18:59:00", "2125-10-07 15:13:00",…
## $ admission_type <chr> "EMERGENCY", "EMERGENCY", "EMERGENCY", "EMERGENCY…
## $ admission_location <chr> "EMERGENCY ROOM ADMIT", "TRANSFER FROM HOSP/EXTRA…
## $ discharge_location <chr> "HOME HEALTH CARE", "DEAD/EXPIRED", "DEAD/EXPIRED…
## $ insurance <chr> "Medicare", "Private", "Medicare", "Medicare", "M…
## $ language <chr> "", "", "", "", "", "", "", "", "", "POLI", "", "…
## $ religion <chr> "CATHOLIC", "CATHOLIC", "CATHOLIC", "CATHOLIC", "…
## $ marital_status <chr> "SEPARATED", "SINGLE", "", "DIVORCED", "DIVORCED"…
## $ ethnicity <chr> "BLACK/AFRICAN AMERICAN", "UNKNOWN/NOT SPECIFIED"…
## $ edregtime <chr> "2164-10-23 16:43:00", "", "", "2149-05-26 12:08:…
## $ edouttime <chr> "2164-10-23 23:00:00", "", "", "2149-05-26 19:45:…
## $ diagnosis <chr> "SEPSIS", "HEPATITIS B", "SEPSIS", "HUMERAL FRACT…
## $ hospital_expire_flag <int> 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0…
## $ has_chartevents_data <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
Zazwyczaj dostępna jest jakaś dokumentacja zbioru na stronie, bądź “zaszyta” w pakiecie, z którego zbiór pozyskujemy.
Opis zmiennych ze zbiorów.
PATIENTS
row_id – techniczny klucz główny (ID wiersza), w
analizach najczęściej niepotrzebny.
subject_id – identyfikator pacjenta, łączy wszystkie
tabele dot. tej osoby (klucz do PATIENTS, ADMISSIONS itd.).
gender – płeć (‘M’, ‘F’).
dob – date of birth – data urodzenia
(przesunięta w czasie dla anonimizacji).
dod – date of death – data zgonu (jeśli
znana, z różnych źródeł; NA jeśli pacjent przeżył ≥90 dni od
wypisu).
dod_hosp – data zgonu wg dokumentacji szpitalnej
(jeśli zmarł w szpitalu).
dod_ssn – data zgonu wg Social Security (rejestr
zgonów USA).
expire_flag – flaga 0/1 – czy pacjent zmarł (1 =
tak).
ADMISSIONS
row_id – techniczny klucz wiersza.
subject_id – pacjent (łączy z PATIENTS).
hadm_id – hospital admission ID – konkretne
przyjęcie do szpitala (klucz tej tabeli).
admittime – data/godzina przyjęcia do
szpitala.
dischtime – data/godzina wypisu ze
szpitala.
deathtime – data/godzina zgonu w szpitalu (jeśli
pacjent zmarł podczas tego pobytu).
admission_type – typ przyjęcia (np. EMERGENCY,
ELECTIVE, URGENT).
admission_location – skąd pacjent trafił (np. z SOR,
z innego oddziału, z innego szpitala).
discharge_location – dokąd został wypisany (do domu,
na inny oddział, do hospicjum itd.).
insurance – typ ubezpieczenia (np. MEDICARE,
PRIVATE).
language – język pacjenta (np. ENGL).
religion – religia (np. CATHOLIC).
marital_status – stan cywilny (MARRIED,
SINGLE…).
ethnicity – grupa etniczna (np. WHITE, BLACK/AFRICAN
AMERICAN).
edregtime – czas rejestracji na SOR.
edouttime – czas wypisu z SOR.
diagnosis – tekstowy opis rozpoznania przy przyjęciu
(nie kod ICD; kody są w DIAGNOSES_ICD).
hospital_expire_flag – flaga 0/1 – czy pacjent zmarł
w tym pobycie w szpitalu.
has_chartevents_data – 0/1 – czy dla tej
hospitalizacji są dane w tabeli CHARTEVENTS (ciągłe pomiary z
OIOM).
# Łączenie na poziomie hospitalizacji (każdy wiersz = jeden pobyt z cechami pacjenta)
dane <- ADMISSIONS %>%
left_join(PATIENTS, by = "subject_id")
Struktura demograficzna pacjentów.
rozklad_plci <- PATIENTS %>%
count(gender) %>%
mutate(Procent = n / sum(n) * 100)
# tabela
kableExtra::kable(rozklad_plci,
caption = "Rozkład płci",
col.names = c("Płeć", "Liczba obserwacji", "%"))
| Płeć | Liczba obserwacji | % |
|---|---|---|
| F | 55 | 55 |
| M | 45 | 45 |
# alternatywnie, inny sposób liczenia i inna funkcja print'ująca tabelę
DT::datatable(
janitor::tabyl(PATIENTS$gender) # <-
,colnames = c("Płeć", "Liczba pacjentów", "%")
)
# etykiety z procentami
labels <- paste0(rozklad_plci$gender, ": ", round(rozklad_plci$Procent, 1), "%")
pie(x = rozklad_plci$Procent,
labels = labels,
col = c("darkblue",
"lightblue"),
main = "Rozkład płci wśród pacjentów")
library(ggplot2)
rozklad_plci <- rozklad_plci %>%
mutate(
label = paste0(gender, ": ", round(Procent, 1), "%") # etykiety z procentami
)
ggplot(rozklad_plci,
aes(x = "",
y = Procent,
fill = gender)) +
geom_col(width = 1) +
coord_polar(theta = "y") +
geom_text(aes(label = label),
position = position_stack(vjust = 0.5)) +
scale_fill_manual(values = c("darkblue",
"lightblue")) +
labs(
x = NULL,
y = NULL,
fill = "Płeć",
title = "Rozkład płci wśród pacjentów"
) +
theme_void()
Wiek przy pierwszej hospitalizacji (zbiór ma przesunięte daty z uwagi na procedury pseudonimizacyjne, ale relacje czasowe są zachowane).
library(lubridate) # pakiet / narzędzie służące do łatwej i wygodnej pracy z datami i czasem
admissions_min <- ADMISSIONS %>%
group_by(subject_id) %>%
summarise(first_admit = min(as_datetime(admittime)))
demo_age <- admissions_min %>%
left_join(PATIENTS, by = "subject_id") %>%
mutate(
dob = as_datetime(dob),
age_first_admit = as.numeric(difftime(first_admit, dob, units = "days")) / 365.25
)
kableExtra::kable(
t(as.matrix(
summary(demo_age$age_first_admit) # <-
)
)
)
| Min. | 1st Qu. | Median | Mean | 3rd Qu. | Max. |
|---|---|---|---|---|---|
| 17.1916 | 64.93596 | 76.90854 | 88.45447 | 85.17951 | 299.9969 |
hist(demo_age$age_first_admit,
main = "Histogram wieku przy pierwszej hospitalizacji",
xlab = "Wiek przy pierwszej hospitalizacji",
col = "lightblue",
border = "black",
breaks = 30
# ,xlim = c(0, 105) # <- jeśli nie chcemy wyrzucać ze zbioru możemy wyskalować oś ox
)
hist(demo_age$age_first_admit,
main = "Histogram wieku przy pierwszej hospitalizacji",
xlab = "Wiek przy pierwszej hospitalizacji",
col = "lightblue",
border = "black",
breaks = 30
,xlim = c(0, 105) # <- jeśli nie chcemy wyrzucać ze zbioru możemy wyskalować oś ox
)
ggplot2::ggplot(demo_age,
aes(x = age_first_admit)) +
geom_histogram(binwidth = 5,
fill = "skyblue2",
color = "black") +
labs(title = "Histogram wieku przy pierwszej hospitalizacji",
x = "Wiek przy pierwszej hospitalizacji",
y = "Liczba") +
theme_minimal()
ggplot2::ggplot(demo_age,
aes(x = age_first_admit)) +
geom_histogram(binwidth = 5,
fill = "skyblue3",
color = "black") +
labs(title = "Histogram wieku przy pierwszej hospitalizacji",
x = "Wiek przy pierwszej hospitalizacji",
y = "Liczba") +
scale_x_continuous(limits = c(1,100)) +
theme_minimal()
Liczba hospitalizacji na pacjenta
admissions_per_patient <- ADMISSIONS %>%
count(subject_id, name = "n_admissions")
kableExtra::kable(
t(as.matrix(
summary(admissions_per_patient$n_admissions) # <-
)
)
)
| Min. | 1st Qu. | Median | Mean | 3rd Qu. | Max. |
|---|---|---|---|---|---|
| 1 | 1 | 1 | 1.29 | 1 | 15 |
kableExtra::kable(
janitor::tabyl(admissions_per_patient$n_admissions) # <-
,caption = "Liczba hositalizacji na pacjenta",
col.names = c("Liczba pobytów", "Liczba pacjentów", "%")
)
| Liczba pobytów | Liczba pacjentów | % |
|---|---|---|
| 1 | 86 | 0.86 |
| 2 | 11 | 0.11 |
| 3 | 2 | 0.02 |
| 15 | 1 | 0.01 |
# tabela: ile osób ma 1, 2, 3+ pobytów?
kableExtra::kable(x =
admissions_per_patient %>%
mutate(n_cat = ifelse(test = n_admissions >= 3,
yes = "3+",
no = as.character(n_admissions))) %>%
count(n_cat)
, col.names = c("Liczba pobytów", "Liczba pacjentów")
, caption = "Liczba pojedynczych i ponownych hospitalizacji"
)
| Liczba pobytów | Liczba pacjentów |
|---|---|
| 1 | 86 |
| 2 | 11 |
| 3+ | 3 |
Długość pobytu (LOS – length of stay)
admissions_los <- ADMISSIONS %>%
mutate(
admittime = as.POSIXct(admittime, tz = "UTC"), # data/godzina przyjęcia do szpitala
dischtime = as.POSIXct(dischtime, tz = "UTC"), # data/godzina wypisu ze szpitala
los_days = as.numeric(difftime(dischtime, admittime, units = "days"))
)
kableExtra::kable(
t(as.matrix(
summary(round(admissions_los$los_days, 1)) # <-
)
)
)
| Min. | 1st Qu. | Median | Mean | 3rd Qu. | Max. |
|---|---|---|---|---|---|
| 0 | 3.3 | 6.6 | 9.332558 | 10.6 | 124 |
ggplot(admissions_los,
aes(x = los_days)) +
geom_density(
fill = "#3399FF", # Niebieski kolor wypełnienia
color = "#0066CC", # Ciemniejsza obwódka
alpha = 0.7 # Przezroczystość wypełnienia
) +
labs(
title = "Rozkład Gęstości Czasu Pobytu (los_days)",
x = "Liczba Dni Pobytu (los_days)",
y = "Gęstość"
) +
theme_minimal()
ggplot(admissions_los %>% filter(los_days < 40),
aes(x = los_days)) +
geom_density(
fill = "#3399FF", # Niebieski kolor wypełnienia
color = "#0066CC", # Ciemniejsza obwódka
alpha = 0.7 # Przezroczystość wypełnienia
) +
labs(
title = "Rozkład Gęstości Czasu Pobytu (los_days)",
x = "Liczba Dni Pobytu (los_days)",
y = "Gęstość"
) +
theme_minimal()
ggplot(admissions_los,
aes(x = los_days)) +
geom_histogram(
bins = 30, # Liczba słupków (przedziałów)
fill = "#3399FF", # kolor wypełnienia
color = "lightslateblue" # obramowanie słupków
) +
labs(
title = "Histogram Czasu Pobytu (los_days)",
x = "Liczba Dni Pobytu (los_days)",
y = "Częstotliwość"
) +
theme_minimal()
ggplot(admissions_los %>% filter(los_days<40),
aes(x = los_days)) +
geom_histogram(
bins = 50, # Liczba słupków (przedziałów)
fill = "lightslateblue", # kolor wypełnienia
color = "#3399FF" # obramowanie słupków
) +
labs(
title = "Histogram Czasu Pobytu (los_days)",
x = "Liczba Dni Pobytu (los_days)",
y = "Częstotliwość"
) +
theme_minimal()
ggplot(admissions_los,
aes(x = admission_type,
y = los_days)) +
geom_boxplot() +
coord_flip() +
labs(x = "Typ przyjęcia",
y = "Długość pobytu [dni]") +
theme_minimal()
ggplot(admissions_los,
aes(x = admission_type,
y = los_days)) +
geom_boxplot() +
coord_flip() +
labs(x = "Typ przyjęcia",
y = "Długość pobytu [dni]") +
scale_y_continuous(limits = c(1,40)) +
theme_minimal()
Po spolszczeniu kategorii, żeby były bardziej intuicyjne.
admissions_los <- admissions_los %>%
mutate(
admission_type_pl = case_when(
admission_type == "ELECTIVE" ~ "planowy",
admission_type == "EMERGENCY" ~ "nagły",
admission_type == "URGENT" ~ "pilny",
admission_type == "NEWBORN" ~ "noworodek",
TRUE ~ "inny"
)
)
ggplot(admissions_los,
aes(x = admission_type_pl,
y = los_days,
fill = admission_type_pl)) + # kolor wg typu
geom_boxplot() +
coord_flip() +
scale_fill_manual(
values = c(
"planowy" = "#4C78A8",
"pilny" = "#F58518",
"nagły" = "#E45756",
"noworodek" = "#72B7B2",
"inny" = "#B279A2"
)
) +
labs(
x = "Typ przyjęcia",
y = "Długość pobytu [dni]",
fill = "Typ przyjęcia"
) +
scale_y_continuous(limits = c(1,40)) +
theme_minimal() +
theme(
legend.position = "none" # jeśli nie chcemy legendy (wynika z osi oy)
)
Śmiertelność w szpitalu.
ADMISSIONS %>%
summarise(
n = n(),
deaths = sum(hospital_expire_flag),
mortality_pct = round(100 * deaths / n, 2)) %>%
kableExtra::kable(
col.names = c("Liczba pacjentów", "Liczba zgonów", "Udział (%)"),
caption = "Zgony szpitalne")
| Liczba pacjentów | Liczba zgonów | Udział (%) |
|---|---|---|
| 129 | 40 | 31.01 |
Śmiertelność wg typu przyjęcia
ADMISSIONS %>%
group_by(admission_type) %>%
summarise(
n = n(),
deaths = sum(hospital_expire_flag),
mortality_pct = 100 * deaths / n
) %>%
arrange(desc(mortality_pct)) %>%
mutate(admission_type = case_when(
admission_type == "ELECTIVE" ~ "planowy",
admission_type == "EMERGENCY" ~ "nagły",
admission_type == "URGENT" ~ "pilny",
TRUE ~ "inny")
) %>%
kableExtra::kable(
col.names = c("Tryb przyjęcia", "Liczba pacjentów", "Liczba zgonów", "Udział (%)"),
caption = "Zgony szpitalne wg typu przyjęcia")
| Tryb przyjęcia | Liczba pacjentów | Liczba zgonów | Udział (%) |
|---|---|---|---|
| pilny | 2 | 1 | 50.00000 |
| nagły | 119 | 39 | 32.77311 |
| planowy | 8 | 0 | 0.00000 |
Krzywa przeżycia w szpitalu
Bazując na ramce danych ADMISSIONS można używając
estymatora Kaplana-Meyera zrobić uproszczoną analizę przeżycia w
szpitalu: czas od przyjęcia do
dischtime/deathtime,
event = hospital_expire_flag.
library(survival)
library(survminer) # ładne wykresy
surv_df <- admissions_los %>%
filter(los_days < 50) %>% # !
mutate(
time_days = los_days,
event = hospital_expire_flag
)
fit <- survfit(
Surv(time_days, event) ~ 1,
data = surv_df
)
ggsurvplot(fit,
xlab = "Dni od przyjęcia",
ylab = "Prawdopodobieństwo przeżycia")
df_full <- admissions_los %>%
left_join(demo_age %>%
select(subject_id, age_first_admit) %>%
filter(age_first_admit < 100), # !
by = "subject_id")
ggplot(df_full,
aes(x = age_first_admit,
y = hospital_expire_flag)) +
geom_smooth(method = "loess") +
labs(x = "Wiek przy pierwszej hospitalizacji",
y = "Szacowane prawdopodobieństwo zgonu w szpitalu") +
theme_minimal()
# komentarz:
# geom_smooth():
# - bierze sobie wszystkie punkty (age_first_admit, hospital_expire_flag)
# - dopasowuje gładką krzywą (tu: LOESS)
# - i rysuje wartość oczekiwaną Y w funkcji X, czyli E[Y | X = wiek].
df_full <- df_full %>%
filter(age_first_admit < 100) %>% # !
mutate(age_group = cut(age_first_admit,
breaks = c(0, 40, 60, 80, 120),
labels = c("<40", "40–59", "60–79", "80+")))
smiert_po_grupach <- df_full %>%
group_by(age_group) %>%
summarise(
n = n(),
deaths = sum(hospital_expire_flag),
mortality_pct = round(100 * deaths / n, 1)
)
kableExtra::kable(smiert_po_grupach,
col.names = c("Grupa wiekowa", "Liczba pacjentów", "Liczba zgonów", "%"),
caption = "Śmiertelność szpitalna po grupach wiekowych")
| Grupa wiekowa | Liczba pacjentów | Liczba zgonów | % |
|---|---|---|---|
| <40 | 6 | 4 | 66.7 |
| 40–59 | 21 | 6 | 28.6 |
| 60–79 | 56 | 10 | 17.9 |
| 80+ | 37 | 16 | 43.2 |
ggplot(smiert_po_grupach,
aes(x = age_group,
y = mortality_pct)) +
geom_col(fill = "steelblue") +
labs(
x = "Grupa wieku",
y = "Śmiertelność w szpitalu [%]",
title = "Śmiertelność szpitalna według grup wieku"
) +
theme_minimal()
ggplot(smiert_po_grupach,
aes(x = age_group,
y = mortality_pct,
fill = age_group)) + # Przypisanie koloru do zmiennej
geom_col() +
# Użycie skali manualnej do ręcznego ustawienia kolorów
scale_fill_manual(
values = c("<40" = "#ADD8E6",
"40–59" = "#ADD8E6",
"60–79" = "#4682B4",
"80+" = "#191970") # Przykładowe kolory dla każdej kategorii
) +
labs(
x = "Grupa wieku",
y = "Śmiertelność w szpitalu [%]",
title = "Śmiertelność szpitalna według grup wieku",
fill = "Grupa Wieku" # Zmiana tytułu legendy
) +
theme_minimal()
{mlbench}{mlbench} to pakiet z przykładowymi zbiorami danych do
ćwiczenia metod uczenia maszynowego (machine learning
benchmark).
Zawiera różne dobrze znane zestawy, m.in.:
PimaIndiansDiabetesBreastCancer# install.packages("mlbench")
library(mlbench)
PimaIndiansDiabetes to data.frame z danymi
medycznymi kobiet z plemienia Pima (rdzenni mieszkańcy
Ameryki), używanymi do przewidywania, czy ktoś ma cukrzycę
(diabetes).
data("PimaIndiansDiabetes")
str(PimaIndiansDiabetes)
## 'data.frame': 768 obs. of 9 variables:
## $ pregnant: num 6 1 8 1 0 5 3 10 2 8 ...
## $ glucose : num 148 85 183 89 137 116 78 115 197 125 ...
## $ pressure: num 72 66 64 66 40 74 50 0 70 96 ...
## $ triceps : num 35 29 0 23 35 0 32 0 45 0 ...
## $ insulin : num 0 0 0 94 168 0 88 0 543 0 ...
## $ mass : num 33.6 26.6 23.3 28.1 43.1 25.6 31 35.3 30.5 0 ...
## $ pedigree: num 0.627 0.351 0.672 0.167 2.288 ...
## $ age : num 50 31 32 21 33 30 26 29 53 54 ...
## $ diabetes: Factor w/ 2 levels "neg","pos": 2 1 2 1 2 1 2 1 2 2 ...
Co oznaczają kolumny?
pregnant – liczba ciąż
glucose – stężenie glukozy w osoczu po 2h w teście
doustnym (oral glucose tolerance test)
pressure – ciśnienie rozkurczowe (diastolic
blood pressure, mm Hg)
triceps – grubość fałdu skórnego tricepsa
(mm)
insulin – stężenie insuliny w surowicy (2 godziny,
μU/ml)
mass – BMI (body mass index), czyli masa /
wzrost
pedigree – wskaźnik obciążenia genetycznego cukrzycą
(diabetes pedigree function)
age – wiek (w latach)
diabetes – zmienna wynikowa (target):
“neg” – brak cukrzycy
“pos” – cukrzyca obecna
Zazwyczaj zapoznajemy się ze zbiorem eksplorując go, oglądając zmienne, rysując sobie rozkłady, tworząc tabele częstości, krzyżując jakieś zmienne ze sobą.
# Cukrzycy i zdrowi
table(PimaIndiansDiabetes$diabetes)
##
## neg pos
## 500 268
# Wiek a BMI
plot(y = PimaIndiansDiabetes$age,
x = PimaIndiansDiabetes$mass)
# Glukowaza a BMI
plot(PimaIndiansDiabetes$glucose,
PimaIndiansDiabetes$mass,
xlab = "Glukoza",
ylab = "BMI")
# Cukrzyca a BMI
plot(PimaIndiansDiabetes$diabetes,
PimaIndiansDiabetes$mass,
xlab = "Cukrzyca",
ylab = "BMI")
Gdy zatrzymamy się na czymś ciekawym, zazwyczaj sięgamy po literaturę, dopytujemy ekspertów, etc., żeby zrozumieć naturę jakiegoś zjawiska, a następnie pogłębiamy analizę.
“Wysokie BMI matką wszystkich chorób” - autor nieznany
Co ogólnie wiemy o BMI?
Jak wygląda nasza populacja pod kątem wagi?
hist(PimaIndiansDiabetes$mass)
Sprawdźmy zatem, jak BMI wpływa na prawdopodobieństwo wystąpienia cukrzycy.
Zbudujmy model.
model_bmi <- glm(diabetes ~ mass,
family = binomial,
data = PimaIndiansDiabetes
)
Powyższy kod ma za zadanie:
zbudować model regresji logistycznej
(glm(..., family = binomial)),
w którym zmienną objaśnianą jest diabetes (czy osoba
ma cukrzycę – tak/nie),
a zmienną objaśniającą jest mass
(BMI – wskaźnik masy ciała),
na danych z ramki PimaIndiansDiabetes
Model matematycznie ma postać:
\[ \text{logit}\big(P(\text{diabetes} = "pos")\big) = \beta_0 + \beta_1 \cdot \text{mass} \]
równoważne z:
\[ \log\left(\frac{p}{1-p}\right) = \beta_0 + \beta_1 \cdot \text{BMI} \]
gdzie:
Po przekształceniu:
\[ p = \frac{1}{1 + e^{-(\beta_0 + \beta_1 \cdot \text{BMI})}} \]
summary(model_bmi)
##
## Call:
## glm(formula = diabetes ~ mass, family = binomial, data = PimaIndiansDiabetes)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -3.68641 0.40896 -9.014 < 2e-16 ***
## mass 0.09353 0.01205 7.761 8.45e-15 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 993.48 on 767 degrees of freedom
## Residual deviance: 920.71 on 766 degrees of freedom
## AIC: 924.71
##
## Number of Fisher Scoring iterations: 4
Krótki, uproszczony opis powyższego podsumowania.
Intercept (−3.686) – to jest „punkt startowy” modelu, logit prawdopodobieństwa cukrzycy, gdy mass = 0 (czysto techniczny, sam w sobie mało interpretowalny, bo BMI = 0 nie ma sensu).
mass: 0.09353 – to główny wynik:
dodatni współczynnik → im wyższe BMI, tym wyższe prawdopodobieństwo cukrzycy
w skali logitów to 0.0935 na każdy 1 punkt BMI
w skali ilorazu szans (odds ratio): exp(0.09353)≈1.10 czyli wzrost BMI o 1 jednostkę zwiększa szanse cukrzycy o ok. 10%.
p-value dla mass: 8.45e-15 i ***
→ efekt BMI jest bardzo istotny statystycznie (praktycznie zerowe prawdopodobieństwo, że to przypadek).
Null deviance: 993.48 – błąd modelu bez zmiennych (tylko stała).
Residual deviance: 920.71 – błąd modelu z BMI.
Spadek deviance (993.48 → 920.71) oznacza, że mass poprawia dopasowanie modelu – model z BMI lepiej przewiduje cukrzycę niż model pusty - „bez niczego”.
AIC: 924.71 – ogólny wskaźnik jakości dopasowania (im niżej, tym lepiej; sens ma przy porównywaniu kilku modeli).
Model pokazuje, że BMI ma silny, dodatni i istotny statystycznie związek z występowaniem cukrzycy w tych danych: im większa masa ciała (wyższe BMI), tym większe szanse, że osoba ma cukrzycę. Model z samym BMI już coś sensownego wyjaśnia, choć oczywiście nie opisuje całego zjawiska choroby.
Czym są szanse (odds) i iloraz szans (odds ratio)? (Przypomnienie)
Dla zdarzenia (np. „ma cukrzycę”) mamy:
Szanse (odds) definiuje się jako:
\[ \text{odds} = \frac{p}{1-p} \]
Przykład:
Interpretacja: „szanse 1 do 4” (1:4).
Iloraz szans porównuje szanse między dwiema grupami:
\[ \text{OR} = \frac{\text{odds w grupie A}}{\text{odds w grupie B}} \]
Przykład z modelu:
mass ma OR ≈ 1.098Dlaczego stosuje się OR w regresji logistycznej?
W regresji logistycznej modelujemy logarytm szans (log-odds):
\[ \log\left(\frac{p}{1-p}\right) = \beta_0 + \beta_1 \cdot \text{mass} \]
Czyli:
exp(coef(model)) przekształca to w
iloraz szans, który można sensownie interpretować dla
praktyki klinicznej lub prezentacji wyników.Jak wyciągnąć powyższe z modelu?
exp( # podnosi e do potęgi każdego współczynnika
coef(model_bmi) # wyciąga współczynniki z modelu logistycznego
)
## (Intercept) mass
## 0.02506174 1.09804408
mass = 1.098...
To jest iloraz szans (odds ratio) dla BMI.
Znaczy to, że wzrost BMI o 1 punkt zwiększa szanse wystąpienia cukrzycy o ok. 9.8% (bo 1.098 ≈ 1 + 0.098).
(Intercept) = 0.02506
To są szanse cukrzycy przy BMI = 0 (czysto technicznie wynik modelu, biologicznie bez sensu, bo BMI=0 nie istnieje).
Rzadko interpretuje się to dosłownie, ważniejszy jest współczynnik przy mass.
Jak praktycznie możemy wykorzystać modele regresji?
# Generujemy sekwencję bmi, min() i max() z danych, i "sensowny krok"
bmi_seq <- seq(
from = min(PimaIndiansDiabetes$mass, na.rm = TRUE),
to = max(PimaIndiansDiabetes$mass, na.rm = TRUE),
by = 0.1
)
# robimy ramkę danych do predykcji używając wygenerowanej sekwencji
new_data <- data.frame(mass = bmi_seq)
# liczymy przewidywane prawdopodobieństwo
pred_prob <- predict(object = model_bmi,
newdata = new_data,
type = "response")
# rysujemy na osiach policzone przed chwilą elementy
plot(new_data$mass,
pred_prob,
type = "l", # l = line
xlab = "BMI (mass)",
ylab = "P(cukrzyca = 'pos')",
main = "Prawdopodobieństwo cukrzycy w funkcji BMI")
plot(new_data$mass,
pred_prob,
type = "l", # l = line
xlab = "BMI (mass)",
ylab = "P(cukrzyca = 'pos')",
main = "Prawdopodobieństwo cukrzycy w funkcji BMI")
# dodanie danych z ramki
points(PimaIndiansDiabetes$mass,
as.numeric(PimaIndiansDiabetes$diabetes == "pos"),
pch = 16, cex = 0.5)
Systemy informatyczne danej placówki medycznej to
specjalistyczne rozwiązania IT, które wspomagają zarządzanie procesami
medycznymi i administracyjnymi. HIS (Hospital
Information System) to zintegrowany szpitalny system informacyjny,
który obejmuje centralne moduły jak rejestracja pacjentów, zlecenia,
elektroniczną dokumentację medyczną oraz moduły peryferyjne obsługujące
laboratorium, radiologię czy farmację. Systemy gabinetowe wspierają
pracę mniejszych jednostek medycznych, umożliwiając prowadzenie
dokumentacji i zarządzanie pacjentami.
RIS
(Radiology Information System) to system informatyczny
dedykowany oddziałom radiologii, który pozwala na zarządzanie
zleceniami, optymalizację wykorzystania zasobów oraz tworzenie
elektronicznej dokumentacji badań obrazowych. PACS
(Picture Archiving and Communication System) to system do
archiwizacji, przesyłania i udostępniania obrazów diagnostycznych, który
integruje się z RIS i HIS, usprawniając wymianę informacji i
przyspieszając diagnostykę medyczną.
Te systemy współdziałają, by
podnieść efektywność pracy placówki, zmniejszyć ryzyko błędów oraz
poprawić jakość opieki nad pacjentem dzięki elektronicznej dokumentacji
i szybkiej wymianie danych między urządzeniami diagnostycznymi a
personelem medycznym. Za: https://ucyfrowienie.pl/systemy-pacsris/↩︎