RMS Titanic opuszcza Southampton, 10 kwietnia 1912 r. Fot. F.G.O. Stuart (domena publiczna, Wikimedia Commons).

RMS Titanic opuszcza Southampton, 10 kwietnia 1912 r. Fot. F.G.O. Stuart (domena publiczna, Wikimedia Commons).


Pakiety i dane

Do analizy potrzebujemy trzech pakietów: {ggplot2} do wykresów, {dplyr} do wygodnej transformacji danych oraz {titanic}, który zawiera data.frame o pasażerach w wersji znanej z konkursu na Kaggle. Korzystamy ze zbioru treningowego (titanic_train).

Dla przyjemnego odbioru wizualnego tabel z podsumowaniami zbioru możemy również użyć pakietów: {skimr} oraz {summarytools}

# install.packages(c("ggplot2", 
#                    "dplyr", 
#                    "titanic", 
#                    "skimr",
#                    "summarytools"))

library(ggplot2)
library(dplyr)
library(titanic)
library(skimr)
library(summarytools)

# Kolory używane na wszystkich wykresach
kolory <- c("Nie" = "#a0442f", 
            "Tak" = "#3d6382")

kolory_plec <- c("Kobiety"   = "#8c6a85",
                 "Mężczyźni" = "#6b7d4f")

kolory_klasy <- c("1. klasa" = "#5c3d24",
                  "2. klasa" = "#a07a52",
                  "3. klasa" = "#cdb083")

# + Paleta "Blues", która pochodzi z pakietu `{RColorBrewer}`, 
# który `{ggplot2}` ma wbudowany, 
# więc nie trzeba niczego dodatkowego ponad to instalować




Opis zmiennych

# dane
titanic <- titanic::titanic_train

Zbiór zawiera 891 pasażerów i 12 oryginalnych zmiennych. Dwie ostatnie zmienne w tabeli (Title i Class) tworzymy samodzielnie w dalszej części analizy. Każdorazowo rozpoczynając pracę z nowym zbiorem, staramy się sprawdzić znaczenie zmiennych w dostępnej dokumentacji.


Zmienna Opis Typ / wartości
PassengerId Identyfikator pasażera liczba całkowita
Survived Czy pasażer przeżył katastrofę? 0 = nie, 1 = tak
Pclass Klasa biletu (przybliżenie statusu społeczno-ekonomicznego) 1 = pierwsza, 2 = druga, 3 = trzecia
Name Imię i nazwisko wraz z tytułem grzecznościowym tekst, np. „Braund, Mr. Owen Harris”
Sex Płeć male, female
Age Wiek w latach (u niemowląt ułamkowy) liczba zmienno przecinkowa (?)
SibSp Liczba rodzeństwa i małżonków na pokładzie liczba całkowita
Parch Liczba rodziców i dzieci na pokładzie liczba całkowita
Ticket Numer biletu tekst
Fare Cena biletu (w funtach brytyjskich) liczba
Cabin Numer kabiny tekst
Embarked Port zaokrętowania C = Cherbourg, Q = Queenstown, S = Southampton
Title Zmienna utworzona: tytuł grzecznościowy z kolumny Name Mr, Mrs, Miss, Master
Class Zmienna utworzona: klasa jako zmienna kategoryczna 1. klasa, 2. klasa, 3. klasa




Pierwszy rzut oka na dane

Zanim zaczniemy cokolwiek zmieniać, warto sprawdzić strukturę ramki danych i podstawowe statystyki (obejrzenie filmu Jamesa Camerona nie jest wystarczające). Funkcja str() pokazuje typy zmiennych, a summary() zakresy wartości i liczbę braków.

Istnieją również inne pakiety, które przygotowują ładnie renderujące się tabele podsumowujące.

# str(titanic)
# summary(titanic)

# `skimr` – zamiennik `str()` i `summary()` w jednym
skimr::skim(titanic) 
Data summary
Name titanic
Number of rows 891
Number of columns 12
_______________________
Column type frequency:
character 5
numeric 7
________________________
Group variables None

Variable type: character

skim_variable n_missing complete_rate min max empty n_unique whitespace
Name 0 1 12 82 0 891 0
Sex 0 1 4 6 0 2 0
Ticket 0 1 3 18 0 681 0
Cabin 0 1 0 15 687 148 0
Embarked 0 1 0 1 2 4 0

Variable type: numeric

skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
PassengerId 0 1.0 446.00 257.35 1.00 223.50 446.00 668.5 891.00 ▇▇▇▇▇
Survived 0 1.0 0.38 0.49 0.00 0.00 0.00 1.0 1.00 ▇▁▁▁▅
Pclass 0 1.0 2.31 0.84 1.00 2.00 3.00 3.0 3.00 ▃▁▃▁▇
Age 177 0.8 29.70 14.53 0.42 20.12 28.00 38.0 80.00 ▂▇▅▂▁
SibSp 0 1.0 0.52 1.10 0.00 0.00 0.00 1.0 8.00 ▇▁▁▁▁
Parch 0 1.0 0.38 0.81 0.00 0.00 0.00 0.0 6.00 ▇▁▁▁▁
Fare 0 1.0 32.20 49.69 0.00 7.91 14.45 31.0 512.33 ▇▁▁▁▁


print(summarytools::dfSummary(titanic, 
                              plain.ascii = FALSE, 
                              style = "grid",
                              graph.magnif = 0.75, 
                              tmp.img.dir = "/tmp"),
      method = "render")

Data Frame Summary

titanic

Dimensions: 891 x 12
Duplicates: 0
No Variable Stats / Values Freqs (% of Valid) Graph Valid Missing
1 PassengerId [integer]
Mean (sd) : 446 (257.4)
min ≤ med ≤ max:
1 ≤ 446 ≤ 891
IQR (CV) : 445 (0.6)
891 distinct values (Integer sequence) 891 (100.0%) 0 (0.0%)
2 Survived [integer]
Min : 0
Mean : 0.4
Max : 1
0:549(61.6%)
1:342(38.4%)
891 (100.0%) 0 (0.0%)
3 Pclass [integer]
Mean (sd) : 2.3 (0.8)
min ≤ med ≤ max:
1 ≤ 3 ≤ 3
IQR (CV) : 1 (0.4)
1:216(24.2%)
2:184(20.7%)
3:491(55.1%)
891 (100.0%) 0 (0.0%)
4 Name [character]
1. Abbing, Mr. Anthony
2. Abbott, Mr. Rossmore Edwa
3. Abbott, Mrs. Stanton (Ros
4. Abelson, Mr. Samuel
5. Abelson, Mrs. Samuel (Han
6. Adahl, Mr. Mauritz Nils M
7. Adams, Mr. John
8. Ahlin, Mrs. Johan (Johann
9. Aks, Mrs. Sam (Leah Rosen
10. Albimona, Mr. Nassef Cass
[ 881 others ]
1(0.1%)
1(0.1%)
1(0.1%)
1(0.1%)
1(0.1%)
1(0.1%)
1(0.1%)
1(0.1%)
1(0.1%)
1(0.1%)
881(98.9%)
891 (100.0%) 0 (0.0%)
5 Sex [character]
1. female
2. male
314(35.2%)
577(64.8%)
891 (100.0%) 0 (0.0%)
6 Age [numeric]
Mean (sd) : 29.7 (14.5)
min ≤ med ≤ max:
0.4 ≤ 28 ≤ 80
IQR (CV) : 17.9 (0.5)
88 distinct values 714 (80.1%) 177 (19.9%)
7 SibSp [integer]
Mean (sd) : 0.5 (1.1)
min ≤ med ≤ max:
0 ≤ 0 ≤ 8
IQR (CV) : 1 (2.1)
0:608(68.2%)
1:209(23.5%)
2:28(3.1%)
3:16(1.8%)
4:18(2.0%)
5:5(0.6%)
8:7(0.8%)
891 (100.0%) 0 (0.0%)
8 Parch [integer]
Mean (sd) : 0.4 (0.8)
min ≤ med ≤ max:
0 ≤ 0 ≤ 6
IQR (CV) : 0 (2.1)
0:678(76.1%)
1:118(13.2%)
2:80(9.0%)
3:5(0.6%)
4:4(0.4%)
5:5(0.6%)
6:1(0.1%)
891 (100.0%) 0 (0.0%)
9 Ticket [character]
1. 1601
2. 347082
3. CA. 2343
4. 3101295
5. 347088
6. CA 2144
7. 382652
8. S.O.C. 14879
9. 113760
10. 113781
[ 671 others ]
7(0.8%)
7(0.8%)
7(0.8%)
6(0.7%)
6(0.7%)
6(0.7%)
5(0.6%)
5(0.6%)
4(0.4%)
4(0.4%)
834(93.6%)
891 (100.0%) 0 (0.0%)
10 Fare [numeric]
Mean (sd) : 32.2 (49.7)
min ≤ med ≤ max:
0 ≤ 14.5 ≤ 512.3
IQR (CV) : 23.1 (1.5)
248 distinct values 891 (100.0%) 0 (0.0%)
11 Cabin [character]
1. (Empty string)
2. B96 B98
3. C23 C25 C27
4. G6
5. C22 C26
6. D
7. E101
8. F2
9. F33
10. B18
[ 138 others ]
687(77.1%)
4(0.4%)
4(0.4%)
4(0.4%)
3(0.3%)
3(0.3%)
3(0.3%)
3(0.3%)
3(0.3%)
2(0.2%)
175(19.6%)
891 (100.0%) 0 (0.0%)
12 Embarked [character]
1. (Empty string)
2. C
3. Q
4. S
2(0.2%)
168(18.9%)
77(8.6%)
644(72.3%)
891 (100.0%) 0 (0.0%)

Generated by summarytools 1.1.5 (R version 4.6.1)
2026-10-22




Czyszczenie danych

Ujednolicenie braków danych

W tym zbiorze braki danych są zapisane na dwa różne sposoby: jako NA (w kolumnie Age) oraz jako pusty napis "" (w kolumnach Cabin i Embarked). Funkcja is.na() nie rozpoznaje pustych napisów, więc pierwsze zliczenie braków jest mylące. Zamieniamy puste napisy na NA i liczymy braki ponownie.

colSums(is.na(titanic))
## PassengerId    Survived      Pclass        Name         Sex         Age 
##           0           0           0           0           0         177 
##       SibSp       Parch      Ticket        Fare       Cabin    Embarked 
##           0           0           0           0           0           0
titanic[titanic == ""] <- NA

colSums(is.na(titanic))
## PassengerId    Survived      Pclass        Name         Sex         Age 
##           0           0           0           0           0         177 
##       SibSp       Parch      Ticket        Fare       Cabin    Embarked 
##           0           0           0           0         687           2

Po ujednoliceniu okazuje się, że kolumna Cabin ma braki dla zdecydowanej większości pasażerów, a Embarked tylko dla dwóch.


Tworzenie nowych zmiennych

Tytuł grzecznościowy

Kolumna Name ma format „Nazwisko, Tytuł. Imiona”, więc tytuł możemy wyciągnąć wyrażeniem regularnym jako tekst między przecinkiem a pierwszą kropką. Tytuł niesie informację o płci, stanie cywilnym i (w przypadku chłopców) o wieku. „Master” to dawna angielska forma grzecznościowa dla chłopców do około 12–13 roku życia, dzięki czemu pozwala odróżnić dzieci od dorosłych mężczyzn nawet wtedy, gdy brakuje wieku.

titanic$Title <- sub(".*, ([^.]+)\\..*", "\\1", titanic$Name)
unique(titanic$Title)
##  [1] "Mr"           "Mrs"          "Miss"         "Master"       "Don"         
##  [6] "Rev"          "Dr"           "Mme"          "Ms"           "Major"       
## [11] "Lady"         "Sir"          "Mlle"         "Col"          "Capt"        
## [16] "the Countess" "Jonkheer"
table(titanic$Title)
## 
##         Capt          Col          Don           Dr     Jonkheer         Lady 
##            1            2            1            7            1            1 
##        Major       Master         Miss         Mlle          Mme           Mr 
##            2           40          182            2            1          517 
##          Mrs           Ms          Rev          Sir the Countess 
##          125            1            6            1            1

Część tytułów występuje tylko kilka razy, co utrudniałoby analizę. Francuskie odpowiedniki (Mlle, Mme) i Ms przypisujemy do Miss i Mrs, a pozostałe rzadkie tytuły (wojskowe, duchowne, arystokratyczne, Dr) rozdzielamy według płci: mężczyzn do „Mr”, kobiety do „Mrs”, bo wszystkie te osoby były dorosłe.

titanic$Title[titanic$Title %in% c("Mlle", "Ms")] <- "Miss"
titanic$Title[titanic$Title == "Mme"] <- "Mrs"
titanic$Title[!titanic$Title %in% c("Mr", "Mrs", "Miss", "Master")] <- "Rare"

titanic$Title[titanic$Title == "Rare" & titanic$Sex == "male"]   <- "Mr"
titanic$Title[titanic$Title == "Rare" & titanic$Sex == "female"] <- "Mrs"

table(titanic$Title)
## 
## Master   Miss     Mr    Mrs 
##     40    185    537    129

Typy danych

Niektóre zmienne, o wyraźnej cesze kategorialnej zamieniamy na czynniki (factor). Dzięki temu ggplot2 traktuje je jako kategorie, a nie wartości liczbowe. Przy okazji nadajemy poziomom polskie etykiety, które pojawią się na wykresach i w nagłówkach paneli.

titanic <- titanic %>%
  mutate(Survived = factor(Survived, 
                           levels = c(0, 1), 
                           labels = c("Nie", "Tak")),
         Class    = factor(Pclass, 
                           levels = 1:3,
                           labels = c("1. klasa", "2. klasa", "3. klasa")),
         Sex      = factor(Sex, 
                           levels = c("female", "male"),
                           labels = c("Kobiety", "Mężczyźni")),
         Embarked = factor(Embarked),
         Title    = factor(Title, 
                           levels = c("Mr", "Mrs", "Miss", "Master")))

str(titanic)
## 'data.frame':    891 obs. of  14 variables:
##  $ PassengerId: int  1 2 3 4 5 6 7 8 9 10 ...
##  $ Survived   : Factor w/ 2 levels "Nie","Tak": 1 2 2 2 1 1 1 1 2 2 ...
##  $ Pclass     : int  3 1 3 1 3 3 1 3 3 2 ...
##  $ Name       : chr  "Braund, Mr. Owen Harris" "Cumings, Mrs. John Bradley (Florence Briggs Thayer)" "Heikkinen, Miss. Laina" "Futrelle, Mrs. Jacques Heath (Lily May Peel)" ...
##  $ Sex        : Factor w/ 2 levels "Kobiety","Mężczyźni": 2 1 1 1 2 2 2 2 1 1 ...
##  $ Age        : num  22 38 26 35 35 NA 54 2 27 14 ...
##  $ SibSp      : int  1 1 0 1 0 0 0 3 0 1 ...
##  $ Parch      : int  0 0 0 0 0 0 0 1 2 0 ...
##  $ Ticket     : chr  "A/5 21171" "PC 17599" "STON/O2. 3101282" "113803" ...
##  $ Fare       : num  7.25 71.28 7.92 53.1 8.05 ...
##  $ Cabin      : chr  NA "C85" NA "C123" ...
##  $ Embarked   : Factor w/ 3 levels "C","Q","S": 3 1 3 3 3 2 3 3 3 1 ...
##  $ Title      : Factor w/ 4 levels "Mr","Mrs","Miss",..: 1 2 3 2 1 1 1 4 2 2 ...
##  $ Class      : Factor w/ 3 levels "1. klasa","2. klasa",..: 3 1 3 1 3 3 1 3 3 2 ...


Możemy podejrzeć jeszcze innym pakietem nasze “przypudrowane” dane.

library(gtsummary)
titanic %>%
  select(Survived, Sex, Age, Class, Fare, Title, Embarked) %>%
  tbl_summary()
Characteristic N = 8911
Survived
    Nie 549 (62%)
    Tak 342 (38%)
Sex
    Kobiety 314 (35%)
    Mężczyźni 577 (65%)
Age 28 (20, 38)
    Unknown 177
Class
    1. klasa 216 (24%)
    2. klasa 184 (21%)
    3. klasa 491 (55%)
Fare 14 (8, 31)
Title
    Mr 537 (60%)
    Mrs 129 (14%)
    Miss 185 (21%)
    Master 40 (4.5%)
Embarked
    C 168 (19%)
    Q 77 (8.7%)
    S 644 (72%)
    Unknown 2
1 n (%); Median (Q1, Q3)


Albo rozpatrywać podsumowania zmiennych już pod kątem przeżycia tragedii na morzu.

titanic %>%
  select(Survived, Sex, Age, Class, Fare, Title, Embarked) %>%
  tbl_summary(by = Survived) %>%
  add_p()
Characteristic Nie
N = 549
1
Tak
N = 342
1
p-value2
Sex

<0.001
    Kobiety 81 (15%) 233 (68%)
    Mężczyźni 468 (85%) 109 (32%)
Age 28 (21, 39) 28 (19, 36) 0.2
    Unknown 125 52
Class

<0.001
    1. klasa 80 (15%) 136 (40%)
    2. klasa 97 (18%) 87 (25%)
    3. klasa 372 (68%) 119 (35%)
Fare 11 (8, 26) 26 (12, 57) <0.001
Title

<0.001
    Mr 451 (82%) 86 (25%)
    Mrs 26 (4.7%) 103 (30%)
    Miss 55 (10%) 130 (38%)
    Master 17 (3.1%) 23 (6.7%)
Embarked

<0.001
    C 75 (14%) 93 (27%)
    Q 47 (8.6%) 30 (8.8%)
    S 427 (78%) 217 (64%)
    Unknown 0 2
1 n (%); Median (Q1, Q3)
2 Pearson’s Chi-squared test; Wilcoxon rank sum test




Charakterystyka pasażerów

W tej części przyglądamy się samym pasażerom: kim byli, jak podróżowali i skąd wsiadali na statek.


Płeć pasażerów

Dla zmiennej kategorycznej, takiej jak płeć, odpowiednikiem histogramu jest wykres słupkowy (geom_bar()), który zlicza obserwacje w każdej kategorii.

ggplot(titanic, aes(x = Sex, 
                    fill = Sex)) +
  theme_bw() +
  geom_bar(show.legend = FALSE) +
  geom_text(stat = "count", 
            aes(label = after_stat(count)),
            vjust = -0.5) +
  scale_fill_manual(values = kolory_plec) +
  labs(x = "Płeć",
       y = "Liczba pasażerów",
       title = "Liczba pasażerów według płci")

Mężczyźni stanowili wyraźną większość: było ich prawie dwa razy więcej niż kobiet (0 wobec 0).


Płeć pasażerów – wykres kołowy

# W `ggplot2` nie ma osobnej funkcji do wykresów kołowych. 
# Tworzy się je jako pojedynczy słupek skumulowany, 
# który następnie „zawija się” w koło za pomocą 
# współrzędnych biegunowych (`coord_polar()`). 
# Najpierw liczymy liczebności i udziały procentowe.

plec <- titanic %>%
  count(Sex) %>%
  mutate(udzial = n / sum(n),
         etykieta = paste0(Sex, "\n", 
                           scales::percent(udzial, 
                                           accuracy = 0.1)))

ggplot(plec, aes(x = "", y = n, fill = Sex)) +
  geom_col(width = 1, color = "white")+ 
  coord_polar(theta = "y") +
  geom_text(aes(label = etykieta), 
            position = position_stack(vjust = 0.5),
            color = "white", size = 5) +
  scale_fill_manual(values = kolory_plec) +
  theme_void() +
  theme(plot.background = element_rect(fill = "#f3ead7", colour = NA)) +
  theme(legend.position = "none") +
  labs(title = "Struktura pasażerów według płci")

Wykres kołowy dobrze pokazuje proporcję „część z całości” przy dwóch, trzech kategoriach: mężczyźni to blisko 64.8% pasażerów. Przy większej liczbie kategorii wykres słupkowy jest zwykle czytelniejszy, bo łatwiej porównuje się długości niż kąty.


Klasa a płeć

# Wykres słupkowy z podziałem na płeć pozwala sprawdzić, 
# czy struktura pasażerów różniła się między klasami.
ggplot(titanic, aes(x = Class, 
                    fill = Sex)) +
  theme_bw() +
  geom_bar(position = "dodge") +
  scale_fill_manual(values = kolory_plec, 
                    name = "Płeć") +
  labs(x = "Klasa",
       y = "Liczba pasażerów",
       title = "Liczba pasażerów według klasy i płci")

Trzecia klasa była najliczniejsza (ponad połowa wszystkich pasażerów) i to tam przewaga mężczyzn była największa. W pierwszej klasie proporcje płci były znacznie bardziej wyrównane.


Rozkład wieku według płci

# Wykres gęstości to wygładzona wersja histogramu. 
# Nakładając na siebie dwie półprzezroczyste krzywe, 
# łatwo porównać kształt rozkładów niezależnie od tego, 
# że grupy różnią się liczebnością.
ggplot(titanic, aes(x = Age, 
                    fill = Sex)) +
  theme_bw() +
  geom_density(alpha = 0.5) +
  scale_fill_manual(values = kolory_plec, 
                    name = "Płeć") +
  labs(x = "Wiek pasażera",
       y = "Gęstość",
       title = "Rozkład wieku kobiet i mężczyzn")

Oba rozkłady mają szczyt w okolicach 20–30 lat, czyli na statku dominowali młodzi dorośli. Pasażerowie bez podanego wieku są tu pominięci.


Ceny biletów w poszczególnych klasach

Ceny biletów są bardzo silnie prawostronnie skośne: większość biletów była tania, a kilka kosztowało ponad 500 funtów. Dlatego oś Y przedstawiamy w skali logarytmicznej (log1p, która poprawnie obsługuje bilety o cenie 0).

ggplot(titanic, aes(x = Class, 
                    y = Fare, 
                    fill = Class)) +
  theme_bw() +
  geom_boxplot(show.legend = FALSE) +
  scale_y_continuous(trans = "log1p", 
                     breaks = c(0, 5, 10, 25, 50, 100, 250, 500)) +
  scale_fill_manual(values = kolory_klasy, name = "Klasa") +
  labs(x = "Klasa",
       y = "Cena biletu (£, skala logarytmiczna)",
       title = "Ceny biletów według klasy")


Bez zabiegu logarytmowania.

ggplot(titanic, aes(x = Class, 
                    y = Fare, 
                    fill = Class)) +
  theme_bw() +
  geom_boxplot(show.legend = FALSE) +
  scale_y_continuous() +
  scale_fill_manual(values = kolory_klasy) +
  labs(x = "Klasa",
       y = "Cena biletu (£)",
       title = "Ceny biletów według klasy")

Różnice między klasami są ogromne: typowy bilet pierwszej klasy kosztował kilkukrotnie więcej niż bilet trzeciej. Wewnątrz klas również widać duży rozrzut, częściowo dlatego, że cena dotyczy całego biletu, który mógł obejmować kilka osób.


Port zaokrętowania

Titanic zabierał pasażerów w trzech portach: Southampton (Anglia), Cherbourg (Francja) i Queenstown (dziś Cobh w Irlandii). Sprawdzamy, ile osób wsiadło w każdym z nich i z jakich klas pochodzili. Dwóch pasażerów bez informacji o porcie pomijamy.

titanic %>%
  filter(!is.na(Embarked)) %>%
  mutate(Port = factor(Embarked, 
                       levels = c("S", "C", "Q"),
                       labels = c("Southampton",
                                  "Cherbourg",
                                  "Queenstown"))) %>%
  ggplot(aes(x = Port, 
             fill = Class)) +
  theme_bw() +
  geom_bar() +
  scale_fill_manual(values = kolory_klasy, name = "Klasa") +
  labs(x = "Port zaokrętowania",
       y = "Liczba pasażerów",
       title = "Pasażerowie według portu zaokrętowania i klasy")

Zdecydowana większość pasażerów wsiadła w Southampton. W Cherbourgu stosunkowo dużą część stanowili pasażerowie pierwszej klasy, natomiast w Queenstown wsiadali niemal wyłącznie pasażerowie trzeciej klasy, w dużej mierze irlandzcy emigranci.




Przeżycie

Wielkość rodziny

Zmienne SibSp (rodzeństwo i małżonkowie) oraz Parch (rodzice i dzieci) można połączyć w jedną zmienną opisującą wielkość rodziny podróżującej razem. Dodajemy 1, żeby uwzględnić samego pasażera.

titanic <- titanic %>%
  mutate(FamilySize = SibSp + Parch + 1)

ggplot(titanic, aes(x = factor(FamilySize), 
                    fill = Survived)) +
  theme_bw() +
  geom_bar() +
  scale_fill_manual(values = kolory, 
                    name = "Przeżył(a)") +
  labs(x = "Wielkość rodziny (łącznie z pasażerem)",
       y = "Liczba pasażerów",
       title = "Wielkość rodziny a przeżycie")

Większość pasażerów podróżowała samotnie i ta grupa miała stosunkowo niskie szanse przeżycia. Najlepiej radziły sobie małe rodziny (2–4 osoby), natomiast duże rodziny, zwykle z trzeciej klasy, zginęły niemal w całości. Zależność jest więc nieliniowa, co warto pamiętać przy budowie modelu.


Odsetek ocalałych według klasy i tytułu

Wykres słupkowy z position = "fill" pokazuje proporcje zamiast liczebności, więc łatwo porównać szanse przeżycia między grupami o bardzo różnej wielkości.

ggplot(titanic, aes(x = Title, 
                    fill = Survived)) +
  theme_bw() +
  facet_grid(~ Class) +
  geom_bar(position = "fill") +
  scale_fill_manual(values = kolory, 
                    name = "Przeżył(a)") +
  scale_y_continuous(labels = scales::percent) +
  labs(x = "Tytuł pasażera",
       y = "Odsetek pasażerów",
       title = "Odsetek ocalałych według klasy i tytułu")

Wyraźnie widać zasadę „najpierw kobiety i dzieci”, ale też jej ograniczenia. W pierwszej i drugiej klasie niemal wszystkie kobiety (Mrs, Miss) oraz chłopcy (Master) przeżyli, natomiast w trzeciej klasie ich szanse spadają do mniej więcej połowy. Dorośli mężczyźni (Mr) mieli najmniejsze szanse we wszystkich klasach, choć w pierwszej klasie wyraźnie większe niż w drugiej i trzeciej.


Wiek a cena biletu

Wykres punktowy pozwala zobaczyć jednocześnie pięć zmiennych: wiek, cenę biletu, przeżycie (kolor) oraz płeć i klasę (panele).

Lekkie rozrzucenie punktów (jitter) zmniejsza ich nakładanie się.

ggplot(titanic, aes(x = Age, 
                    y = Fare, 
                    color = Survived)) +
  theme_bw() +
  facet_grid(Sex ~ Class) +
  geom_point(position = "jitter",
             size = 2, 
             alpha = 0.8) +
  scale_color_manual(values = kolory,
                     name = "Przeżył(a)") +
  labs(x = "Wiek pasażera",
       y = "Cena biletu (£)",
       title = "Wiek i cena biletu według przeżycia, płci i klasy")

Ceny biletów w pierwszej klasie są wielokrotnie wyższe niż w pozostałych i bardzo zróżnicowane, z kilkoma skrajnie drogimi biletami. Panele kobiet z pierwszej i drugiej klasy są niemal w całości niebieskie, a panele mężczyzn z drugiej i trzeciej klasy w przeważającej części czerwone. Pasażerowie bez podanego wieku nie są widoczni na tym wykresie.


Rozkład wieku

Histogram pokazuje, jak rozkładał się wiek pasażerów w poszczególnych grupach i jaka część każdego przedziału wiekowego przeżyła.

ggplot(titanic, aes(x = Age, 
                    fill = Survived)) +
  theme_bw() +
  facet_grid(Sex ~ Class) +
  geom_histogram(binwidth = 5) +
  scale_fill_manual(values = kolory, 
                    name = "Przeżył(a)") +
  labs(x = "Wiek pasażera",
       y = "Liczba pasażerów",
       title = "Rozkład wieku według przeżycia, płci i klasy")

Najliczniejszą grupą na statku byli młodzi mężczyźni w wieku 20–35 lat podróżujący trzecią klasą i to właśnie w tej grupie zginęło najwięcej osób. Wśród mężczyzn w drugiej klasie przeżywali głównie najmłodsi chłopcy, co dobrze zgadza się z wcześniejszym obserwowanym efektem tytułu „Master”.


Wiek ocalałych i ofiar

Wykres pudełkowy porównuje rozkład wieku osób, które przeżyły i które zginęły, w każdej kombinacji płci i klasy.

ggplot(titanic, aes(x = Survived, 
                    y = Age, 
                    fill = Survived)) +
  theme_bw() +
  facet_grid(Sex ~ Class) +
  geom_boxplot(show.legend = FALSE) +
  scale_fill_manual(values = kolory) +
  labs(x = "Przeżył(a)",
       y = "Wiek pasażera",
       title = "Wiek pasażerów według przeżycia, płci i klasy")

U mężczyzn ocaleni są zazwyczaj młodsi od ofiar, co widać szczególnie w drugiej klasie, gdzie mediana wieku ocalałych mężczyzn jest bardzo niska. Warto też zauważyć, że pasażerowie pierwszej klasy byli przeciętnie starsi niż pasażerowie trzeciej.


Liczba ocalałych według klasy i tytułu

Na koniec ten sam podział co na pierwszym wykresie, ale w liczbach bezwzględnych. Uzupełnia on wykres proporcji, bo pokazuje, jak liczne były poszczególne grupy.

ggplot(titanic, aes(x = Title, 
                    fill = Survived)) +
  theme_bw() +
  facet_grid(~ Class) +
  geom_bar() +
  scale_fill_manual(values = kolory, 
                    name = "Przeżył(a)") +
  labs(x = "Tytuł pasażera",
       y = "Liczba pasażerów",
       title = "Liczba ocalałych według klasy i tytułu")

Ten wykres uświadamia skalę tragedii: zdecydowana większość ofiar to dorośli mężczyźni z trzeciej klasy. Wysoki odsetek ocalałych chłopców w pierwszej klasie dotyczy zaledwie kilku osób, natomiast w trzeciej klasie zginęło kilkaset osób.




Korelacje zmiennych

zmienne_num <- titanic %>%
  transmute(`Przeżycie`           = as.numeric(Survived == "Tak"),
            `Klasa`               = Pclass,
            `Płeć (mężczyzna)`    = as.numeric(Sex == "Mężczyźni"),
            `Wiek`                = Age,
            `Rodzeństwo/małżonek` = SibSp,
            `Rodzice/dzieci`      = Parch,
            `Cena biletu`         = Fare)

kor <- cor(zmienne_num, use = "pairwise.complete.obs")


# Zostawiamy tylko dolny trójkąt, bo macierz jest symetryczna
kor_dolny <- kor
kor_dolny[upper.tri(kor_dolny)] <- NA

kor_dlugi <- as.data.frame(as.table(kor_dolny)) %>%
  filter(!is.na(Freq))

ggplot(kor_dlugi, aes(x = Var2, y = Var1, fill = Freq)) +
  geom_tile(color = "white") +
  geom_text(aes(label = sprintf("%.2f", Freq)), size = 4) +
  scale_fill_gradient2(low = "#a0442f", mid = "white", high = "#3d6382",
                       midpoint = 0, limits = c(-1, 1), name = "Korelacja") +
  scale_y_discrete(limits = rev) +
  coord_fixed() +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1),
        panel.grid = element_blank()) +
  theme(plot.background = element_rect(fill = "#f3ead7", colour = NA)) +
  labs(x = NULL, y = NULL,
       title = "Macierz korelacji zmiennych")

Najsilniej z przeżyciem związana jest płeć: korelacja z byciem mężczyzną jest wyraźnie ujemna (około −0,54). Na drugim miejscu jest klasa: im wyższy numer klasy (czyli gorsza klasa), tym mniejsze szanse przeżycia. Dodatnia korelacja przeżycia z ceną biletu jest w dużej mierze odbiciem tego samego zjawiska, bo cena i klasa są ze sobą silnie powiązane. Wiek sam w sobie koreluje z przeżyciem bardzo słabo, co nie znaczy, że jest nieistotny: jego wpływ jest nieliniowy (dzieci przeżywały częściej) i zależy od płci, czego współczynnik korelacji nie wychwytuje.

Spośród pozostałych par warto zauważyć, że pasażerowie pierwszej klasy byli przeciętnie starsi (ujemna korelacja klasy z wiekiem), a osoby podróżujące z rodzeństwem lub rodzicami były zwykle młodsze, co ma sens, bo to często dzieci podróżujące z rodzinami.

Trzeba pamiętać, że współczynnik Pearsona jest tu stosowany do zmiennych zero-jedynkowych i porządkowych (klasa), więc wyniki należy traktować jako przybliżony obraz zależności, a nie precyzyjną miarę.

# install.packages("ggcorrplot")

# W jednej linijce:
ggcorrplot::ggcorrplot(kor, type = "lower", lab = TRUE,
                       colors = c("#a0442f", "white", "#3d6382"),
                       title = "Macierz korelacji zmiennych") +
  theme(plot.background = element_rect(fill = "#f3ead7", colour = NA))




Testy chi-kwadrat

Na czym polega test


Wykresy sugerują, że płeć, klasa czy tytuł wiązały się z szansą przeżycia.

Test niezależności chi-kwadrat (χ²) pozwala sprawdzić formalnie, czy taki związek mógłby być dziełem przypadku.

Test porównuje liczebności obserwowane w tabeli krzyżowej z liczebnościami oczekiwanymi, czyli takimi, jakie wystąpiłyby, gdyby obie zmienne były od siebie zupełnie niezależne. Im większe różnice między nimi, tym większa wartość statystyki χ².

  • Hipoteza zerowa (H₀): przeżycie nie zależy od badanej zmiennej.
  • Hipoteza alternatywna (H₁): przeżycie zależy od badanej zmiennej.


Jeśli wartość p jest mniejsza od przyjętego poziomu istotności (zwykle 0,05), odrzucamy H₀. Warunkiem poprawności testu jest, by liczebności oczekiwane w każdej komórce tabeli wynosiły co najmniej 5.

Sama wartość p mówi tylko, czy związek istnieje, ale nie jak jest silny (przy dużej próbie nawet słaby związek bywa istotny). Dlatego dodatkowo liczymy V Craméra, miarę siły związku od 0 (brak) do 1 (pełna zależność). Orientacyjnie: około 0,1 to związek słaby, 0,3 umiarkowany, a 0,5 i więcej silny.


Przykład krok po kroku: płeć

Najpierw tabela krzyżowa oraz odsetki ocalałych w każdej grupie.

tab_plec <- table(titanic$Sex, titanic$Survived)
tab_plec
##            
##             Nie Tak
##   Kobiety    81 233
##   Mężczyźni 468 109
round(prop.table(tab_plec, margin = 1) * 100, 1)
##            
##              Nie  Tak
##   Kobiety   25.8 74.2
##   Mężczyźni 81.1 18.9

Następnie liczebności oczekiwane przy założeniu niezależności, które pozwalają sprawdzić warunek testu.

chisq.test(tab_plec)$expected
##            
##                  Nie      Tak
##   Kobiety   193.4747 120.5253
##   Mężczyźni 355.5253 221.4747

Wszystkie wartości oczekiwane są znacznie większe od 5, więc możemy przeprowadzić test.

chisq.test(tab_plec)
## 
##  Pearson's Chi-squared test with Yates' continuity correction
## 
## data:  tab_plec
## X-squared = 260.72, df = 1, p-value < 2.2e-16

Dla tabeli 2×2 R automatycznie stosuje poprawkę Yatesa na ciągłość, która nieco zmniejsza wartość statystyki. Statystyka wynosi około 260,7 przy 1 stopniu swobody (df), a wartość p jest praktycznie równa zeru. Odrzucamy hipotezę zerową: przeżycie silnie zależało od płci. Przeżyło około 74% kobiet i tylko około 19% mężczyzn.


Zestawienie testów dla wszystkich zmiennych

Ten sam test wykonujemy dla płci, klasy, tytułu i portu zaokrętowania, a wyniki zbieramy w jednej tabeli. V Craméra liczymy z wersji testu bez poprawki Yatesa, tak jak wymaga tego definicja tej miary. Braki danych (dwóch pasażerów bez portu) są automatycznie pomijane przez funkcję table().

library(kableExtra)

cramer_v <- function(tab) {
  chi <- suppressWarnings(chisq.test(tab, correct = FALSE))$statistic
  unname(sqrt(chi / (sum(tab) * (min(dim(tab)) - 1))))
}

zmienne_test <- c(Sex = "Płeć", Class = "Klasa", Title = "Tytuł",
                  Embarked = "Port zaokrętowania")

wyniki_chi <- lapply(names(zmienne_test), function(v) {
  tab  <- table(titanic[[v]], titanic$Survived)
  test <- chisq.test(tab)
  data.frame(
    Zmienna = zmienne_test[[v]],
    Chi2 = round(unname(test$statistic), 2),
    df = unname(test$parameter),
    p = formatC(test$p.value, format = "e", digits = 1),
    V = round(cramer_v(tab), 3),
    min_oczekiwana = round(min(test$expected), 1)
  )
}) %>% bind_rows()

kbl(wyniki_chi,
    col.names = c("Zmienna", "χ²", "Stopnie swobody", "Wartość p",
                  "V Craméra", "Min. liczebność oczekiwana"),
    align = "lccccc") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Zmienna χ² Stopnie swobody Wartość p V Craméra Min. liczebność oczekiwana
Płeć 260.72 1 1.2e-58 0.543 120.5
Klasa 102.89 2 4.5e-23 0.340 70.6
Tytuł 293.09 3 3.1e-63 0.574 15.4
Port zaokrętowania 26.49 2 1.8e-06 0.173 29.4

Wszystkie cztery zmienne są istotnie związane z przeżyciem, a warunek liczebności oczekiwanych jest wszędzie spełniony. Różnią się jednak wyraźnie siłą związku:

  • Tytuł (V ≈ 0,57) i płeć (V ≈ 0,54) mają związek silny. To, że tytuł wypada nieco lepiej niż sama płeć, ma sens: niesie on tę samą informację o płci, a dodatkowo odróżnia chłopców (Master) od dorosłych mężczyzn.

  • Klasa (V ≈ 0,34) ma związek umiarkowany. Odsetek ocalałych spada z około 63% w pierwszej klasie, przez 47% w drugiej, do 24% w trzeciej.

  • Port zaokrętowania (V ≈ 0,17) ma związek słaby, choć istotny statystycznie.


Gdzie leżą różnice: reszty standaryzowane

Test χ² mówi, że zależność istnieje, ale nie wskazuje, które grupy się wyróżniają. Pomagają w tym reszty standaryzowane: wartości powyżej 2 (lub poniżej −2) oznaczają komórki, w których obserwacji jest wyraźnie więcej (lub mniej), niż wynikałoby z niezależności.

round(chisq.test(table(titanic$Title, titanic$Survived))$stdres, 1)
##         
##            Nie   Tak
##   Mr      16.9 -16.9
##   Mrs    -10.5  10.5
##   Miss   -10.0  10.0
##   Master  -2.5   2.5
round(chisq.test(table(titanic$Class, titanic$Survived))$stdres, 1)
##           
##             Nie  Tak
##   1. klasa -8.5  8.5
##   2. klasa -2.8  2.8
##   3. klasa  9.6 -9.6

W przypadku tytułu zdecydowanie wyróżnia się grupa Mr: ofiar było w niej znacznie więcej, niż oczekiwalibyśmy przy niezależności (reszta około 17). Kobiety (Mrs i Miss) mają z kolei wyraźną nadwyżkę ocalałych. Chłopcy (Master) też przeżywali częściej, niż wynikałoby z przypadku, ale ich reszta jest dużo mniejsza (około 2,5). Wynika to zarówno z małej liczebności tej grupy, jak i z tego, że chłopcy z trzeciej klasy często nie przeżyli.

Dla klasy wyraźnie wyróżniają się dwa skrajne poziomy: w pierwszej klasie ocalałych jest więcej, niż wynikałoby z niezależności, a w trzeciej mniej. Druga klasa jest bliżej środka.


Ograniczenia

Test χ² bada każdą zmienną osobno, więc nie potrafi oddzielić efektów zmiennych powiązanych ze sobą. Dobrym przykładem jest port zaokrętowania: wśród pasażerów z Cherbourga przeżyło ponad 55%, wśród tych z Southampton około 34%. Nie oznacza to jednak, że sam port miał znaczenie. W Cherbourgu wsiadło stosunkowo wielu pasażerów pierwszej klasy, więc ten związek jest najprawdopodobniej odbiciem efektu klasy. Rozdzielić takie efekty pozwala dopiero model uwzględniający kilka zmiennych jednocześnie, np. regresja logistyczna.




Uzupełnianie brakujących wartości wieku

Czym jest imputacja

Imputacja to zastępowanie brakujących wartości oszacowaniami. W naszych danych wieku brakuje dla 177 z 891 pasażerów, czyli prawie co piątej osoby. Większość metod statystycznych, w tym regresja logistyczna, po prostu pomija obserwacje z brakami. Oznaczałoby to utratę około 20% danych i potencjalne zniekształcenie wyników, jeśli braki nie są rozłożone losowo.

Najprostsze podejścia to:

  • usunięcie obserwacji z brakami, co jest proste, ale marnuje dane;

  • wstawienie średniej lub mediany z całej próby, co jest proste, ale każdemu, także dziecku, przypisuje wiek dorosłego;

  • wstawienie mediany w podgrupach, czyli wykorzystanie innych zmiennych powiązanych z wiekiem;

  • imputacja modelowa (np. pakiet {mice} lub drzewo regresyjne), która przewiduje wiek na podstawie wielu zmiennych jednocześnie.

Wybieramy trzecie podejście, czyli medianę wieku w grupach wyznaczonych przez tytuł i klasę. Tytuł niesie dużo informacji o wieku (Master to chłopcy, Miss to często młode kobiety), a klasa jest z wiekiem powiązana, co widzieliśmy na macierzy korelacji. Medianę wybieramy zamiast średniej, bo jest odporna na wartości skrajne.


Mediany wieku w grupach

Zanim cokolwiek zmienimy, sprawdzamy, jak wyglądają grupy: ile w nich jest osób, ilu brakuje wieku i jakie wartości zostaną wstawione.

mediany_wieku <- titanic %>%
  group_by(Title, Class) %>%
  summarise(Liczba = n(),
            Braki = sum(is.na(Age)),
            Mediana = median(Age, na.rm = TRUE),
            .groups = "drop")

kbl(mediany_wieku,
    col.names = c("Tytuł", 
                  "Klasa", 
                  "Liczba pasażerów", 
                  "Braki wieku",
                  "Mediana wieku"),
    align = "llccc") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), 
                full_width = FALSE)
Tytuł Klasa Liczba pasażerów Braki wieku Mediana wieku
Mr
  1. klasa
119 21 42.0
Mr
  1. klasa
99 9 31.0
Mr
  1. klasa
319 90 26.0
Mrs
  1. klasa
46 8 41.5
Mrs
  1. klasa
41 0 32.0
Mrs
  1. klasa
42 9 31.0
Miss
  1. klasa
48 1 30.0
Miss
  1. klasa
35 2 24.0
Miss
  1. klasa
102 33 18.0
Master
  1. klasa
3 0 4.0
Master
  1. klasa
9 0 1.0
Master
  1. klasa
28 4 4.0

Mediany wyraźnie się różnią: chłopcy (Master) mają po kilka lat, a dorośli mężczyźni z pierwszej klasy około 42 lat. Gdybyśmy wstawili jedną wspólną medianę (28 lat), chłopiec bez podanego wieku zostałby potraktowany jak dorosły. Każda grupa ma przynajmniej kilka osób ze znanym wiekiem, więc dla wszystkich da się wyznaczyć medianę. Najwięcej braków (90) dotyczy dorosłych mężczyzn z trzeciej klasy.

Uzupełnienie braków

Przed uzupełnieniem zachowujemy oryginalny wiek w kolumnie Age_orig i dodajemy zmienną AgeImputed, która informuje, czy wartość jest prawdziwa, czy oszacowana. Dzięki temu zawsze można wrócić do danych źródłowych albo sprawdzić, czy imputacja nie wpływa na wyniki.

titanic <- titanic %>%
  mutate(Age_orig = Age,
         AgeImputed = factor(is.na(Age), 
                             levels = c(FALSE, TRUE),
                             labels = c("Oryginalny", "Uzupełniony"))) %>%
  group_by(Title, Class) %>%
  mutate(Age = ifelse(is.na(Age), median(Age, na.rm = TRUE), Age)) %>%
  ungroup()

sum(is.na(titanic$Age))
## [1] 0

Po imputacji w kolumnie Age nie ma już braków.


Ocena skutków imputacji

Imputacja zawsze w jakimś stopniu zmienia rozkład zmiennej, więc warto sprawdzić, jak bardzo. Porównujemy podstawowe statystyki przed i po uzupełnieniu.

porownanie_wieku <- data.frame(
  Statystyka = c("Liczba obserwacji", 
                 "Średnia", 
                 "Mediana",
                 "Odchylenie standardowe"),
  Przed = c(sum(!is.na(titanic$Age_orig)),
            mean(titanic$Age_orig, na.rm = TRUE),
            median(titanic$Age_orig, na.rm = TRUE),
            sd(titanic$Age_orig, na.rm = TRUE)),
  Po = c(sum(!is.na(titanic$Age)),
         mean(titanic$Age),
         median(titanic$Age),
         sd(titanic$Age))
)

kbl(porownanie_wieku, digits = 2,
    col.names = c("Statystyka", 
                  "Przed imputacją", 
                  "Po imputacji"),
    align = "lcc") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), 
                full_width = FALSE)
Statystyka Przed imputacją Po imputacji
Liczba obserwacji 714.00 891.00
Średnia 29.70 29.19
Mediana 28.00 26.00
Odchylenie standardowe 14.53 13.53

Średnia zmieniła się nieznacznie (z około 29,7 do 29,2 roku), ale mediana spadła z 28 do 26 lat, a odchylenie standardowe zmalało z około 14,5 do 13,5. To typowy efekt imputacji medianą: wstawiając wiele identycznych wartości, sztucznie zmniejszamy zmienność danych. Spadek mediany wynika z tego, że braki dotyczyły głównie młodych mężczyzn i kobiet z trzeciej klasy.

Najlepiej widać to na histogramie, na którym kolorem oznaczamy uzupełnione wartości.

ggplot(titanic, aes(x = Age, fill = AgeImputed)) +
  theme_bw() +
  geom_histogram(binwidth = 2) +
  scale_fill_manual(values = c("Oryginalny" = "#95a5a6", 
                               "Uzupełniony" = "#e67e22"),
                    name = "Wiek") +
  labs(x = "Wiek pasażera",
       y = "Liczba pasażerów",
       title = "Rozkład wieku po uzupełnieniu braków")

Uzupełnione wartości tworzą wyraźne „szpilki” w kilku miejscach, przede wszystkim przy 26 latach (mediana dla dorosłych mężczyzn z trzeciej klasy, wstawiona aż 90 razy). Dla celów modelowania takie uproszczenie jest zwykle akceptowalne, ale trzeba pamiętać, że zmienność wieku jest teraz nieco zaniżona. Bardziej zaawansowane metody, np. wielokrotna imputacja pakietem mice, wstawiają wartości z losowym rozrzutem i lepiej zachowują kształt rozkładu.




Dalsze analizy wkrótce