Titanic - analiza danych¶

Wstęp - o danych¶

Dane o pasażerach Titanica

Zbiór danych zawiera informacje o pasażerach RMS Titanic, który zatonął 15 kwietnia 1912 roku po zderzeniu z górą lodową. Dane obejmują takie atrybuty jak klasa podróży, wiek, płeć, liczba rodzeństwa/małżonków na pokładzie, liczba rodziców/dzieci na pokładzie, cena biletu oraz miejsce zaokrętowania.

Zbiór zawiera także informację o tym, czy pasażer przeżył katastrofę.

Titanic przewoził ponad 2,200 osób, z czego ponad 1,500 zginęło, co czyni tę katastrofę jedną z najbardziej tragicznych w historii morskiej.

Kolumny:

  • pclass - Klasa biletu
  • survived - Czy pasażer przeżył katastrofę
  • name - Imię i nazwisko pasażera
  • sex - Płeć pasażera
  • age - Wiek pasażera
  • sibsp - Liczba rodzeństwa/małżonków na pokładzie
  • parch - Liczba rodziców/dzieci na pokładzie
  • ticket - Numer biletu
  • fare - Cena biletu
  • cabin - Numer kabiny
  • embarked - Port, w którym pasażer wszedł na pokład (C = Cherbourg, Q = Queenstown, S = Southampton)
  • boat - Numer łodzi ratunkowej
  • body - Numer ciała (jeśli pasażer nie przeżył i ciało zostało odnalezione)
  • home.dest - Miejsce docelowe

1. Ogólny przegląd danych¶

Wczytanie danych z pliku do analizy¶

In [1]:
import pandas as pd
In [2]:
df = pd.read_csv('26__titanic.csv', sep=",")
df
Out[2]:
pclass survived name sex age sibsp parch ticket fare cabin embarked boat body home.dest
0 1.0 1.0 Allen, Miss. Elisabeth Walton female 29.0000 0.0 0.0 24160 211.3375 B5 S 2 NaN St Louis, MO
1 1.0 1.0 Allison, Master. Hudson Trevor male 0.9167 1.0 2.0 113781 151.5500 C22 C26 S 11 NaN Montreal, PQ / Chesterville, ON
2 1.0 0.0 Allison, Miss. Helen Loraine female 2.0000 1.0 2.0 113781 151.5500 C22 C26 S NaN NaN Montreal, PQ / Chesterville, ON
3 1.0 0.0 Allison, Mr. Hudson Joshua Creighton male 30.0000 1.0 2.0 113781 151.5500 C22 C26 S NaN 135.0 Montreal, PQ / Chesterville, ON
4 1.0 0.0 Allison, Mrs. Hudson J C (Bessie Waldo Daniels) female 25.0000 1.0 2.0 113781 151.5500 C22 C26 S NaN NaN Montreal, PQ / Chesterville, ON
... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
1305 3.0 0.0 Zabour, Miss. Thamine female NaN 1.0 0.0 2665 14.4542 NaN C NaN NaN NaN
1306 3.0 0.0 Zakarian, Mr. Mapriededer male 26.5000 0.0 0.0 2656 7.2250 NaN C NaN 304.0 NaN
1307 3.0 0.0 Zakarian, Mr. Ortin male 27.0000 0.0 0.0 2670 7.2250 NaN C NaN NaN NaN
1308 3.0 0.0 Zimmerman, Mr. Leo male 29.0000 0.0 0.0 315082 7.8750 NaN S NaN NaN NaN
1309 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

1310 rows × 14 columns

Zmiana nazw kolumn na zapis w j. polskim¶

In [3]:
## pokaż nazwy wszystkich kolumn
df.columns
Out[3]:
Index(['pclass', 'survived', 'name', 'sex', 'age', 'sibsp', 'parch', 'ticket',
       'fare', 'cabin', 'embarked', 'boat', 'body', 'home.dest'],
      dtype='object')
In [4]:
## zmieniamy nazyw kolumn na zapis w jęzku polskim
df.columns = ['klasa', 'ocalały', 'imię i nazwisko', 'płeć', 'wiek', 'rodzoństwo/małżonek', 'rodzice/dzieci', 'numer biletu', 
              'cena biletu', 'numer kabiny', 'port', 'numer szalupy', 'numer zwłok', 'miejsce docelowe']

## zmieniamy nazwy płci 'female' i 'male' na j. polski, tak aby wszystko było spójne z polskimi nazwami kolumn
df['płeć'] = df['płeć'].replace({'female': 'kobieta', 'male': 'mężczyzna'})
df
Out[4]:
klasa ocalały imię i nazwisko płeć wiek rodzoństwo/małżonek rodzice/dzieci numer biletu cena biletu numer kabiny port numer szalupy numer zwłok miejsce docelowe
0 1.0 1.0 Allen, Miss. Elisabeth Walton kobieta 29.0000 0.0 0.0 24160 211.3375 B5 S 2 NaN St Louis, MO
1 1.0 1.0 Allison, Master. Hudson Trevor mężczyzna 0.9167 1.0 2.0 113781 151.5500 C22 C26 S 11 NaN Montreal, PQ / Chesterville, ON
2 1.0 0.0 Allison, Miss. Helen Loraine kobieta 2.0000 1.0 2.0 113781 151.5500 C22 C26 S NaN NaN Montreal, PQ / Chesterville, ON
3 1.0 0.0 Allison, Mr. Hudson Joshua Creighton mężczyzna 30.0000 1.0 2.0 113781 151.5500 C22 C26 S NaN 135.0 Montreal, PQ / Chesterville, ON
4 1.0 0.0 Allison, Mrs. Hudson J C (Bessie Waldo Daniels) kobieta 25.0000 1.0 2.0 113781 151.5500 C22 C26 S NaN NaN Montreal, PQ / Chesterville, ON
... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
1305 3.0 0.0 Zabour, Miss. Thamine kobieta NaN 1.0 0.0 2665 14.4542 NaN C NaN NaN NaN
1306 3.0 0.0 Zakarian, Mr. Mapriededer mężczyzna 26.5000 0.0 0.0 2656 7.2250 NaN C NaN 304.0 NaN
1307 3.0 0.0 Zakarian, Mr. Ortin mężczyzna 27.0000 0.0 0.0 2670 7.2250 NaN C NaN NaN NaN
1308 3.0 0.0 Zimmerman, Mr. Leo mężczyzna 29.0000 0.0 0.0 315082 7.8750 NaN S NaN NaN NaN
1309 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

1310 rows × 14 columns

Losowe 10 rekordów¶

In [5]:
df.sample(10)
Out[5]:
klasa ocalały imię i nazwisko płeć wiek rodzoństwo/małżonek rodzice/dzieci numer biletu cena biletu numer kabiny port numer szalupy numer zwłok miejsce docelowe
326 2.0 0.0 Andrew, Mr. Edgardo Samuel mężczyzna 18.0 0.0 0.0 231945 11.5000 NaN S NaN NaN Buenos Aires, Argentina / New Jersey, NJ
269 1.0 0.0 Smith, Mr. Richard William mężczyzna NaN 0.0 0.0 113056 26.0000 A19 S NaN NaN Streatham, Surrey
707 3.0 0.0 Carlsson, Mr. August Sigfrid mężczyzna 28.0 0.0 0.0 350042 7.7958 NaN S NaN NaN Dagsas, Sweden Fower, MN
583 2.0 1.0 Watt, Mrs. James (Elizabeth "Bessie" Inglis Mi... kobieta 40.0 0.0 0.0 C.A. 33595 15.7500 NaN S 9 NaN Aberdeen / Portland, OR
424 2.0 0.0 Givard, Mr. Hans Kristensen mężczyzna 30.0 0.0 0.0 250646 13.0000 NaN S NaN 305.0 NaN
944 3.0 0.0 Laleff, Mr. Kristo mężczyzna NaN 0.0 0.0 349217 7.8958 NaN S NaN NaN NaN
537 2.0 0.0 Ponesell, Mr. Martin mężczyzna 34.0 0.0 0.0 250647 13.0000 NaN S NaN NaN Denmark / New York, NY
396 2.0 1.0 Doling, Mrs. John T (Ada Julia Bone) kobieta 34.0 0.0 1.0 231919 23.0000 NaN S NaN NaN Southampton
156 1.0 0.0 Head, Mr. Christopher mężczyzna 42.0 0.0 0.0 113038 42.5000 B11 S NaN NaN London / Middlesex
1030 3.0 0.0 Moran, Mr. James mężczyzna NaN 0.0 0.0 330877 8.4583 NaN Q NaN NaN NaN

Podstawowe informacje o danych zawartych w tabeli¶

In [6]:
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1310 entries, 0 to 1309
Data columns (total 14 columns):
 #   Column               Non-Null Count  Dtype  
---  ------               --------------  -----  
 0   klasa                1309 non-null   float64
 1   ocalały              1309 non-null   float64
 2   imię i nazwisko      1309 non-null   object 
 3   płeć                 1309 non-null   object 
 4   wiek                 1046 non-null   float64
 5   rodzoństwo/małżonek  1309 non-null   float64
 6   rodzice/dzieci       1309 non-null   float64
 7   numer biletu         1309 non-null   object 
 8   cena biletu          1308 non-null   float64
 9   numer kabiny         295 non-null    object 
 10  port                 1307 non-null   object 
 11  numer szalupy        486 non-null    object 
 12  numer zwłok          121 non-null    float64
 13  miejsce docelowe     745 non-null    object 
dtypes: float64(7), object(7)
memory usage: 143.4+ KB

Wniosek: Tabela posiada 1310 indeksów. Kilka kolumn ma bardzo niepełne dane - do nich należą przede wszystkim kolumny: wiek, numer kabiny, numer szalupy, numer zwłok i miejsce docelowe. Niektóre z kolumn ma zastosowane niewłaściwe typy danych - klasa, wiek, rodzeństwo/małżone, rodzice/dzieci, numer szalupy, numer zwłok - te kolumny powinien być typ INT. Kolumna "ocalały" powinna być stringiem.

Pokazujemy liczbę wartości unikatowych w każdej z kolumn¶

In [7]:
df.nunique()
Out[7]:
klasa                     3
ocalały                   2
imię i nazwisko        1307
płeć                      2
wiek                     98
rodzoństwo/małżonek       7
rodzice/dzieci            8
numer biletu            929
cena biletu             281
numer kabiny            186
port                      3
numer szalupy            27
numer zwłok             121
miejsce docelowe        369
dtype: int64

Wniosek:¶

Kolumna 'imię i nazwisko' nie jest równa ilości wszystkich indeksów, więc prawdopodobnie są 4 rekordy (2 pary duplikatów) z identycznymi imionami i nazwiskami. Z pozostałych kolumn widać, że dane są w dużym stopniu niepełne. Dodatkowo dane zawierają 121 unikalnych wartości dla 'numeru zwłok', co pozwala przypuszczać, że tylko 121 ciał wydobyto z wody po katastrofie, pozostałe ciała spoczęły na dnie oceanu.

Podstawowe obliczenia numeryczne dla poszczególnych kolumn¶

In [8]:
df.describe().T
Out[8]:
count mean std min 25% 50% 75% max
klasa 1309.0 2.294882 0.837836 1.0000 2.0000 3.0000 3.000 3.0000
ocalały 1309.0 0.381971 0.486055 0.0000 0.0000 0.0000 1.000 1.0000
wiek 1046.0 29.881135 14.413500 0.1667 21.0000 28.0000 39.000 80.0000
rodzoństwo/małżonek 1309.0 0.498854 1.041658 0.0000 0.0000 0.0000 1.000 8.0000
rodzice/dzieci 1309.0 0.385027 0.865560 0.0000 0.0000 0.0000 0.000 9.0000
cena biletu 1308.0 33.295479 51.758668 0.0000 7.8958 14.4542 31.275 512.3292
numer zwłok 121.0 160.809917 97.696922 1.0000 72.0000 155.0000 256.000 328.0000

Wniosek: z powyższych danych numerycznych można wyczytać kilka ciekawych rzeczy:¶

  • średni wiek pasażerów to niespełna 30 lat, a najczęściej powtarzającym sie wiekiem jest wiek 28 lat, najmłodszym pasażerem było 2-miesięczne dziecko, a najstarszy pasażer miał 80 lat
  • większość pasażerów podróżowała sama, bez rodziny
  • średnia cena biletu wynosiła 33 jednostki (brak informacji w jakiej walucie podano cenę biletu prawdopodobnie jest to funt brytyjski lub dolar amerykański). Jednakże zróżnicowanie cen jest duże, w zależności od klasy, widać rozrzut danych o 0 do 512.

2. Analiza wartości brakujących¶

Sprawdzanie ile rekordów ma puste wartości w każdej z kolumn¶

In [9]:
df.isnull().sum()
Out[9]:
klasa                     1
ocalały                   1
imię i nazwisko           1
płeć                      1
wiek                    264
rodzoństwo/małżonek       1
rodzice/dzieci            1
numer biletu              1
cena biletu               2
numer kabiny           1015
port                      3
numer szalupy           824
numer zwłok            1189
miejsce docelowe        565
dtype: int64

Czy któryś z rekordów ma puste wszystkie kolumny?¶

In [10]:
df.isnull().all(axis=1).sum()
Out[10]:
1

Usuwanie rekordu, który ma puste wartości we wszystkich kolumnach¶

In [11]:
df = df.dropna(how='all')
## Parametr 'how' jest ustawiony na 'all', co oznacza, że zostaną usunięte tylko te wiersze, w których wszystkie wartości są brakujące.
In [12]:
## Upewniamy się, że teraz nie ma już rekordu który ma wszystkie kolumny puste
df.isnull().all(axis=1).sum()
Out[12]:
0

Zduplikowane rekordy¶

In [13]:
df[df.duplicated()]
Out[13]:
klasa ocalały imię i nazwisko płeć wiek rodzoństwo/małżonek rodzice/dzieci numer biletu cena biletu numer kabiny port numer szalupy numer zwłok miejsce docelowe
In [14]:
df[df['imię i nazwisko'].duplicated()]
Out[14]:
klasa ocalały imię i nazwisko płeć wiek rodzoństwo/małżonek rodzice/dzieci numer biletu cena biletu numer kabiny port numer szalupy numer zwłok miejsce docelowe
726 3.0 0.0 Connolly, Miss. Kate kobieta 30.0 0.0 0.0 330972 7.6292 NaN Q NaN NaN Ireland
925 3.0 0.0 Kelly, Mr. James mężczyzna 44.0 0.0 0.0 363592 8.0500 NaN S NaN NaN NaN
In [15]:
## Pokaż rekordy z zduplikowanymi wartościami w kolumnie 'imię i nazwisko', aby móc porównać pozostałe dane dla tych rekordów
duplicate_names = df[df.duplicated(subset='imię i nazwisko', keep=False)]
duplicate_names
Out[15]:
klasa ocalały imię i nazwisko płeć wiek rodzoństwo/małżonek rodzice/dzieci numer biletu cena biletu numer kabiny port numer szalupy numer zwłok miejsce docelowe
725 3.0 1.0 Connolly, Miss. Kate kobieta 22.0 0.0 0.0 370373 7.7500 NaN Q 13 NaN Ireland
726 3.0 0.0 Connolly, Miss. Kate kobieta 30.0 0.0 0.0 330972 7.6292 NaN Q NaN NaN Ireland
924 3.0 0.0 Kelly, Mr. James mężczyzna 34.5 0.0 0.0 330911 7.8292 NaN Q NaN 70.0 NaN
925 3.0 0.0 Kelly, Mr. James mężczyzna 44.0 0.0 0.0 363592 8.0500 NaN S NaN NaN NaN

Wniosek: Dane nie mają zduplikowanych rekordów. Sprawdzono również zduplikowane dane w kolumnie 'imię i nazwisko' i wykryto 2 pary rekordów z takimi samymi imionami i nazwiskami, jednakże jest to prawdopodobne zdarzenie, pozostałe dane dla tych rekordów różnią się od siebie.

Zmiana typu danych na string dla kolumny 'Ocalały' i przypisanie dla ocalałych wartości "Tak" a dla zabitych wartości "Nie"¶

In [16]:
## Zmiana typu na string
df['ocalały'] = df['ocalały'].replace({1.0: 'Tak', 0.0: 'Nie'}).astype(str)
df['ocalały'].info()
<class 'pandas.core.series.Series'>
Int64Index: 1309 entries, 0 to 1308
Series name: ocalały
Non-Null Count  Dtype 
--------------  ----- 
1309 non-null   object
dtypes: object(1)
memory usage: 20.5+ KB
C:\Users\Łukasz\AppData\Local\Temp\ipykernel_2352\3388569788.py:2: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  df['ocalały'] = df['ocalały'].replace({1.0: 'Tak', 0.0: 'Nie'}).astype(str)

3. Transformacja danych¶

Uzupełnianie pustych wartości w kolumnie 'wiek' medianą wyliczoną osobno dla mężczyzn i kobiet oraz dodatkowo z podziałem na klasy.¶

In [17]:
## sprawdzanie ile jest pustych wartości w kolumnie 'wiek'
df['wiek'].isna().sum()
Out[17]:
263
In [18]:
## obliczamy medianę wieku dla mężczyzn i kobiet dodatkowo grupując to jeszcze na klasy
mezczyzni = df[df['płeć'] == 'mężczyzna']
mezczyzni_mediana_wiek = mezczyzni.groupby('klasa')['wiek'].median()

kobiety = df[df['płeć'] == 'kobieta']
kobiety_mediana_wiek = kobiety.groupby('klasa')['wiek'].median()

## wyświetlamy wynik obliczeń w tabeli, nie jest to krok konieczny, chodzi tylko o przedstawienie wizualne jak prezentuje sie wiek w poszczególnych klasach w zależności od płci
df_mediana_wieku = pd.DataFrame({
    'mediana wieku mężczyźni': mezczyzni_mediana_wiek,
    'mediana wieku kobiety': kobiety_mediana_wiek
})
df_mediana_wieku

    
Out[18]:
mediana wieku mężczyźni mediana wieku kobiety
klasa
1.0 42.0 36.0
2.0 29.5 28.0
3.0 25.0 22.0

Uzupełnie pustych pól w kolumnie wiek medianą wieku obliczoną osobno dla kobiet i mężczyzn i grupująć ich dodatkowo według klasy¶

In [19]:
## funkcja do uzupełniania pustych pól w kolumnie wiek
def uzupelnij_wiek(rzad):
    if pd.isna(rzad['wiek']) and rzad['płeć'] == 'mężczyzna':
        return mezczyzni_mediana_wiek.get(rzad['klasa'], rzad['wiek'])
    elif pd.isna(rzad['wiek']) and rzad['płeć'] == 'kobieta':
        return kobiety_mediana_wiek.get(rzad['klasa'], rzad['wiek'])
    return rzad['wiek']

df['wiek'] = df.apply(uzupelnij_wiek, axis=1)
C:\Users\Łukasz\AppData\Local\Temp\ipykernel_2352\4072451876.py:9: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  df['wiek'] = df.apply(uzupelnij_wiek, axis=1)
In [20]:
## sprawdzenie czy powyższa funkcja 'uzupelnij wiek' działą prawidłowo - wyświetlamy indeks 15 tabeli (wiersz 16), w którym był mężczyzna z 1 klasy a który nie miał przypisanego wieku
df.iloc[15]
Out[20]:
klasa                                  1.0
ocalały                                Nie
imię i nazwisko        Baumann, Mr. John D
płeć                             mężczyzna
wiek                                  42.0
rodzoństwo/małżonek                    0.0
rodzice/dzieci                         0.0
numer biletu                      PC 17318
cena biletu                         25.925
numer kabiny                           NaN
port                                     S
numer szalupy                          NaN
numer zwłok                            NaN
miejsce docelowe              New York, NY
Name: 15, dtype: object

4. Analiza pojedyńczych zmiennych¶

Podział pasażerów na dzieci, kobiety i mężczyzn

In [44]:
import matplotlib.pyplot as plt

# Calculate the number of children, women, and men
ilosc_dzieci = df[df['wiek'] < 18].shape[0]
ilosc_kobiet = df[(df['płeć'] == 'kobieta') & (df['wiek'] >= 18)].shape[0]
ilosc_mezczyzn = df[(df['płeć'] == 'mężczyzna') & (df['wiek'] >= 18)].shape[0]

# Total number of passengers
suma_pasazerow = df.shape[0]

# Calculate percentages
dzieci_procent = (ilosc_dzieci / suma_pasazerow) * 100
kobiety_procent = (ilosc_kobiet / suma_pasazerow) * 100
mezczyzni_procent = (ilosc_mezczyzn / suma_pasazerow) * 100

# Data for the pie chart
labels = ['Dzieci', 'Kobiety', 'Mężczyźni']
sizes = [dzieci_procent, kobiety_procent, mezczyzni_procent]
colors = ['#ff9999','#66b3ff','#99ff99']
explode = (0.1, 0.1, 0.1)  # explode all slices

# Create the pie chart
plt.figure(figsize=(8, 8))
plt.pie(sizes, explode=explode, labels=labels, colors=colors, autopct='%1.1f%%', startangle=140, wedgeprops={'edgecolor': 'black'})
plt.title('Rozkład procentowy pasażerów - dzieci, kobiety, mężczyźni')

# Equal aspect ratio ensures that pie is drawn as a circle.
plt.axis('equal')
Out[44]:
(-1.2034621067247384,
 1.1903676209341627,
 -1.1904827458015403,
 1.1649285137017713)
No description has been provided for this image

Ile osób samotnych (bez rodziny i małżonka)¶

In [58]:
# Filter passengers who have neither siblings/spouses nor parents/children on board
osoby_samotne = df[(df['rodzoństwo/małżonek'] == 0) & (df['rodzice/dzieci'] == 0)]

# Count the number of such passengers
suma_samotnych = osoby_samotne.shape[0]
suma_samotnych
Out[58]:
790
In [65]:
osoby_samotne_klasa = osoby_samotne.groupby(['klasa'])
osoby_samotne_klasa
Out[65]:
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x000002521E6273D0>
In [21]:
suma_ocalałych = (df['ocalały'] == 'Tak').sum()
suma_ocalałych
Out[21]:
500
In [22]:
suma_zabitych = (df['ocalały'] == 'Nie').sum()

suma_zabitych
Out[22]:
809
In [23]:
import matplotlib.pyplot as plt
# Create a bar chart
plt.figure(figsize=(8, 6))
plt.bar(['Ocalali', 'Zabici'], [suma_ocalałych, suma_zabitych], color=['green', 'red'])
plt.title('Liczba ocalałaych i zabitych')
plt.xlabel('Status')
plt.ylabel('Liczba osób')
Out[23]:
Text(0, 0.5, 'Liczba osób')
No description has been provided for this image
In [24]:
df['wiek'].hist(bins=30, edgecolor='black')
plt.title('Histogram wieku pasażerów')
plt.xlabel('Wiek')
plt.ylabel('Liczba pasażerów')
Out[24]:
Text(0, 0.5, 'Liczba pasażerów')
No description has been provided for this image

Wniosek: na statku przeważali pasażerowie młodzi, najczęściej między 20 a 30 rokiem życia. Mediana wieku wynosiła 28 lat. Najmniejszą liczbę stanowiły osoby w wieku powyżej 65 lat oraz dzieci między 8-12 rokiem życia.

In [25]:
## sprawdzamy ile razy występuje puste pole w cenie biletu lub wartość 0 - tych wartości nie chcemy uwzględniać w histogramie
zerowa_cena_biletu = df['cena biletu'].value_counts().get(0, 0)
zerowa_cena_biletu
Out[25]:
17
In [26]:
filtr_cena_biletu = df['cena biletu'].dropna()
filtr_cena_biletu = filtr_cena_biletu[(filtr_cena_biletu > 0) & (filtr_cena_biletu < 300)]
plt.hist(filtr_cena_biletu, bins=50, edgecolor='black')
plt.title('Histogram ceny biletu')
plt.xlabel('Cena')
plt.ylabel('Liczba wystąpień')
Out[26]:
Text(0, 0.5, 'Liczba wystąpień')
No description has been provided for this image
In [27]:
## Tworzymy tabelę df2, jako kopię df, w której zapiszemy tylko kolumny 'klasa' i 'ocalały', po to aby w nastepnym kroku zmienić nazwy klas na string, bo na wykresie lepiej wygląda zapis "klasa 1" niż "1.0"
df2 = df[['klasa', 'ocalały']].copy()
df2['klasa'] = df2['klasa'].map({1.0: 'klasa 1', 2.0: 'klasa 2', 3.0: 'klasa 3'})

ocalały_klasa = df2.groupby(['klasa', 'ocalały']).size().unstack()
ocalały_procentowo = ocalały_klasa.div(ocalały_klasa.sum(axis=1), axis=0) * 100

fig, ax = plt.subplots(figsize=(8, 6))
ocalały_klasa.plot(kind='bar', stacked=True, ax=ax, color=['red', 'green'])

for i, (klasa, rzad) in enumerate(ocalały_procentowo.iterrows()):
    for j, (ocalały, procenty) in enumerate(rzad.items()):
        ax.text(i, ocalały_klasa.iloc[i, j] / 2 + sum(ocalały_klasa.iloc[i, :j]), f'{procenty:.1f}%', ha='center', va='center', color='white')

ax.set_title('Ocaleni z podziałem na klasy')
ax.set_xlabel('Klasa')
ax.set_ylabel('Liczba osób')
ax.legend(['Zabici', 'Ocaleni'], title='Legenda')
Out[27]:
<matplotlib.legend.Legend at 0x2520e782b90>
No description has been provided for this image
In [28]:
ocalały_klasa
Out[28]:
ocalały Nie Tak
klasa
klasa 1 123 200
klasa 2 158 119
klasa 3 528 181
In [29]:
ocaleni_płeć = df.groupby(['płeć', 'ocalały'])
ocaleni_płeć
Out[29]:
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x000002520E9B1B10>
In [30]:
ocalały_płeć = df.groupby(['płeć', 'ocalały']).size().unstack()
ocalały_procentowo = ocalały_płeć.div(ocalały_płeć.sum(axis=1), axis=0) * 100

ax = ocalały_procentowo.plot(kind='bar', stacked=True, figsize=(8, 6), color=['red', 'green'])
ax.set_title('Ocaleni z podziałem na płeć')
ax.set_xlabel('Płeć')
ax.set_ylabel('Ile procent?')
ax.legend(['Zabici', 'Ocaleni'], title='Legenda')

for p in ax.patches:
    width, height = p.get_width(), p.get_height()
    x, y = p.get_xy() 
    ax.annotate(f'{height:.1f}%', (x + width/2, y + height/2), ha='center', va='center')
No description has been provided for this image
In [31]:
ocalały_płeć
Out[31]:
ocalały Nie Tak
płeć
kobieta 127 339
mężczyzna 682 161
In [32]:
plt.hist(df[df['ocalały'] == 'Nie']['wiek'], bins=30, alpha=0.5, label='Zabici', color='r')

# Histogram for not survived passengers
## plt.hist(df[df['ocalały'] == 'Tak']['wiek'], bins=10, alpha=0.5, label='Not Survived', color='r')

# Add labels and title
plt.xlabel('Wiek')
plt.ylabel('Liczba osób')
plt.title('Histogram dla osób zabitych z podziałem na wiek')
plt.legend()
Out[32]:
<matplotlib.legend.Legend at 0x2520c34e310>
No description has been provided for this image
In [33]:
# Histogram for not survived passengers
plt.hist(df[df['ocalały'] == 'Tak']['wiek'], bins=30, alpha=0.5, label='Ocaleni', color='g')

# Add labels and title
plt.xlabel('Wiek')
plt.ylabel('Liczba osób')
plt.title('Histogram dla osób ocalonych z podziałem na wiek')
plt.legend()
Out[33]:
<matplotlib.legend.Legend at 0x2520e7cc350>
No description has been provided for this image

Wniosek: histogramy wieku zabitych i ocalałych są zbliżone do ogólnego histogramu wieku wszystkich pasażerów.

Porównanie skali zabitych z podziałem na dzieci i dorosłych¶

In [34]:
# Create two subsets based on age
dzieci = df[df['wiek'] < 18]
dorośli = df[df['wiek'] >= 18]

# Calculate the counts for each group
dzieci_ocalałe = dzieci['ocalały'].value_counts()
dorośli_ocaleni = dorośli['ocalały'].value_counts()

# Plot the pie chart for under 18
plt.figure(figsize=(10, 5))

plt.subplot(1, 2, 1)
plt.pie(dzieci_ocalałe, labels=['Zabici', 'Ocalali'], autopct='%1.1f%%', startangle=90)
plt.title('Dzieci - wiek poniżej 18 lat')

# Plot the pie chart for 18 and over
plt.subplot(1, 2, 2)
plt.pie(dorośli_ocaleni, labels=['Zabici', 'Ocalali'], autopct='%1.1f%%', startangle=90)
plt.title('Dorośli - wiek 18 lat i więcej')
Out[34]:
Text(0.5, 1.0, 'Dorośli - wiek 18 lat i więcej')
No description has been provided for this image

Wsniosek: Odsetek ocalałych wśród dzieci jest nieco większy niż wśród dorosłych (o około 11 pkt %), ale wciąż więcej było zabitych (52,6%). Jest to dość zaskakująca dana, ponieważ nie znając wyniku tej analizy możnaby sądzić że dzieci były ratowane w dużo większym odsetku.

In [60]:
ilosc_dzieci
Out[60]:
154
In [46]:
# Filter children below 18 years of age
dzieci_df = df[df['wiek'] < 18]

# Group by class and count the number of children in each class
dzieci_klasy = dzieci_df['klasa'].value_counts().sort_index()

# Plot a pie chart
plt.figure(figsize=(8, 8))
plt.pie(dzieci_klasy, labels=dzieci_klasy.index, autopct='%1.1f%%', startangle=90, wedgeprops={'edgecolor': 'black'})
plt.title('Do jakich klas należały dzieci?')
plt.axis('equal')  # Equal aspect ratio ensures that pie is drawn as a circle.
Out[46]:
(-1.09998871840244,
 1.0999993357476645,
 -1.0999992641338077,
 1.0999999649587529)
No description has been provided for this image
In [61]:
# Import the required dependencies
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np

# Filter children (age < 18)
dzieci_df = df[df['wiek'] < 18]

# Group by class and survival status
grouped = dzieci_df.groupby(['klasa', 'ocalały']).size().unstack(fill_value=0)


# Calculate percentages of survivors
grouped['total'] = grouped['Nie'] + grouped['Tak']
grouped['survived_percent'] = (grouped['Tak'] / grouped['total']) * 100

# Plotting
fig, ax = plt.subplots(figsize=(10, 6))

# Bar plot for survived and not survived
bar_width = 0.35
index = np.arange(len(grouped))

bars1 = ax.bar(index, grouped['Nie'], bar_width, label='Zabity', color='red')
bars2 = ax.bar(index + bar_width, grouped['Tak'], bar_width, label='Ocalały', color='green')

# Add percentage labels on green bars
for i, rect in enumerate(bars2):
    height = rect.get_height()
    ax.text(rect.get_x() + rect.get_width() / 2.0, height, f'{grouped["survived_percent"].iloc[i]:.1f}%', ha='center', va='bottom', color='gray')

# Labels and title
ax.set_xlabel('Klasa')
ax.set_ylabel('Liczba dzieci')
ax.set_title('Ocalałe dzieci z podziałem na klasy')
ax.set_xticks(index + bar_width / 2)
ax.set_xticklabels(['1st Class', '2nd Class', '3rd Class'])
ax.legend()

plt.tight_layout()
No description has been provided for this image
In [66]:
plt.figure(figsize=(8, 6))
df['port'].dropna().value_counts().plot(kind='bar', color='skyblue')
plt.title('W którym porcie wsiadło na pokład najwięcej osób')
plt.xlabel('Port (C = Cherbourg, Q = Queenstown, S = Southampton)')
plt.ylabel('Liczba pasażerów')
plt.xticks(rotation=0)
plt.tight_layout()
No description has been provided for this image

Wniosek: Najwięcej osób weszło na statek w porcie w Southampton (blisko 900), około 300 osób w Cherbourg, a około 100 osób w Queenstown.

In [36]:
# Group by 'home.dest' and count the number of occurrences
destination_counts = df['miejsce docelowe'].value_counts().head(10)

# Plotting
plt.figure(figsize=(10, 6))
destination_counts.plot(kind='bar', color='skyblue')
plt.title('Top 10 destynacji')
plt.xlabel('Destynacja')
plt.ylabel('Liczba osób')
plt.xticks(rotation=45, ha='right')
plt.tight_layout()
No description has been provided for this image
In [37]:
import matplotlib.pyplot as plt
import seaborn as sns

# Calculate the correlation matrix
correlation_matrix = df.corr()

# Plot the correlation matrix
plt.figure(figsize=(10, 8))
sns.heatmap(correlation_matrix, annot=True, fmt=".2f", cmap='coolwarm', cbar=True)
plt.title('Macierz korelacji')
plt.tight_layout()
C:\Users\Łukasz\AppData\Local\Temp\ipykernel_2352\2928059081.py:5: FutureWarning: The default value of numeric_only in DataFrame.corr is deprecated. In a future version, it will default to False. Select only valid columns or specify the value of numeric_only to silence this warning.
  correlation_matrix = df.corr()
No description has been provided for this image

Wniosek: w analizowanych danych nie ma jakiś bardzo dużych zależności między danymi. Widać tylko pewną zależność ceny biletu od klasy - im wyższa klasa tym droższy bilet, oraz wieku od klasy - im wyższa klasa tym starsi pasażerowie

5. Wartości odstające¶

In [38]:
plt.figure(figsize=(12, 6))

# Boxplot for 'age'
plt.subplot(1, 2, 1)
df.boxplot(column='wiek')
plt.title('Boxplot dla wieku')

# Boxplot for 'fare'
plt.subplot(1, 2, 2)
df.boxplot(column='cena biletu')
plt.title('Boxplot dla ceny biletu')
Out[38]:
Text(0.5, 1.0, 'Boxplot dla ceny biletu')
No description has been provided for this image

6. Podsumowanie i wnioski¶

Przegląd danych oraz dokonane zmiany i transformacje danych.¶

  1. Tabela zawiera 1310 indeksów, każdy rekord odpowiada 1 osobie płynącej statkiem Titanic.
  2. Według ogólnodostępnych informacji statkiem Titanic płynęło około 2200 osób, łącznie z załogą, a więc dane które zawiera analizowana tabela nie obejmują wszysztich osób na pokładzie, jest to około 60% wszystkich osób płynących statkiem. To też może powodować przekłamanie i nieco fałszować realne statystyki z katastrofy Titanica. Jednakże analiza została przeprowadzona szczegółowo na danych które posiadamy.
  3. Zmieniono nazwy kolumn na zapis w j. polskim.
  4. Kilka kolumn ma znacząco niepełne dane - do nich należą przede wszystkim kolumny: wiek, numer kabiny, numer szalupy, numer zwłok i miejsce docelowe. Tylko 1 rekord był całkowicie pusty i nie posiadał wartości w żadnej z kolumn - rekord ten został usunięty.
  5. Wśród niepełnych danych ciekawym przypadkiem może być tylko 121 rekordów, które posiadały wpisany numer zwłok. Ogółem liczba zabitych z analizowanej tabeli wynosi 809 osób. Tylko `121 rekordów z przypisanym numerem zwłok pozwala przypuszczać, że tylko 121 ciał wydobyto z wody po katastrofie.
  6. Dane zawierały tylko 2 pary duplikatów w kolumnie dotyczącej imienia i nazwiska pasażera. Nie usuwano tych duplikatów bo powtórzenie się popularnego imienia i nazwiska dla tak dużej liczby osób jest zdarzeniem prawdopodobnym, a dodatkowo dane w innych kolumnach różniły się od siebie.
  7. Zmieniono typ danych dla kolumny "ocalały" z float na string. Kolumna 'ocalały' mówi o tym czy osoba przeżyła czy zginęła, domyślnie w tabeli było to zapisane w sposób zero-jedynkowy, gdzie 0 oznaczało że osoba zginęła a 1, że osoba przeżyła. Dla lepszej przejrzystości i łatwiejszej analizy lepiej jest tą wartość odczytywać słownie a nie w sposób binarny.
  8. Puste wartości w kolumnie wiek uzupełniono medianą wyliczoną osobno dla mężczyzn i kobiet oraz dodatkowo z podziałem na klasy.

Wnioski do analizy zmiennych i wartości odstających¶

  1. Na statku przeważali dorośli mężczyźni - 58% pasażerów, kobiet było 30%, a dzieci 12%.
  2. Większość pasażerów była samotna, czyli płynęła bez rodziny czy małżonka (790 osób co stanowi 60% wszystkich).
  3. Na statku przeważali pasażerowie młodzi, najczęściej między 20 a 30 rokiem życia. Mediana wieku wynosiła 28 lat, a średnia niespełna 30 lat. Najmniejszą liczbę stanowiły osoby w wieku powyżej 65 lat oraz dzieci między 8-12 rokiem życia. Najmłodszym pasażerem było 2-miesięczne dziecko, a najstarszy pasażer miał 80 lat.
  4. Średnia cena biletu wynosiła 33 jednostki (brak informacji w jakiej walucie podano cenę biletu prawdopodobnie jest to funt brytyjski lub dolar amerykański). Jednakże zróżnicowanie cen jest duże, w zależności od klasy, widać rozrzut danych od 0 do 512.
  5. Ogólna liczba zabitych to 809 spośród 1310 osób, co stanowi około 62%.
    • stosunek zabitych do ocalałych rośnie wraz ze stopniem klasy do której należeli pasażerowie - im gorsza klasa tym większy procent zabitych - np. w klasie 1 "tylko" 38% osób zginęło, podczas gdy w klasie 2 było to już 57%, a w klasie 3 aż 75%
    • zdecydowanie większy odsetek ofiar stanowili mężczyźni - na 809 ofiar 127 to były kobiety a 682 mężczyźni, co daje odpowiednio 15,7% ofiar o płci żeńskiej i 84,3% płci męskiej. Jednakże wiemy, że na statku zdecydowanie przeważali mężczyźni, dlatego sprawdzono wskaźnik ofiar również osobno dla kobiet i mężczyzn - kobiet zginęło 27% a mężczyzn aż 81%. To oznacza, że tylko co piąty mężczyzna przeżył.
    • 53% dzieci nie przeżyło, 47% ocalało. To dość zaskakujące, ponieważ nie znając wyniku tej analizy można by sądzić, że dzieci były ratowane w dużo większym odsetku - a jednak nie. Przeżywalność katastrofy wśród dzieci jest zbliżona do dorosłych, różni się tylko o ok. 11 pkt.% na korzyść dzieci. Jednakże tak duży odsetek ofiar wśród dzieci może tłumaczyć analiza następnych danych, która wykazuje że zdecydowana większość dzieci należała do klasy 3, w której jak już wcześniej pokazano, odsetek ofiar był dużo wyższy niż w klasie 1 i 2
    • na statku było 154 dzieci, prawie 70% należało do 3 klasy, 20% do 2 klasy, a tylko 10% do klasy 1
    • różnica w ofiarach wśród dzieci z podziałem na klasy jest porażająca - w klasie 3 tylko 37% dzieci przeżyło, podczas gdy w klasie 1 i 2 było to odpowiednio 87% i 88%. W liczbach bezwzględnych różnicę widać jeszcze bardziej: łącznie w klasie 1 i 2 zginęło tylko 6 dzieci, natomiast w klasie 3 aż 67 dzieci
  6. Najwięcej osób weszło na statek w porcie w Southampton (blisko 900), około 300 osób w Cherbourg, a około 100 osób w Queenstown.
  7. W analizowanych danych nie ma jakiś bardzo dużych zależności między danymi. Widać tylko pewną zależność ceny biletu od klasy - im wyższa klasa tym droższy bilet, oraz wieku od klasy - im wyższa klasa tym starsi pasażerowie
  8. Połowa pasażerów była między 22 a 36 rokiem życia. Wartościami odstającymi dla wieku były wartości poniżej 1 roku życia i powyżej 60 roku życia. Natomiast dla cen biletu wartościami odstającymi były wartości powyżej 70, a większość cen biletów mieściłą się w przedziale 10 do 30.
In [ ]: