Jakie dane są zbierane i przez kogo?


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.


  1. NFZ (Narodowy Fundusz Zdrowia)

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


  1. CeZ (Centrum e-Zdrowia – dawniej CSIOZ)

To instytucja odpowiedzialna za systemy e-zdrowia (P1, IKP itd.).

  1. System P1 (platforma e-zdrowia)

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.

  1. IKP – Internetowe Konto Pacjenta

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.


  1. Podmioty lecznicze (szpitale, przychodnie, gabinety)

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


  1. Sanepid (Państwowa Inspekcja Sanitarna) i inne instytucje nadzoru

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.


  1. GUS (Główny Urząd Statystyczny)

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


  1. ZUS (Zakład Ubezpieczeń Społecznych)

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.


  1. Uczelnie, instytuty badawcze, rejestry medyczne

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


  1. Pracodawcy i medycyna pracy

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.


  1. Firmy prywatne – ubezpieczyciele, abonamenty medyczne, aplikacje

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.




Gdzie szukać?

Mapy Potrzeb Zdrowotnych

Dostępne dashboard’y / analizy na stronie:

Link do MPZ



Link do MPZ




Przykładowa analiza


Physionet


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

Dane


Link do strony


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")

Zrozumienie zbioru

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’).

  • dobdate of birth – data urodzenia (przesunięta w czasie dla anonimizacji).

  • doddate 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).


Opis zbiorów


Łączenie zbiorów

# Łączenie na poziomie hospitalizacji (każdy wiersz = jeden pobyt z cechami pacjenta)
dane <- ADMISSIONS %>%
  left_join(PATIENTS, by = "subject_id")



Analiza


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", "%"))
Rozkład płci
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 hositalizacji na pacjenta
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 pojedynczych i ponownych hospitalizacji
Liczba pobytów Liczba pacjentów
1 86
2 11
3+ 3


Długość pobytu (LOSlength 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")
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")
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")
Ś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).


Dane

Zawiera różne dobrze znane zestawy, m.in.:

  • PimaIndiansDiabetes
  • BreastCancer
  • i inne
# 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 ...


Dokumentacja


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


Eksploracja zbioru


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ę.


Interesujące zagadnienie


“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)


Model

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:

  • \(p = P(\text{cukrzyca} = "pos")\),
  • \(\text{mass}\) to BMI,
  • \(\beta_0\) – wyraz wolny (przesunięcie),
  • \(\beta_1\) – wpływ BMI na log-odds cukrzycy.


Po przekształceniu:

\[ p = \frac{1}{1 + e^{-(\beta_0 + \beta_1 \cdot \text{BMI})}} \]



Podsumowanie modelu

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.

  1. Współczynniki
  • 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).

  1. Dopasowanie modelu
  • 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)


  1. Co to są szanse (odds)?

Dla zdarzenia (np. „ma cukrzycę”) mamy:

  • \(p\) = prawdopodobieństwo zdarzenia
  • \(1 - p\) = prawdopodobieństwo braku zdarzenia

Szanse (odds) definiuje się jako:

\[ \text{odds} = \frac{p}{1-p} \]

Przykład:

  • \(p = 0.2\) (20% prawdopodobieństwa)
  • \(\text{odds} = 0.2 / 0.8 = 0.25\)

Interpretacja: „szanse 1 do 4” (1:4).


  1. Co to jest iloraz szans (odds ratio, OR)?

Iloraz szans porównuje szanse między dwiema grupami:

\[ \text{OR} = \frac{\text{odds w grupie A}}{\text{odds w grupie B}} \]

  • OR = 1 → brak różnicy między grupami
  • OR > 1 → w grupie A szanse zdarzenia są większe niż w B
  • OR < 1 → w grupie A szanse zdarzenia są mniejsze

Przykład z modelu:

  • mass ma OR ≈ 1.098
  • Interpretacja: jeśli BMI wzrośnie o 1 jednostkę, szanse cukrzycy rosną ok. 1.098 raza, czyli o ok. 9.8%.


Dlaczego 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} \]

  • \(\beta_1\) to zmiana log-szans przy wzroście BMI o 1.
  • \(\exp(\beta_1)\) daje iloraz szans (OR), który jest łatwiejszy do interpretacji:
    „przy wzroście BMI o 1 jednostkę, szanse cukrzycy rosną o X%”.

Czyli:

  • Model liczy w tle log-szanse, bo prosta zależność jest liniowa.
  • Funkcja 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.



Przewidywanie

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)



Wyjaśnienia pojęć



  1. 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/↩︎