top of page

Der Untergang der Titanic

titanic disaster SQL.png

Natürlich kann man Daten mit Phyton und R analysieren, aber wie wäre es einmal mit dem guten alten SQL.

​

Hier zeige ich meinen Prozess einer reinen SQL Datenanalyse mit den Daten der Titanic Katastrophe.

​

Es ist ein bischen länger zu lesen, aber spannend bis zum Schluss

Einleitung

Auch in 2022 ist SQL das meist verbreitete Datenbankmanagement System:  Most popular database management systems 2022 | Statista. Der Titanic-Datensatz ist eine relativ einfache csv-Datei, die die Daten von 1306 Passagieren und ihr Schicksal in der Nacht zum 15. April 1912 enthält.

Vorbereitung

Ich verwende die SQL Server 2019 Installation und SQL Server Management Studio 18. Die Daten sind in einer CSV-Datei, hier: https://data.world/nrippner/titanic-disaster-dataset. 

Schritt 1: die Datenbank und Tabelle erstellen

Ich habe die Datenbank und die Tabelle dbo.passengers erstellt, um die Daten zu laden habe ich die BULK INSERT-Anweisung verwendet.

use titanic;

 

drop table if exists dbo.passengers;

 

create table passengers

(

passengerID int primary key,

pclass  smallint not null,

survived smallint not null,

last_name varchar(50),

first_name varchar(100),

sex varchar(10),

age decimal(12,2),

sibsp smallint,

parch smallint,

ticket  varchar(50),

fare decimal(12,2),

cabin varchar(30),

embarked varchar(30),

boat varchar(30),

body int,

homedest varchar(50)

)

 

 

BULK INSERT dbo.passengers

FROM 'D:\COMPUTER\data\nrippner-titanic-disaster-dataset\nrippner-titanic-disaster-dataset\original\titanic_disaster_original2.csv'

WITH

(

        FORMAT='CSV',

        FIRSTROW=2

);

Titanic desaster data.docx.png
Schritt 2: die Daten auf Null Werte prüfen

Fehlende Werte bzw. Null Werte haben einen erheblichen Einfluss auf die Datenqualität und somit auf die Ergebnisse der Datenanalyse. Für jede Datenanalyse-Aufgabe gilt nach wie vor der Satz „Shit in – shit out“. Zunächst prüfe ich also, wie viele leere Zeilen jede Spalte hat und gebe deren Prozentsatz zurück.

with CTE1 as

(

select

cast(100.00 * sum(case when pclass IS NULL then 1 else 0 end)/ max(all_passengers) as decimal(6,1)) as pclass,

cast(100.00 * sum(case when survived IS NULL then 1 else 0 end) / max(all_passengers) as decimal(6,1)) as survived,

cast(100.00 * sum(case when last_name IS NULL then 1 else 0 end) / max(all_passengers) as decimal(6,1)) as lastname,

cast(100.00 * sum(case when first_name IS NULL then 1 else 0 end) / max(all_passengers) as decimal(6,1)) as firstname,

cast(100.00 * sum(case when sex IS NULL then 1 else 0 end) / max(all_passengers) as decimal(6,1)) as sex,

cast(100.00 * sum(case when age IS NULL then 1 else 0 end) / max(all_passengers) as decimal(6,1)) as age,

cast(100.00 * sum(case when sibsp IS NULL then 1 else 0 end) / max(all_passengers) as decimal(6,1)) as sib,

cast(100.00 * sum(case when parch IS NULL then 1 else 0 end) / max(all_passengers) as decimal(6,1)) as parch,

cast(100.00 * sum(case when ticket IS NULL then 1 else 0 end) / max(all_passengers) as decimal(6,1)) as ticket,

cast(100.00 * sum(case when fare IS NULL then 1 else 0 end) / max(all_passengers) as decimal(6,1)) as fare,

cast(100.00 * sum(case when cabin IS NULL then 1 else 0 end) / max(all_passengers) as decimal(6,1)) as cabin,

cast(100.00 * sum(case when embarked IS NULL then 1 else 0 end) / max(all_passengers) as decimal(6,1)) as embarked,

cast(100.00 * sum(case when boat IS NULL then 1 else 0 end) / max(all_passengers) as decimal(6,1)) as boat,

cast(100.00 * sum(case when body IS NULL then 1 else 0 end) / max(all_passengers) as decimal(6,1)) as body,

cast(100.00 * sum(case when homedest IS NULL then 1 else 0 end) / max(all_passengers) as decimal(6,1)) as home

from

(

select count(*) over() as all_passengers, *

from passengers

) sub1

)

select 'pclass' as attribute, pclass as null_percent from CTE1

union all

select 'survived', survived from CTE1

union all

select 'lastname', lastname from CTE1

union all

select 'firstname', firstname from CTE1

union all

select 'sex', sex from CTE1

union all

select 'age', age from CTE1

union all

select 'sibsp', sib from CTE1

union all

select 'parch', parch from CTE1

union all

select 'ticket', ticket from CTE1

union all

select 'fare', survived from CTE1

union all

select 'cabin', cabin from CTE1

union all

select 'embarked', embarked from CTE1

union all

select 'boat', boat from CTE1

union all

select 'body', body from CTE1

union all

select 'homedest', home from CTE1

;

null values.jpg

Das Query zeigt, dass die Spalte [age] 20 % Nullen enthält, [cabin] 77,5 %, [boat] 62,9 %, [body] 90,8 % und [homedest] (homedestination) 43,1 %. Da ich [boat], [body] und [homedest] nicht für meine Analyse verwenden werde, bleiben nur die beiden kritischen Spalten [age] und [fare]. Mit diesen befassen wir uns später.

Datenbereich

Jetzt habe ich mir [age] und [fare] genauer angesehen, indem ich den Durchschnitt und die Spannweite der Werte berechnet habe. Beide Spalten sind rechtsschief, was bedeutet, dass die Maximalwerte weiter vom Durchschnitt entfernt sind als die Minimalwerte, d.h. es gibt Ausreißer im oberen Bereich der Werte, was sich auf den Durchschnitts auswirkt.

select 'age' as attribute, count (distinct coalesce(age,0)) as dist_ct, min(age) as min_val, avg(age) as avg_val, max(age) as max_val from passengers
union all
select '
fare', count (distinct fare), min(fare), avg(fare), max(fare) from passengers;

range of values.jpg
Ersetzen oder ignorieren?

Es gibt zwei Möglichkeiten mit fehlenden (Null-)Daten umzugehen. Entweder Sie mit etwas Sinnvollerem zu ersetzen oder zu ignorieren, z.B. mit der WHERE [age] IS NOT NULL-Anweisung. Ich habe mich entschieden, die Auswirkungen zu überprüfen, wenn ich die fehlenden Werte durch das Durchschnittsalter ersetzte, das auf dem Wert Titel basiert, der Teil der Spalte [first?name] ist. Dies erscheint ein guter Weg zu sein, um eine ausreichend detaillierte Zahl zum Ersetzten der fehlenden Werte zu erhalten. Dazu muss jedoch der Titel aus der Spalte [first_name] extrahiert werden.

​

Damit habe ich einen view (age_replaced) erstellt, um die zugehörigen Daten zu sammeln:

-- drop view age_replaced;

-- average age by title


-- create view age_replaced as


with CTE1 as
(
select 

passengerID,
trim(substring(
first_name,1,charindex('.',first_name,1))) as title,
first_name,
last_name,
age

from passengers p
)
,
CTE2 as
(
select 

title,
avg(
age) as avg_age
from
(
SELECT 
      [
first_name],
     trim(substring(
first_name,1,charindex('.',first_name,1))) as title,
      [
sex],
     [
age]
FROM [titanic].[dbo].[passengers]
) sub1
group by
title

select 
t1.
passengerID,
t1.
title,
t1.
first_name,
t1.
last_name,
t1.
age,
cast(

-- here the values are replaced
case 
   when t1.
age <1 then 1
   when t1.
age is null then t2.avg_age 
   else t1.
age 
   end
as decimal(6,2) 
) as replacedage,
t2.avg_age
from CTE1 t1
inner join CTE2 t2 on t1.title = t2.title;

 

age_replacement.jpg

select 
p.
sex,
AVG(p.
age) as avg_original,
avg(
replacedage) as avg_replaced,
AVG(p.
age) - avg(replacedage) as avg_difference
from passengers p
join [dbo].[age_replaced] r on r.
passengerid = p.passengerID
group by p.sex;

age_variance.jpg

Offensichtlich hat das Ersetzen der fehlenden Werte durch das Durchschnittsalter gemäß dem Titel der Passagiere keine signifikanten Auswirkung, daher habe ich mich entschieden, mit der bestehenden Altersgruppierung fortzufahren und die fehlenden Werte zu ignorieren (263 Nullzeilen von 1306).

Mit dem Ergebnis und der Datenqualität bin ich bisher zufrieden, jetzt tauchen wir in die Analyse ein! 

Jetzt kann ich das Query zum Vergleich der Daten auf dem view laufen lassen.

Analyse der Überlebensrate
Die Überlebensrate gesamt

Der allgemeine Ansatz zur Berechnung der Überlebensrate basiert auf einem Subquery mit WINDOW-Funktionen, um den Divisor gruppiert nach der gewünschten Dimension gruppiert zu erhalten. Dieses Schema wiederholt sich für die folgenden Analysen in ähnlicher Weise.

/* overall survival */

select 
t1.
survived,
count(*) as num_of_pass,
max(t1.
total_passengers) as total_pass,
format(round(1.00 * count(*)  / max(t1.
total_passengers),4),'P') as survival_rate,

replicate('|',floor(100 * count(*)  / max(t1.total_passengers))) as visual
from 

-- subquery for the overall number of passengers
(  
    select     
       count(*) over () as
total_passengers, passengers.*
   from passengers
) t1
group by t1.survived;        
-- group by the code survived (1 or 0)

overall survival.jpg

Tragischerweise überlebten fast zwei Drittel der Passagiere nicht, nur 500 von 1306 wurden gerettet. Lassen Sie uns nun etwas tiefer graben, um zu verstehen, wie die Überlebenden nach Klasse, Geschlecht, Alter usw. verteilt sind.

Überlebende nach Geschlecht und Alter

Wie wir alle wissen, hatten Frauen eine deutlich höhere Überlebenschance als Männer. Die Analyse bestätigt, dass weibliche Passagiere in allen Klassen eine Überlebensrate von 72,7 % hatten, während 4 von 5 Männern starben. Mit GROUP BY ROLLUP habe ich Zwischensummen nach Geschlecht berechnet.

/* survial rate by sex and class */

with CTE1 as 
(

select 
t1.
pclass as pclass,
case 
when t1.
pclass = 1 then 'First' 
when t1.
pclass = 2 then 'Second'
when t1.
pclass = 3 then 'Third'
else 'False'
end as class,
t1.
sex as sex,
-- count(*) as num_of_pass_by_pclass,
max(total_passengers) as all_passengers,
sum(
survived) as survived
from 
(
   select 
       
passengerid as pass_id, 
       
pclass as pclass,
       
sex as sex,
       
survived as survived,
       count(*) over (partition by pclass, sex) as total_passengers
   from passengers
) t1
where
survived = 1
group by
sex, pclass
)

select 
sex,
class,
sum(all_passengers) as all_passengers,
sum(
survived) as survived,
cast((100.00 * sum(
survived)) / sum(all_passengers) as decimal(5,1))  as 'survived %',
100-cast((100.00 * sum(
survived)) / sum(all_passengers) as decimal(5,1))  as 'died %'
from CTE1
group by rollup (
sex, class)
;

survival sex and class.jpg
Überleben nach Geschlecht
Das Durchschnittsalter der Passagiere

Werfen wir einen Blick auf das Durchschnittsalter nach Klasse und Geschlecht; nur aus Neugier und um die PIVOT-Anweisung in SQL zu üben ;-).

select * from
(
select

pclass,
sex,
age
from passengers
) t
PIVOT(
avg(
age)
for sex IN ([female], [male])
) as pvt
order by pclass
;

avg_age_class and sex.jpg

Die Passagiere in der dritten Klasse waren deutlich jünger als in der ersten Klasse (15 Jahre jünger für Frauen und 16 jünger Jahre für Männer). Hatten die Jüngeren und Fitteren eine bessere Überlebenschance?

Die Altersgruppen berechnen

Die Spalte [Alter] hat 99 verschiedene Werte, daher macht es wenig Sinn, die Überlebensrate für jeden von diesen zu berechnen. Zweckmäßiger ist der Ansatz, [Alter] in Bins zu gruppieren. Ich habe dazu die folgende Hilfstabelle erstellt:

-- drop table age_ranges;

​

/*  create a table age_ranges */
 

IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='age_ranges' and xtype='U')
create table age_ranges (
agegrp int primary key, agegroup varchar(20), age_from int, age_to int) ;

insert into age_ranges 
values 
(1,'under 10',0,10),
(2,'between 10 and 20',10,20),
(3,'between 20 and 30',20,30),
(4,'between 30 and 40',30,40),
(5,'between 40 and 50',40,50),
(6,'over 50',50,99),
(7,'missing value',null,null);

select * from age_ranges;
-- truncate table age_grouping;

age range table.jpg

Diese Abfrage verknüpft die Hilfsbereichstabelle mit der Faktentabelle

Auf diese Weise wird jedem Passagier eine Altersklasse zugeordnet

select 
passengerid,
age,
r.
agegroup
from passengers p
inner join age_ranges r
on p.age >= r.age_from and p.age < r.age_to
order by p.age;

agerange join.jpg
Überleben nach Altersgruppe

Nun ist es ziemlich einfach, die Überlebensrate pro Altersgruppe zu berechnen. Zu beachten ist, dass die Gesamtzahl der Passagiere in Altersklassen nur 1046 beträgt, die Anzahl der Überlebenden nur 427 statt 500, da Nullwerte in der Spalte [age] nicht berücksichtigt wurden (263 Nullen).

select 
agegrp,
agegroup,
count(agegrp) as count_by_age_group,
format((1.00 * count(
agegrp) / max(all_passengers)),'P') as bin_percent,
sum(
survived) as count_survived,
format((1.00 * sum(
survived)  /  count(agegrp)),'P') as percent_survived
from (
       select
       
passengerID,
       
age,
       
survived,
       r.[
agegrp],
       r.[
agegroup],
       count(
passengerid) over() as all_passengers
       from passengers p
     
 /* join with the age ranges table */
       inner join age_ranges r on p.age >= r.age_from and p.age < r.age_to
   ) sub1
group by agegrp,agegroup
order by agegrp;

survived by age bin.jpg

Säuglinge und Kinder unter 10 Jahren bildeten die kleinste Gruppe (7,8 %), ihre Überlebenschance war jedoch fast doppelt so hoch (61 %) im Vergleich zu Erwachsenen, die eine Überlebensrate zwischen 36 und 42 % hatten.

Alles verbinden (Klasse, Geschlecht, Alter)

Nun sehen wir die Ergebnisse nach Altersklasse, Geschlecht und Klasse.

with CTE1 as
(
select

passengerID,
pclass,
sex,
age,
survived,
case when
age <= 10 then 1
when
age > 10 and age <= 20 then 2
when
age > 20 and age <= 30 then 3
when
age > 30 and age <= 40 then 4
when
age > 40 and age <= 50 then 5
when
age > 50 then 6
else 7
end as agesegm
from passengers
where
age is not null
)
select 
c.
pclass,
c.sex,
t.agegroup,
count(
passengerid) as no_of_passenger,
sum(c.
survived) as survived,
format(1.00 * sum(c.
survived)  / count(passengerid),'P') as survicedpercent
-- replicate('|',floor(100 * sum(v.survived)  / count(passengerid))) as visual
from CTE1 c
inner join age_grouping t on t.agegrp = c.agesegm
group by c.
pclass,c.sex, c.agesegm,t.agegroup
order by c.pclass,c.sex, c.agesegm
;

Die Ergebnisliste hat 36 Zeilen (3 Klassen, 2 Geschlechter, 6 Altersgruppen), wovon ich hier nur die ersten 10 Zeilen zeige. Es fällt eine sehr interessante Tatsache auf, wenn wir die Daten visualisieren (Entschuldigung, dafür habe ich Power BI verwendet, da SQL wirklich nicht die besten Visualisierungskapazitäten hat :-).

​

​Die männlichen Passagiere der zweiten Klasse hatten eine noch schlechtere Überlebenschance als die in der dritten Klasse (mit Ausnahme der Kleinkinder): Nur 5 von 147 Männern überlebten die Überfahrt in der zweiten Klasse (3,4%), die Überlebensrate in der dritten Klasse für diese Passagiergruppen waren 15,1 % und 33,8 % in der ersten Klasse.

survival table class sex age.jpg
survival percent pbi.jpg
Die 10 besten Überlebensgruppen

Ok, das sind viele Zahlen. Wie wäre es, wenn wir uns die Top 10 Gruppen nach Klasse, Geschlecht und Alter ansehen, um zu sehen, wer die besten Chancen hatte.

select top 10
a.
pclass,
a.
sex,
ag.
agegroup,
count(
pclass) as no_of_passenger,
sum(
survived) as survived,
format(1.00 * sum(
survived)  / count(passengerid),'P') as survicedpercent 
from v_agegrouping a       (I used a view here to make my life easier)
inner join age_grouping ag on ag.
agegrp = a.agesegm
group by a.pclass,ag.agegroup,a.sex
order by (1.00 * sum(survived)  / count(passengerid) ) desc, pclass
;

top 10 survivors.jpg

Es überrascht nicht, dass die 10 besten Überlebensgruppen hauptsächlich Frauen sind, tatsächlich 8 von 10, während männliche Säuglinge die beiden verbleibenden Gruppen von Überlebenden in den Top 10 ausmachten. Nur 14 männliche Passagiere sind in den 10 besten Überlebensgruppen, aber 159 weibliche.

Und da ist noch mehr:
Solo oder mit Familie: wer hat überlebt? 

Um diese Frage zu beantworten, müssen wir uns die Informationen zu Geschwistern oder Ehepartnern [sibsp] bzw. Eltern oder Kindern [parch] sowie die Spalten [ticket] und [last_name] ansehen, um herauszufinden, wer Alleinreisende(r) oder Familienmitglied ist . Dabei zeigen sich einige Herausforderungen:

In der ersten Klasse reisten zum Beispiel Familien oft mit ihren Angestellten/Bediensteten auf dem lgeichen Ticket (Beispiel A), aber diese Passagiere gehören nicht zur Familie, die Spalten [sibsp] und [parch] sind. Sollen also Passagiere, die als Personal reisten als Alleinreisende gelten? In der dritten Klasse gibt es ebenfalls Passagiere, die auf dem gleichen Ticket reisten, aber wahrscheinlich als Alleinreisende zu betrachten sind (Beispiel B). Es gibt jadoch auch Familien und Paare, die aus irgendwelchen Günden mit unterschiedlichen Tickets reisen, aber nicht als sibsp gekennzeichnet sind, wie das Beispiel C zeigt. 

Beispiel A
familysize_A.jpg
Beispiel B
exampleB.png
Beispiel C
exampleD.png
Die Passagier-Segmentierung erstellen

Schließlich entschied ich mich für die folgende Segmentierung. Das offensichtlichste Attribut für eine Familienzugehörigkeit besteht in dem Wert in der Spalte [sibsp] oder [parch]; diese habe ich als "Familiengröße" bezeichnet. Passagiere mit einer Familiengröße von 0, einer lastname_ticket-Zählung (partition by last_name and ticket) von 1 und einer Ticketzählung von 1 gelten als Alleinreisende. Die restlichen Passagiere werden als "unklarer Status" eingestuft, dies sind 127 Passagiere. Ich nenne diese Segmentierung [fam_group].

/* calculate the familysize3 */

with CTE1 as
(
select  

passengerID,
survived,
pclass,
last_name,
first_name,
ticket,
cabin,
sex,
age,
sibsp,
parch
,
familysize,
lastnamecount,
-- lastnamerow,
lastnameticketcount,
ticketcount,
ticketrow
from 
   (
   select 
   *,
   
sibsp + parch as familysize,                                                    -- simply add sibsp and parch to determine, whether there is a value > 0
   count(last_name) over(partition by last_name) as lastnamecount,                            -- count the occurance of name by last_name
   -- ROW_NUMBER() over(partition by last_name order by last_name) as lastnamerow, -- the  last name in order of their occurance
   count(last_name) over(partition by last_name, ticket) as lastnameticketcount,    -- count the name by last_name and ticketnumber grouping
   count(ticket) over(partition by ticket) as ticketcount,                                                           -- number of tickets count by the ticket number
   ROW_NUMBER() over(partition by ticket order by ticket) as ticketrow                       -- count of ticket number in order of their occurance
   from passengers
   ) t1
)
select * from
(
Select *,
   case 
   when familysize > 0 then 'Rel'                                                  
 -- here they are or having relatives of some kind -> means family
   when familysize = 0 and lastnameticketcount = 1 and ticketcount = 1 then 'Solo'                        -- the familysize is 0 and there is one ticket per name -> solo
   else 'unclear' 
   end fam_group
from CTE1
) t2
order by last_name,ticket,ticketrow;

familystatus_final2.jpg

Mal sehen, ob das die Frage beantwortet. Die Segmentierung in eine CTE einfügen und die Analyse laufen lassen:

Sie zeigt, dass bei Passagieren mit Verwandten (Ehepartner oder Kinder) die Überlebenschancen für männliche (16,4 % ↗ 25,9 %) und weibliche (66,9 % ↗ 72,4 %) Passagiere im Vergleich zu Alleinreisenden leicht erhöht ist. Es ist zu vermuten, dass Familienmitglieder eher einen gemeinsamen Platz in einem der Rettungsbote bekamen als Soloreisende.

solo travellers survival.jpg
Und zu guter Letzt: der Einschiffungshafen

Die Überlebensrate nach Einschiffungshafen ist ein perfektes Beispiel dafür, dass man manchmal zweimal hinschauen muss, um die Zahlen richtig zu interprätieren.

select 
t1.
embarked,
max(pass_embarked) as allpassengers,
sum(
survived) as survived,
100.00 * sum(
survived) / max(pass_embarked) as survival_rate
from 
(
   select 
       
passengerid as pass_id, 
       
embarked,
       
survived,
       count(*) over (partition by
embarked) as pass_embarked
   from passengers
   where embarked is not null 
-- two passengers have no embarked code
) t1
where survived = 1
group by t1.embarked;

embarked.jpg

Es zeigt, dass die Überlebenschancen bei der Einschiffung in Cherbourg deutlich höher zu sein scheinen als bei einer Reise von Queenstown oder Southampton. Das macht natürlich allein keinen Sinn, also muss es eine zugrunde liegende Information geben, die dieses Ergebnis verursacht. Eine Erklärung kann in einer unterschiedlichen Verteilung der Passagiere liegen, zum Beispiel nach ihrer Passagierklasse oder der Kabine, in der sie bereisten. Da wir keine konsistenten Informationen über die Kabine haben, werfen wir einen Blick auf die Reiseklasse.

-- the number of passengers embarked
with CTE1 as
(
   select
   
embarked,
   
pclass,
   count(
passengerid) as passengercount,
   100.00 * count(
passengerid) / max(no_pass_emb)  as perc_emb_class,
   100.00 * count(
passengerid) / max(pass_all) as perc_pass_all
   from 
       (
           select
           
passengerid,
           
pclass,
           
embarked,
           count(
passengerid) over(partition by embarked) as no_pass_emb,
           count(*) over () as pass_all
           from passengers
           where embarked is not null
       ) sub1
   group by embarked,pclass
)
,

-- survival rate by embarked
CTE2 as 
(
   select 
   
embarked,
   max(pass_embarked) as embarkedpassengers,
   sum(
survived) as survived,
   100.00 * sum(
survived) / max(pass_embarked) as survival_rate
   from 
       (
           select 
               
passengerid as pass_id, 
               
embarked,
               
survived,
               count(*) over (partition by
embarked) as pass_embarked
               from passengers
           where embarked is not null
       ) sub2
where survived = 1
group by embarked
)
-- select * from CTE2;

-- return information
select 
case 
when c1.
embarked = 'C' then 'Cherbourg (FRA)'
when c1.
embarked = 'Q' then 'Queenstown (IRL)'
when c1.
embarked = 'S' then 'Southampton (ENG)'
else 'missing'
end as embarked_harbour,
c1.
pclass,
c1.
passengercount,
convert(decimal(6,1),c1.perc_pass_all) as all_pass_perc,
convert(decimal(6,1),c1.perc_emb_class) as embarked_pass_perc,
convert(decimal(6,1),c2.survival_rate) as embarked_survival_perc
from CTE1 c1
inner join CTE2 c2 on c2.
embarked=c1.embarked
;

embarked and class.jpg

Die Antwort liegt in der Passagierverteilung nach Passagierklasse. Die höhere Überlebensrate für die Passagiere von Cherbourg liegt in der proportional höheren Anzahl von Passagieren, die in der ersten Klasse reisten (52,2%); wie wir wissen, war die Überlebensrate in der ersten Klasse deutlich höher als in der zweiten oder dritte Klasse. Im Gegensatz dazu reisten 92 % der Passagiere von Queenstown in Klasse 3 mit einer viel geringeren Überlebenschance, ebenso wie von Southampton, wo 52 % der Passagiere in der dritten Klasse reisten.

/* calculate the survival rate by family group */
select 
t1.fam_group,

sex,
-- count(*) as num_of_pass_by_famgroup,
max(total_passengers) as allpassengers,
sum(
survived) as survived,
format(round(sum(
survived) / cast(max(total_passengers) as float),4),'P') as survival_rate
from
(
select 
       
passengerid as pass_id, 
       fam_group,
       
survived,
       
sex,
       count(*) over (partition by
fam_group, sex) as total_passengers
from CTE2
) t1
where survived = 1
group by t1.fam_group, t1.sex;

und schließlich ...

Mit diesem Beitrag möchte ich nicht nur die Verwendung von SQL zur Datenanalyse darstellen, ich empfinde es auch als spannend und befriedigend, Einblicke in die menschlichen Schicksal bei einer der größten Passagierschiffkatastrophen der Geschichte zu erhalten.

​

Und natürlich bin ich nicht der Erste, der die Titanic-Katastrophe mit SQL analysiert, dieser Artikel wurde von Do Lee und seinem Artikel auf Kaggle inspiriert Kaggle Titanic Competition in SQL | by Do Lee | Towards Data Science.

bottom of page