Pour faire suite à mon précédent article sur la prédiction des survivants du Titanic, il me parait important d’illustrer quelques autres techniques et par là même aller plus loin dans la modélisation. Cet article est donc dédié au travail sur les caractéristiques qui nous sont données. Comme d’habitude pour ce type de projet de Machine Learning j’utiliserai Python, Scikit-learn et Jupyter.
Travail préalable
- Déclarer les librairies Python que nous allons utiliser (Pandas, RegEx, scikit-learn, etc.)
- Importer/lire les fichiers d’entrainement et test dans des DataFrame Pandas
- Créer un DataFrame complet (full) qui concatène les deux jeux de données précédents.
import pandas as pd import re from sklearn.ensemble import RandomForestClassifier from sklearn.svm import LinearSVC from sklearn.preprocessing import MinMaxScaler train = pd.read_csv("./data/train.csv") test = pd.read_csv("./data/test.csv") full = pd.concat([train, test]) # Assemble les deux jeux de données
Feature ingeneering
L’objectif ce billet est de travailler sur les caractéristiques et non sur les algorithmes eux-même. Pour cela nous allons retravailler certaines données qui en l’état sont peu ou pas exploitable.
Ticket & Prix : Avez-vous remarqué que le tarif mentionné n’est pas le prix unitaire par personne ? et que la donnée Ticket n’est pas une clé unique ? Nous allons devoir corriger celà en calculant le prix unitaire par personne et modifier les prix (Fare) faux (c’est à dire les billets groupés)
Passager sans prix : C’est une erreur dans les données. En effet un passager n’a pas de prix (Fare). Nous allons devoir lui en attribuer un afin de conserver cette donnée.
Le nom de famille : Ce n’est pas une information directement fournie, il va donc falloir « parser » la chaine Name pour la récupérer correctement. Ensuite on regroupera les données (train & test) par nom de famille et on comptera le nombre de personnes par famille. Fort de ce constat, il sera interressant de fair en encodage one-hot sur les familles de plus de 2, 3 personnes.
Le titre : Comme le nom de famille il faut récupérer cette information à partir de la colonne Name. Ensuite il sera interressant de re-catégoriser ces titre pour par exemple avoir 3 catégories finales : Femmes & enfants, adultes et VIP.
l’Age : l’age est une information interressante en soi mais créer des catégories d’age en complément l’est encore plus.
Nous nous arreterons là pour cet article, mais il reste encore bien d’autres pistes d’amélioration.
Ticket & Prix
Tout d’abord regardons la caractéristique Ticket. Vérifions que tous les passagers ont bien un ticket :
noticket = [] full['Ticket'].fillna('X') for ticketnn in full['Ticket']: if (ticketnn == 'X'): noticket.append(1) else: noticket.append(0) pd.DataFrame(noticket)[0].value_counts()
Bonne nouvelle, tous les passagers possèdent cette information !
test['Ticket'].value_counts().head()
Regardons maintenant les valeurs distinctes de ces Tickets :
PC 17608 5
113503 4
CA. 2343 4
C.A. 31029 3
347077 3
Name: Ticket, dtype: int64
Interressant, les valeurs ne sont pas uniques comme nous aurions pu le penser. Celà est en fait du que l’on pouvait avoir des tickets groupés (plusieurs personnes avec le même ticket). Clà change beaucoup de choses car si le Ticket pouvait être groupé, le prix aussi donc.
Il va donc falloir diviser le prix du Ticket par le nombre de personnes ayant le même ticket !
Calcul du prix unitaire du billet
Pour ce faire nous allons utiliser les capacités de Pandas a effectuer des jointures (gauche) entres DataFrame. Au préalable nous allons constituer un DataFrame qui regroupe les Tickets avec leur nombre d’occurences : TicketCounts. Ensuite nous ferons une jointure gauche entre le jeu de données et ce nouveau DataFrame. Nous n’aurons ensuite plus qu’à ajouter une colonne PrixUnitaire qui divise le prix total par le nombre de personne sur le Ticket. Attention ici de bien utiliser la fonction fillna() sur le nombre de ticket.
# Prépartion d'un DF (TicketCounts) contenant les ticket avec leur nb d'occurence TicketCounts = pd.DataFrame(test['Ticket'].value_counts().head()) TicketCounts['TicketCount'] = TicketCounts['Ticket'] # renomme la colonne Ticket TicketCounts['Ticket'] = TicketCounts.index # rajoute une colonne Ticket pour le merge (jointure) # Reporte le résultat dans le dataframe test (jointure des datasets) fin = pd.merge(test, TicketCounts, how='left', on='Ticket') fin['PrixUnitaire'] = fin['Fare'] / fin['TicketCount'].fillna(1)
Passager sans Prix !
Attention, car nous avons aussi un passager qui n’a pas de Prix. Regardons de qui il s’agit :
import numpy as np test.loc[np.isnan(test['Fare'])]
Il s’agit d’un passager de 3ème classe, calculons donc le prix moyen de ce type de billet:
test.loc[test['Pclass'] == 3]['Fare'].mean()
12.459677880184334
Nous affecterons ce prix à ce passager.
Le Nom de famille
Le nom de famille n’est pas immédiatement utilisable. Il faut l’extraire de la caracéristique Name qui contient d’autres informations comme le titre. Utilisons pour celà les RegEx :
familynames = [] for noms in full["Name"]: familynames.append(re.search('([A-Za-z0-9]*),\ ([A-Za-z0-9 ]*)\. (.*)', noms).group(1)) pdfamilynames = pd.DataFrame(familynames, columns = ['familynames'])
L’idée est maintenant de faire un encodage one-hot avec le nom de famille. Ça peut paraitre un peu fou mais nous avons peu de données et certains noms de famille apparaissent dans les deux jeux de données justement.
Nous allons tout d’abord créer un DataFrame avec les noms de famille apparaissant 2 fois ou plus :
# Créé une liste des noms de famille avec plus de 2 occurences famsurv = full.join(pdfamilynames) famCount = famsurv['familynames'].value_counts() pdfamCounts = pd.DataFrame(famCount, columns = ['familynames']) pdfamCounts['famCount'] = pdfamCounts['familynames'] pdfamCounts['familynames'] = pdfamCounts.index pdfamCounts[pdfamCounts['famCount'] >= 2]
Ce DataFrame pourra être ensuite utilisé au travers d’une fonction pour rajouter les colonnes dummies (one-hot) :
# Fonction ajoutant les colonnes noms famille dans un DF def addColumnFamilyName(data): # ajoute les colonnes nulles avec les noms de famille for family in pdfamCounts['familynames']: data[family] = 0 # récupère le nom de famille dans le DF for idx, f in enumerate(data["Name"]): # Modifie les colonnes dummies du nom de famille en 1 ou 0 selon le nom de famille iNom = re.search('([A-Za-z0-9]*),\ ([A-Za-z0-9 ]*)\. (.*)', f).group(1) for col in data.columns: if (col == iNom): data.loc[idx, col] = 1
Nous utiliserons cette fonction lors de la préparation des données (plus loin).
Le Titre
De la même manière que le nom de famille, nous devons extraire le titre en parsant la caractéristique Name. Regardons les titres sur l’ensemble du jeu de données (full) :
full['Titre'] = full.Name.map(lambda x : x.split(",")[1].split(".")[0]) full['NomFamille'] = full.Name.map(lambda x : x.split(",")[0]) titre = pd.DataFrame(full['Titre']) full['Titre'].value_counts() # affiche tous les titres possible
Voici les possibilités que nous allons traiter :
Mr 757
Miss 260
Mrs 197
Master 61
Dr 8
Rev 8
Col 4
Ms 2
Mlle 2
Major 2
Mme 1
Lady 1
Capt 1
Don 1
Jonkheer 1
Sir 1
the Countess 1
Dona 1
Name: Titre, dtype: int64
Pour les titre nous allons créer des catégories que nous encoderons (one-hot) ensuite. Normalement la consigne les femmes et les enfants a dû être respectée, mais a mon avis les personnes de rangs ont aussi été privilégiées. Créons donc 3 catégories : Femme et enfant, VIP et les autres :
X = test X['Rang'] = 0 X['Titre'] = X.Name.map(lambda x : x.split(",")[1].split(".")[0]) vip = ['Don','Sir', 'Major', 'Col', 'Jonkheer', 'Dr', 'Rev'] femmeenfant = ['Miss', 'Mrs', 'Lady', 'Mlle', 'the Countess', 'Ms', 'Mme', 'Dona', 'Master'] for idx, titre in enumerate(X['Titre']): if (titre.strip() in femmeenfant) : X.loc[idx, 'Rang'] = 'FE' elif (titre.strip() in vip) : X.loc[idx, 'Rang'] = 'VIP' else : X.loc[idx, 'Rang'] = 'Autres' X['Rang'].value_counts()
L’age
Nous allons créer là aussi plusieurs catégories d’age selon la variable Age:
- Les bébés : de 0 a 3 ans
- Les enfants: de 3 à 15 ans
- Les adultes de 15 à 60 ans
- Les « vieux » de plus de 60 ans
age = X['Age'].fillna(X['Age'].mean()) catAge = [] for i in range(X.shape[0]) : if age[i] <= 3: catAge.append("bebe") elif age[i] > 3 and age[i] >= 15: catAge.append("enfant") elif age[i] > 15 and age[i] <= 60: catAge.append("adulte") else: catAge.append("vieux") print(pd.DataFrame(catAge, columns = ['catAge'])['catAge'].value_counts()) cat = pd.get_dummies(pd.DataFrame(catAge, columns = ['catAge']), prefix='catAge') cat.head(3)
Jetons un coup d'oeil au résultat :
adulte 373
enfant 21
vieux 14
bebe 10
Name: catAge, dtype: int64
Fonction globale de préparation/modèle
Regroupons maintenant tous ces éléments dans une fonction de préparation :
def dataprep(data): # Sexe sexe = pd.get_dummies(data['Sex'], prefix='sex') # Cabine, récupération du pont (on remplace le pont T proche du pont A) cabin = pd.get_dummies(data['Cabin'].fillna('X').str[0].replace('T', 'A'), prefix='Cabin') # Age et catégories d'age age = data['Age'].fillna(data['Age'].mean()) catAge = [] for i in range(data.shape[0]) : if age[i] > 3: catAge.append("bebe") elif age[i] >= 3 and age[i] < 15: catAge.append("enfant") elif age[i] >= 15 and age[i] < 60: catAge.append("adulte") else: catAge.append("vieux") catage = pd.get_dummies(pd.DataFrame(catAge, columns = ['catAge']), prefix='catAge') # Titre et Rang data['Titre'] = data.Name.map(lambda x : x.split(",")[1].split(".")[0]).fillna('X') data['Rang'] = 0 vip = ['Don','Sir', 'Major', 'Col', 'Jonkheer', 'Dr'] femmeenfant = ['Miss', 'Mrs', 'Lady', 'Mlle', 'the Countess', 'Ms', 'Mme', 'Dona', 'Master'] for idx, titre in enumerate(data['Titre']): if (titre.strip() in femmeenfant) : data.loc[idx, 'Rang'] = 'FE' elif (titre.strip() in vip) : data.loc[idx, 'Rang'] = 'VIP' else : data.loc[idx, 'Rang'] = 'Autres' rg = pd.get_dummies(data['Rang'], prefix='Rang') # Embarquement emb = pd.get_dummies(data['Embarked'], prefix='emb') # Prix unitaire - Ticket, Prépartion d'un DF (TicketCounts) contenant les ticket avec leur nb d'occurence TicketCounts = pd.DataFrame(data['Ticket'].value_counts()) TicketCounts['TicketCount'] = TicketCounts['Ticket'] # renomme la colonne Ticket TicketCounts['Ticket'] = TicketCounts.index # rajoute une colonne Ticket pour le merge (jointure) # reporte le résultat dans le dataframe test (jointure des datasets) fin = pd.merge(data, TicketCounts, how='left', on='Ticket') fin['PrixUnitaire'] = fin['Fare'] / fin['TicketCount'].fillna(1) prxunit = pd.DataFrame(fin['PrixUnitaire']) # Prix moyen 3eme classe (pour le passager de 3eme qui n'a pas de prix) ... on aurait pu faire une fonction ici ;-) prx3eme = data.loc[data['Pclass'] == 3]['Fare'].mean() prxunit = prxunit['PrixUnitaire'].fillna(prx3eme) # Classe pc = pd.DataFrame(MinMaxScaler().fit_transform(data[['Pclass']]), columns = ['Classe']) dp = data[['SibSp', 'Parch', 'Name']].join(pc).join(sexe).join(emb).join(prxunit).join(cabin).join(age).join(catage).join(rg) addColumnFamilyName(dp) del dp['Name'] return dp
Entrainons le modèle
Xtrain = dataprep(train) Xtest = dataprep(test) y = train.Survived clf = LinearSVC(random_state=4) clf.fit(Xtrain, y) p_tr = clf.predict(Xtrain) print ("Score Train : ", round(clf.score(Xtrain, y) *100,4), " %")
Nous obtenons ainsi un très (trop !?) beau 98% (sur les données d'entrainement). Sur les données de test nous aurons un raisonnable 76,5% !
Ingénieur en informatique avec plus de 20 ans d’expérience dans la gestion et l’utilisation de données, Benoit CAYLA a mis son expertise au profit de projets très variés tels que l’intégration, la gouvernance, l’analyse, l’IA, la mise en place de MDM ou de solution PIM pour le compte de diverses entreprises spécialisées dans la donnée (dont IBM, Informatica et Tableau). Ces riches expériences l’ont naturellement conduit à intervenir dans des projets de plus grande envergure autour de la gestion et de la valorisation des données, et ce principalement dans des secteurs d’activités tels que l’industrie, la grande distribution, l’assurance et la finance. Également, passionné d’IA (Machine Learning, NLP et Deep Learning), l’auteur a rejoint Blue Prism en 2019 et travaille aujourd’hui en tant qu’expert data/IA et processus. Son sens pédagogique ainsi que son expertise l’ont aussi amené à animer un blog en français (datacorner.fr) ayant pour but de montrer comment comprendre, analyser et utiliser ses données le plus simplement possible.
Bonsoir;
Ah vraiment merci de ce tuto ,très riche et instructif.