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 1), procedurach (ICD-9 2 , 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) 3
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) Jak GUS prowadzi statystykę zgonów?
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.
Na jakie nowotwory zapadają mieszkańcy “zapylonych”, dużych miast, raki związane z drogami oddechowymi??
Claude:
Wygenerujmy na stronie KRN podsumowanie
zapadalności na podane przez model językowy rozpoznania ICD-10
(C34, C32, C30–C31,
C11, C33), w okresie 2000-2010, dla
obu płci łącznie, dla wszystkich grup wiekowych
Sporo się mówi, że Kraków jest położony w niecce, palą tam “kopciuchami”, a Katowice, to takie uprzemysłowione, zanieczyszczone miasto, z wiecznymi dymami z ogromnych kominów unoszących się nad dachami miasta. Zakładam zatem, że mieszkańcy właśnie tych miast żyją najkrócej!
Przeczesując odpowiednie zakładki na stronie oraz eksplorując szereg
typów zbieranych statystyk, natrafiamy w koću na właściwą
podstronę.
Warto zacząć od publikacji w .pdf, przybliżyć sobie kontekst,
stosowane definicje i metody. Wówczas możemy znaleźć skojarzony z
tematem plik .xlsx i analizować go na własną rękę.
Wyniki są co najmniej zastanawiające.
Dwa wyimki z raportu z roku 2023
“Trwanie życia było najdłuższe dla mężczyzn w Krakowie (77,1 roku)”
“Największą różnicę między trwaniem życia mężczyzn i kobiet odnotowano w Łodzi (7,5 roku). Na tle innych wielkich miast bardzo niekorzystnie wypadają podregion katowicki oraz Łódź, w których trwanie życie jest krótsze o więcej niż rok w stosunku do średniej krajowe”.
Jeden cytat z raportu z roku 2024
“W 2024 r. najdłuższe trwanie życia dla mężczyzn i kobiet odnotowano w Warszawie (odpowiednio 77,74 oraz 83,86 roku). Najkrótsze natomiast dla mężczyzn w Łodzi (73,51 roku), a dla kobiet w podregionie katowickim (80,25 roku).”
Jak możemy to potencjalnie wyjaśnić?
Występuje tu tzw. efekt “premii miejskiej”, który w skrócie sprowadza się do tego, że w mieście mamy:
Lepszą opiekę medyczną (Dostęp do specjalistów, szpitali klinicznych)
Wyższe wykształcenie, a co za tym idzie zdrowszy tryb życia, profilaktyka
Wyższe dochody, a to oznacza lepszą dietę, lepszej jakości produkty, aktywność fizyczną
Migracja selekcyjna, do miast przyjeżdżają zdrowsi, młodsi ludzie, za pracą, na studia, etc.
Mniejsze palenie tytoniu? W miastach pali się mniej niż na wsi. Ale tu poczekałbym na efekty ‘wapowania’.
“Kiedyś to było bezpiecznie! Teraz strach dziecko na podwórko puścić, Panie Kochany…”
“Drogi puste, ale nie spokojniejsze”
Na co umierali Polacy w trakcie pandemii Covid-19?
Nowotwory i ich profilaktyka w dobie pandemii Covid-19
Liczba depresji?
Depresja widziana z punktu widzenia płatnika świadczeń.
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)
Zgony - proces zbierania informacji przez GUS
Krok 1 – Lekarz wystawia kartę zgonu Na karcie
zgonu lekarz stwierdzający zgon ma obowiązek wpisać trzy przyczyny zgonu
w formie opisu słownego: bezpośrednią (ostateczna przyczyna zgonu),
wtórną (stan dający jej początek) oraz wyjściową (pierwotną) – chorobę
lub okoliczności, które zapoczątkowały łańcuch zdarzeń prowadzących do
śmierci. Lekarz wystawiający kartę zgonu nie jest uprawniony do
kodowania przyczyn, ma obowiązek jedynie opisać stany chorobowe słownie.
Krok 2 – Dane trafiają do USC i dalej do GUS GUS
otrzymuje dane o osobach zmarłych z dwóch źródeł: z urzędów stanu
cywilnego – zarówno w postaci elektronicznej (zbiór danych przesyłany
bezpośrednio na serwer GUS), jak i w postaci papierowej kserokopii z
opisem słownym przyczyn zgonu, wysyłanej do Urzędu Statystycznego w
Olsztynie. Dodatkowo, w celach kontrolnych, z Ministerstwa Cyfryzacji
trafiają zbiorcze zestawienia aktów zgonów sporządzonych przez USC za
każdy miesiąc.
Krok 3 – Kodowanie przez lekarzy-koderów To jest
serce całego procesu i jego największa słabość. Kopie kart zgonu w
formie papierowej wysyłane są ze wszystkich USC w Polsce do Urzędu
Statystycznego w Olsztynie raz w miesiącu. Tam lekarz-koder – na
podstawie opisów słownych, a w razie wątpliwości dokumentacji medycznej
lub konsultacji z lekarzem orzekającym – rozstrzyga wyjściową przyczynę
zgonu i nadaje jej kod ICD-10. Kod ten wprowadzany jest do zbioru
statystycznego, a nie na samą kartę zgonu. Funkcję kodera wykonuje
zaledwie 15 lekarzy, zaprzysiężonych co do zachowania tajemnicy
statystycznej.
Krok 4 – Kontrola i walidacja Wskazane kody przyczyn
zgonów poddawane są kilkupoziomowej kontroli: automatycznej walidacji w
powiązaniu z płcią i wiekiem zmarłego; ręcznej weryfikacji przez naukowe
instytuty medyczne (Instytut Onkologii, Instytut Matki i Dziecka,
Instytut Kardiologii, NIZP-PZH, CSIOZ); oraz kontroli oprogramowaniem
EDIT opracowanym przez Eurostat. Przypadki wyłonione w toku kontroli
trafiają do ponownego sprawdzenia przez lekarzy-koderów.
Krok 5 – Publikacja danych Dane o liczbie zgonów
ogółem (bez przyczyn) publikowane są co miesiąc w Biuletynie
Statystycznym. Natomiast szczegółowe dane skorelowane z przyczynami
zgonów dostępne są dopiero w styczniu roku następnego po roku następnym
– czyli z ok. dwuletnim opóźnieniem.
Kluczowe ograniczenia systemu Cały proces kodowania
jedynie wyjściowej (a nie wszystkich trzech) przyczyny zgonu wynika
m.in. z braku elektronicznej karty zgonu w Polsce. Jej wprowadzenie
umożliwiłoby automatyczne kodowanie i uwzględnienie wszystkich przyczyn,
co wiązałoby się jednak z koniecznością zmian prawnych. Polska i tak
przekazuje do Eurostatu i WHO wyłącznie wyjściową przyczynę zgonu, bo
żadna z tych organizacji nie wymaga pełnych trzech przyczyn.
Krótko
mówiąc – to system mocno zależny od ręcznej pracy kilkunastu lekarzy,
papierowych dokumentów i dużego opóźnienia czasowego.
ICD-10 Międzynarodowa Statystyczna
Klasyfikacja Chorób i Problemów Zdrowotnych, 10. rewizja (ang.
International Classification of Diseases) – opracowany przez WHO system
kodowania chorób, zaburzeń, urazów i innych stanów zdrowotnych. Każda
jednostka chorobowa ma przypisany unikalny alfanumeryczny kod (np. J45 =
astma, I10 = nadciśnienie; Litera + cyfry). Służy do diagnozowania,
sprawozdawczości, statystyk zdrowotnych i rozliczeń z NFZ. Czasem kod
jest bardziej szczegółowy, np. J45 – astma (kategoria ogólna), J45.0 –
astma przeważnie alergiczna, J45.1 – astma niealergiczna, J45.8 – astma
mieszana. Część po kropce może określać m.in.: postać/typ choroby (np.
alergiczna vs. niealergiczna), stopień nasilenia (łagodna, umiarkowana,
ciężka), lokalizację (np. przy złamaniach – która kość, która strona),
fazę (np. ostra vs. przewlekła) itd.
Etiologia (czyli przyczyna
choroby) jest w ICD-10 obsługiwana przez podwójne kodowanie, czyli
użycie dwóch kodów jednocześnie: kod † (krzyżyk) – oznacza chorobę
podstawową / przyczynę kod * (gwiazdka) – oznacza manifestację /
powikłanie w konkretnym narządzie Na przykład cukrzycowa retinopatia =
E14.3† + H36.0*
↩︎
ICD-9 Poprzednia, 9. rewizja tej samej
klasyfikacji – w Polsce stosowana głównie do kodowania procedur
medycznych (zabiegów, operacji, badań diagnostycznych), nie chorób. W
kontekście polskiej dokumentacji medycznej i NFZ funkcjonuje jako ICD-9
CM (Clinical Modification) i odpowiada na pytanie „co
zrobiono pacjentowi?“, podczas gdy ICD-10 odpowiada na pytanie
„co mu dolega?” (np. 88.71 – USG serca; Cyfry (+ podkody)).
↩︎
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/↩︎