Titanic dataset

Titanic dataset – esercizio di Machine Learning

Abbiamo, fino a questo punto, parlato sempre di concetti teorici. In questo nuovo articolo voglio realizzare un esercizio utilizzando il dataset Titanic che ci permette di mettere in pratica le nozioni che abbiamo studiato. Vediamo di che cosa si tratta e come portare avanti il progetto.

Ho deciso di utilizzare questo tipo di dataset perchè è uno dei più utilizzati e dal mio punto di vista è anche uno gestibili date le nostre conoscenze fino ad ora apprese. L’obbiettivo principale dell’utilizzo di questo gruppo di dati è eseguire la previsione sulla soppravivenza di un passeggero in base a determinati dati e caratteristiche che lo identificano.

Il Titanic dataset è facilmente scaricabile da qui.

Per comodità ho deciso di utilizzare due dataset divisi:

  • training set (train.csv) che viene utilizzato per creare modelli di machine learning e il nostro modello deve essere costruito sulle caratteristiche dei passeggeri come il sesso e la classe;
  • test set (test.csv) lo utilizzeremo per vedere come performa il nostro modello su dati invisibili. Attraverso esso andremo a prevedere se i passeggeri sono sopravvissuti o meno al disatro del Titanic. 

Sviluppiamo l'esercizio

Importiamo le librerie necessarie

				
					# algebra lineare
import numpy as np 

# data processing
import pandas as pd 

# visualizzazione dei dati con i grafici
import seaborn as sns
%matplotlib inline
from matplotlib import pyplot as plt
from matplotlib import style

# Algoritmi
from sklearn import linear_model
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import Perceptron
from sklearn.linear_model import SGDClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.svm import SVC, LinearSVC
from sklearn.naive_bayes import GaussianNB
				
			

Come possiamo notare, utilizziamo tutte librerie che abbiamo visto nel corso dei nostri articoli. Abbiamo a disposizione le librerie principali, utili a svolgere la maggior parte dei compiti di Machine Learning come la matplotlib che permette di visualizzare graficamente i dati e rappresentare in maniera più ordinata ciò che abbiamo a disposizione.

Recuperiamo i dati dai dataset in formato csv

				
					test_df = pd.read_csv("test.csv")
train_df = pd.read_csv("train.csv")
				
			

Il Titanic datset va inserito all’interno della cartella del progetto o indicato con il percorso dove risiede.

Esplorazione e analisi dei dati

				
					train_df.info()
				
			
titanic dataset

La funzione info permette di visualizzare alcuni importanti informazioni riguardanti il dataset train. Il set di addestramento, come ci viene indicato, ha 891 esempi e 11 caratteristiche più la variabile di destinazione (Survived). In più sappiamo che due delle caratteristiche sono float, cinque sono numeri interi e cinque sono oggetti.

Utilizziamo ora un’altra semplice funzione da applicare allo stesso dataset e vediamo cosa succede:

				
					train_df.describe()
				
			
titanic dataset

Possiamo intuire già dalla tabella la funzionalità della funzione describe. Essa infatti ci indica chè sul totale dei passeggeri, il 38% è soppravissuto e l’età degli stessi varia da un minimo di 0,4 a un massimo di 80. Queste informazioni sono utili a inquadrare i dati e schiarirci le idee sul da farsi. 

Un’altra funzione molto utile è la funzione head(n) che permette di visualizzare le prime n righe del dataset:

				
					train_df.head(8)
				
			

Dalla tabella sopra, possiamo notare alcune cose. Dobbiamo convertire molte funzionalità in numeri in modo che gli algoritmi di apprendimento automatico possano elaborarle. Inoltre, possiamo vedere che le caratteristiche hanno intervalli molto diversi, che dovremo convertire all’incirca nella stessa scala. Possiamo anche individuare alcune altre funzionalità, che contengono valori mancanti (NaN = non un numero), alle quali dobbiamo far fronte.

Leggi articolo   Una somma per amica

Diamo uno sguardo più approfondito ai dati effettivamente mancanti:

				
					total = train_df.isnull().sum().sort_values(ascending=False)
percent_1 = train_df.isnull().sum()/train_df.isnull().count()*100
percent_2 = (round(percent_1, 1)).sort_values(ascending=False)
missing_data = pd.concat([total, percent_2], axis=1, keys=['Total', '%'])
missing_data.head(12)
				
			
titanic dataset

La funzione Imbarco(Embarked) ha solo 2 valori mancanti, che possono essere facilmente riempiti. Sarà molto più complicato gestire la funzione Età(“Age”), che ha 177 valori mancanti. La funzione Cabina(“Cabin”) necessita di ulteriori indagini, ma potremmo eliminarla dal set di dati, poiché manca il 77%.

Quali caratteristiche potrebbero contribuire ad un alto tasso di sopravvivenza?

Attraverso il comando:

				
					train_df.columns.values
				
			

andiamo a visualizzare le caratteristiche del train.csv del Titanic dataset. Ora, quali potrebbero essere quelle che garantiscono un alto tasso di soppravivenza per i passeggeri?

Per una mia personale valutazione il Ticket, il Nome e Id del passeggero sono influenti sulla possibilità di salvataggio. Ma vediamo in maniera dettagliata le altre caratteristiche come potrebbero influire.

Età e sesso

				
					survived = 'survived'
not_survived = 'not survived'
fig, axes = plt.subplots(nrows=1, ncols=2,figsize=(10, 4))
women = train_df[train_df['Sex']=='female']
men = train_df[train_df['Sex']=='male']
ax = sns.distplot(women[women['Survived']==1].Age.dropna(), bins=18, label = survived, ax = axes[0], kde =False)
ax = sns.distplot(women[women['Survived']==0].Age.dropna(), bins=40, label = not_survived, ax = axes[0], kde =False)
ax.legend()
ax.set_title('Female')
ax = sns.distplot(men[men['Survived']==1].Age.dropna(), bins=18, label = survived, ax = axes[1], kde = False)
ax = sns.distplot(men[men['Survived']==0].Age.dropna(), bins=40, label = not_survived, ax = axes[1], kde = False)
ax.legend()
_ = ax.set_title('Male')
				
			

Il codice qui sopra permette di creare due grafici distinti, uno per i passeggeri maschi e uno per le femmine. In questo modo andiamo a visualizzare graficamente il numero di sopravissuti e di morti per i due differenti generi. L’asse x indica l’età dei passeggeri mentre l’asse y indica il numero di sopravissuti/morti.

Preciso che con la funzione age.dropna( ) elimino i valori mancanti. Tutta la documetazione di Pandas è disponibile online.

titanic dataset

Notiamo che le donne hanno più possibilità di salvarsi se esse sono comprese tra 14 e 40 anni mentre gli uomini sono più propensi a uscirne vivi se essi hanno un’età compresa tra i 5 e i 18 anni. Le età quasi coincidono, anche se con piccole differenze, ma ciò ci permette di andare a creare dei gruppi di età in modo da avere ogni caratteristica sulla stessa scala di misura in maniera ancora più dettagliata.

Persone imbarcate, Pclass e Sesso

				
					FacetGrid = sns.FacetGrid(train_df, row='Embarked', size=4.5, aspect=1.6)
FacetGrid.map(sns.pointplot, 'Pclass', 'Survived', 'Sex', palette=None,  order=None, hue_order=None )
FacetGrid.add_legend()
				
			

La sopravvivenza sembra essere correlata all’imbarco dei passeggeri a seconda del sesso.

titanic dataset

Le donne sul porto Q e sul porto S hanno una maggiore probabilità di sopravvivenza. Le cose cambiano se esse si trovano sul porto C.

Gli uomini hanno un’alta probabilità di sopravvivenza se sono nel porto C, ma una bassa probabilità se sono nel porto Q o S.

Ma la soppravivenza sembra essere correlata anche alla classe. Vediamo tutto ciò attraverso un semplice grafico a barre:

				
					sns.barplot(x='Pclass', y='Survived', data=train_df)
				
			

Come vediamo chiaramente, le persone in classe 1 hanno probabilità maggiori di salavarsi rispetto a quelli in classe 2 e 3.

SibSp e Parch

SibSp e Parch avrebbero più senso come una funzione combinata, che mostra il numero totale di parenti che una persona ha sul Titanic. 

titanic dataset

Qui possiamo vedere che si aveva un’alta probabilità di sopravvivenza con 1 a 3 parenti, ma una più bassa se si aveva meno di 1 o più di 3 (tranne alcuni casi con 6 parenti).

Processiamo i dati

Nella fase iniziale abbiamo deciso di un’altatogliere l’Id del passeggero perchè non aveva nessuna correlazione con la soppravivenza. Ma nel set di test avremo bisogno anche di questo parametro in fase di presentazione dei dati.

Attraverso l’isitruzione sottostante togliamo la colonna PassengerId dal train set.

				
					train_df = train_df.drop(['PassengerId'], axis=1)
				
			

Dati mancanti

Cabina

Per quanto riguarda i dati mancanti, se ci ricordiamo, abbiamo a che fare con Cabin (687), Embarked (2) e Age (177). All’inizio ho pensato di dover cancellare la variabile ‘Cabin‘ ma…

Un numero di cabina assomiglia a ‘C123’ e la lettera si riferisce al ponte. Quindi estrarremo questi dati e creiamo una nuova caratteristica, che contiene il ponte. In seguito convertiremo la caratteristica in una variabile numerica. I valori mancanti saranno convertiti a zero. Nell’immagine qui sotto potete vedere i ponti attuali del Titanic, che vanno dalla A alla G.

				
					import re
deck = {"A": 1, "B": 2, "C": 3, "D": 4, "E": 5, "F": 6, "G": 7, "U": 8}
data = [train_df, test_df]

for dataset in data:
    dataset['Cabin'] = dataset['Cabin'].fillna("U0")
    dataset['Deck'] = dataset['Cabin'].map(lambda x: re.compile("([a-zA-Z]+)").search(x).group())
    dataset['Deck'] = dataset['Deck'].map(deck)
    dataset['Deck'] = dataset['Deck'].fillna(0)
    dataset['Deck'] = dataset['Deck'].astype(int) #ora possiamo eliminare la caratteristica cabin
train_df = train_df.drop(['Cabin'], axis=1)
test_df = test_df.drop(['Cabin'], axis=1)
				
			

Età

Ora possiamo affrontare il problema dei valori mancanti presenti all’interno della caratteristica dell’età. Ho creato un array che contiene numeri casuali, che sono calcolati in base al valore medio dell’età rispetto alla deviazione standard e a al valore is_null.

Leggi articolo   Media dei numeri di una lista
				
					data = [train_df, test_df]

for dataset in data:
    mean = train_df["Age"].mean()
    std = test_df["Age"].std()
    is_null = dataset["Age"].isnull().sum()
    # calcola numeri casuali tra la media, std e is_null
    rand_age = np.random.randint(mean - std, mean + std, size = is_null)
    # riempire i valori NaN nella colonna Age con valori casuali generati
    age_slice = dataset["Age"].copy()
    age_slice[np.isnan(age_slice)] = rand_age
    dataset["Age"] = age_slice
    dataset["Age"] = train_df["Age"].astype(int)
    train_df["Age"].isnull().sum()
				
			

Imbarcati

In precedenza abbaimo visto che per la caratteristica imbarcati abbiamo solo due valori mancanti. Per questo motivo ho deciso di riempire la colonna con il valore più comune.

				
					train_df['Embarked'].describe()
				
			
titanic dataset
				
					common_value = 'S'
data = [train_df, test_df]

for dataset in data:
    dataset['Embarked'] = dataset['Embarked'].fillna(common_value)
				
			

Caratteristiche di conversione in titanic dataset

Attraverso l’utilizzo della funzione info riusciamo a capire le caratteristiche di che tipo sono ( Integer, Float … ). La caratteristica ‘Fare’ è di tipo float e per questo deve essere convertita perchè dobbiamo trattarla con altre caratteristiche di tipo oggetto come il Nome, il Sesso e il Ticket.

Fare

				
					data = [train_df, test_df]

for dataset in data:
    dataset['Fare'] = dataset['Fare'].fillna(0)
    dataset['Fare'] = dataset['Fare'].astype(int)
				
			

Eseguendo il codice qui sopra trasformiamo la nostra caratteristica.

titanic dataset

Nome

Per estrapolare ancora più informazioni andiamo ad utilizzare la caratteristica Nome per creare i titoli delle persone, ad esempio Mr, Miss … Facciamo ciò perchè i nomi delle persone non sono utili al nostro scopo a differenza del sesso identificando così il numero di donne e uomini. Creiamo una nuova semplice funzione:

				
					data = [train_df, test_df]
titles = {"Mr": 1, "Miss": 2, "Mrs": 3, "Master": 4, "Rare": 5}

for dataset in data:
    # Estraiamo i titoli
    dataset['Title'] = dataset.Name.str.extract(' ([A-Za-z]+)\.', expand=False)
    # sotituiamo i titoli con uno più comune o impostiamo Rare
    dataset['Title'] = dataset['Title'].replace(['Lady', 'Countess','Capt', 'Col','Don', 'Dr','Major', 'Rev', 'Sir', 'Jonkheer','Dona'], 'Rare')
    dataset['Title'] = dataset['Title'].replace('Mlle', 'Miss')
    dataset['Title'] = dataset['Title'].replace('Ms', 'Miss')
    dataset['Title'] = dataset['Title'].replace('Mme', 'Mrs')
    # Mappiamo i valori dei titoli
    dataset['Title'] = dataset['Title'].map(titles)
    # riempiamo NaN con valore 0
    dataset['Title'] = dataset['Title'].fillna(0)
train_df = train_df.drop(['Name'], axis=1)
test_df = test_df.drop(['Name'], axis=1)
				
			

Eseguendo funzione info vediamo che la tabella nome è scomparsa e abbiamo creato così la caratteristica titles.

Sesso e Ticket

Come prima cosa, convertiamo la caratteristica sesso in numero, mappando i valori.

				
					genders = {"male": 0, "female": 1}
data = [train_df, test_df]

for dataset in data:
    dataset['Sex'] = dataset['Sex'].map(genders)
				
			

Mentre, con la caratteristica Ticket, dato il suo numero elevato di elementi presenti (681) decido di eliminarlo dal dataset perchè troppo grande per creare delle categorie:

				
					train_df = train_df.drop(['Ticket'], axis=1)
test_df = test_df.drop(['Ticket'], axis=1)
				
			

Imbarcati

Convertiamo semplicemente la caratteristica Embarked in numerica:

				
					ports = {"S": 0, "C": 1, "Q": 2}
data = [train_df, test_df]

for dataset in data:
    dataset['Embarked'] = dataset['Embarked'].map(ports)
				
			

Creiamo le categorie

E’ giunto il momento di creare le categorie per il nostro Titanic dataset. Andiamo a vedere come procedere:

Età

Trattiamo ora la caratteristica età. Come prima cosa dobbiamo convertirla da float a integer. Solo successivamente possiamo creare una nuova variabile chiamata ‘AgeGroup‘ categorizzando ogni età in un gruppo. E’ fondamentale fare attenzione a come creiamo i nostri gruppi in modo che l’80% dei dati non finisca, per esempio, nel gruppo 1.

				
					data = [train_df, test_df]
for dataset in data:
    dataset['Age'] = dataset['Age'].astype(int)
    dataset.loc[ dataset['Age'] <= 11, 'Age'] = 0
    dataset.loc[(dataset['Age'] > 11) & (dataset['Age'] <= 18), 'Age'] = 1
    dataset.loc[(dataset['Age'] > 18) & (dataset['Age'] <= 22), 'Age'] = 2
    dataset.loc[(dataset['Age'] > 22) & (dataset['Age'] <= 27), 'Age'] = 3
    dataset.loc[(dataset['Age'] > 27) & (dataset['Age'] <= 33), 'Age'] = 4
    dataset.loc[(dataset['Age'] > 33) & (dataset['Age'] <= 40), 'Age'] = 5
    dataset.loc[(dataset['Age'] > 40) & (dataset['Age'] <= 66), 'Age'] = 6
    dataset.loc[ dataset['Age'] > 66, 'Age'] = 6
train_df['Age'].value_counts()
				
			

Fare

Per la caratteristica ‘Fare’, dobbiamo fare lo stesso che con la caratteristica ‘Età’. Ma non è così facile, perché se tagliassimo la gamma dei valori della tariffa in alcune categorie ugualmente grandi, l’80% dei valori cadrebbe nella prima categoria. Fortunatamente, possiamo usare la funzione “qcut()” di Scikit-learn, per vedere come possiamo formare le categorie.

				
					data = [train_df, test_df]

for dataset in data:
    dataset.loc[ dataset['Fare'] <= 7.91, 'Fare'] = 0
    dataset.loc[(dataset['Fare'] > 7.91) & (dataset['Fare'] <= 14.454), 'Fare'] = 1
    dataset.loc[(dataset['Fare'] > 14.454) & (dataset['Fare'] <= 31), 'Fare']   = 2
    dataset.loc[(dataset['Fare'] > 31) & (dataset['Fare'] <= 99), 'Fare']   = 3
    dataset.loc[(dataset['Fare'] > 99) & (dataset['Fare'] <= 250), 'Fare']   = 4
    dataset.loc[ dataset['Fare'] > 250, 'Fare'] = 5
    dataset['Fare'] = dataset['Fare'].astype(int)
				
			

Creiamo le nuove caratteristiche

Il Titanic dataset è molto ampio e richiede un po’ di lavoro dietro. Questo è il bello del Machine Learning. Vediamo ora come creare le nuove caratteristiche utili ai modelli successivi.

Aggiungo due nuove caratteristiche al set di dati, calcolandole a partire da altre caratteristiche.

Age_class e Fare_per_person

				
					data = [train_df, test_df]
for dataset in data:
    dataset['Age_Class']= dataset['Age']* dataset['Pclass']
				
			
				
					for dataset in data:
    dataset['Fare_Per_Person'] = dataset['Fare']/(dataset['relatives']+1)
    dataset['Fare_Per_Person'] = dataset['Fare_Per_Person'].astype(int)
train_df.head(10)
				
			

Creaimo i modelli di Machine Learning per il Titanic Dataset

Iniziamo a mettere in pratica ciò che abbiamo trattato durante il corso di tutti gli articoli. Abbiamo imparato ad utilizzare i vari modelli di Machine Learning e ora siamo in grado di confontarli tra di loro utilizzando, per questo progetto, il Titanic dataset.

				
					X_train = train_df.drop("Survived", axis=1)
Y_train = train_df["Survived"]
X_test  = test_df.drop("PassengerId", axis=1).copy()
				
			

Discesa stocastica del gradiente

				
					sgd = linear_model.SGDClassifier(max_iter=5, tol=None)
sgd.fit(X_train, Y_train)
Y_pred = sgd.predict(X_test)

sgd.score(X_train, Y_train)

acc_sgd = round(sgd.score(X_train, Y_train) * 100, 2)
				
			

Foreste casuali

				
					random_forest = RandomForestClassifier(n_estimators=100)
random_forest.fit(X_train, Y_train)

Y_prediction = random_forest.predict(X_test)

random_forest.score(X_train, Y_train)
acc_random_forest = round(random_forest.score(X_train, Y_train) * 100, 2)
				
			

Regressione Logistica

				
					logreg = LogisticRegression()
logreg.fit(X_train, Y_train)

Y_pred = logreg.predict(X_test)

acc_log = round(logreg.score(X_train, Y_train) * 100, 2)
				
			

Perceptron

				
					perceptron = Perceptron(max_iter=5)
perceptron.fit(X_train, Y_train)

Y_pred = perceptron.predict(X_test)

acc_perceptron = round(perceptron.score(X_train, Y_train) * 100, 2)
				
			

Vettori lineari di supporto

				
					linear_svc = LinearSVC()
linear_svc.fit(X_train, Y_train)

Y_pred = linear_svc.predict(X_test)

acc_linear_svc = round(linear_svc.score(X_train, Y_train) * 100, 2)
				
			

Albero decisionale

				
					decision_tree = DecisionTreeClassifier() 
decision_tree.fit(X_train, Y_train)  
Y_pred = decision_tree.predict(X_test)  
acc_decision_tree = round(decision_tree.score(X_train, Y_train) * 100, 2)
				
			

Quale performa di più ?

Bene! Ora che abbiamo addestrato tutti questi modelli è il momento di vedere quale performa di più tra tutti per quanto riguarda l’utilizzo del Titanic dataset.

				
					results = pd.DataFrame({
    'Model': ['Support Vector Machines', 'Logistic Regression', 
              'Random Forest', 'Perceptron', 
              'Stochastic Gradient Decent', 
              'Decision Tree'],
    'Score': [acc_linear_svc, acc_log, 
              acc_random_forest, acc_perceptron, 
              acc_sgd, acc_decision_tree]})
result_df = results.sort_values(by='Score', ascending=False)
result_df = result_df.set_index('Score')
result_df.head(6)
				
			

Come possiamo vedere dalla tabellina qua sopra, il modello che performa di più è sicuramente quello in cui abbiamo utilizzato le foreste casuali, l’ultimo algormento da noi trattato. Vediamo ora, in maniera ancora più approfndita alcuni aspetti che scikit-learn ci permette di utilizzare.

Infatti, attraverso questa libreria, possiamo misurare l’importanza di una caratteristica guardando quanto i nodi dell’albero, che usano quella caratteristica, riducono l’impurità in media (tra tutti gli alberi della foresta). Essa calcola questo punteggio automaticamente per ogni caratteristica dopo l’addestramento e scala i risultati in modo che la somma di tutti i valori sia uguale a 1:

				
					importances = pd.DataFrame({'feature':X_train.columns,'importance':np.round(random_forest.feature_importances_,3)})
importances = importances.sort_values('importance',ascending=False).set_index('feature')
importances.head(15)
				
			
titanic dataset
titanic dataset

Ciò ci permette di capire che sia Parch che not_alone sono poco utili alla foresta casuale e quindi possiamo eliminarle da titanic dataset in modo che performi in maniera più ottimale ancora. Vediamo come fare:

				
					train_df  = train_df.drop("not_alone", axis=1)
test_df  = test_df.drop("not_alone", axis=1)

train_df  = train_df.drop("Parch", axis=1)
test_df  = test_df.drop("Parch", axis=1)
				
			
				
					random_forest = RandomForestClassifier(n_estimators=100, oob_score = True)
random_forest.fit(X_train, Y_train)
Y_prediction = random_forest.predict(X_test)

random_forest.score(X_train, Y_train)

acc_random_forest = round(random_forest.score(X_train, Y_train) * 100, 2)
print(round(acc_random_forest,2,), "%")
				
			

Ritestando il Titanic dataset otteniamo un punteggio di 92,48. Quindi, comprando il punteggio con il valore precedente, confermiamo l’influenza delle ultime due caratteristiche e notiamo che nonostante eliminiamo due variabili il risultato non cambia per il Titanic dataset. Di solito, è molto probabile che più caratteristiche hai e più è facile incorrere in problemi come overfitting

Concludiamo il nostro progetto

Siamo arrivati alla conclusione di questo progetto nel quale abbiamo utilizzato il Titanic dataset. All’inizio abbiamo analizzato i dati, capito quali fossero i mancanti e gestito in maniera ottimale le caratteristiche. In tutto ciò ci è venuto in aiuto la libreria matplotlib e seaborn per la visualizzazione dei dati tramite i grafici a barre e non solo.

Abbiamo effettuato la fase di preelaborazione dei dati dove abbiamo calcolato i valori mancanti, convertito le caratteristiche in valori numerici, raggruppato i valori in categorie e creato alcune nuove caratteristiche. In seguito abbiamo iniziato ad addestrare 6 diversi modelli di apprendimento automatico, trattati negl’articoli precedenti e ne abbiamo scelto la foresta casuale perchè quella più performante.

Spero che questo esercizio sia servito a chiarirvi le idee su come procede quando ci troviamo davanti a progetti come questo. Il Titanic dataset può essere considerato una base sulla quale effettuare altri test come vedremo prossimamente, solo dopo aver chiarito altri concetti riguardanti i modelli di addestramento.

Condividi il post

Condividi su facebook
Condividi su google
Condividi su twitter
Condividi su email
Condividi su whatsapp