Feature engineering avec Spark
Nous discutions dans l’article sur l’alimentation du Datalake en donnée de la brique élémentaire sur laquelle nous construisons nos modélisations : l’event. Cette brique décrit une interaction unitaire dans un process et a la particularité d’être timestampée.
Un exemple monétique
Le plus souvent en banque, l’event caractérise un client qui décide d’interagir avec sa banque sur un de ses canaux. Prenons typiquement le cas de la monétique (car elle fournit une donnée riche en information) et quelques données inventées :
id_client | timestamp_interaction | montant_dhs | lieu_interaction | type_interaction |
---|---|---|---|---|
sgjuL0CYJf | 2019-01-01T09:01:25.000 | 1500 | GAB CASA 118C | retrait_gab |
17AbPYC2M7 | 2019-02-01T01:08:23.000 | 1207.10 | MAROC | paiement_tpe |
sgjuL0CYJf | 2019-01-03T07:01:21.000 | 200 | GAB RABAT 129B | retrait_gab |
sgjuL0CYJf | 2019-04-01T01:06:19.000 | NA | GAB AGADIR 10 | consultation_solde |
17AbPYC2M7 | 2019-01-05T05:01:17.000 | 500 | GAB CASA 118C | retrait_gab |
17AbPYC2M7 | 2019-06-01T01:04:15.000 | 299 | INTERNATIONAL | paiement_tpe |
17AbPYC2M7 | 2019-01-07T03:01:13.000 | NA | MAROC | pin_errone |
Une table plate avec des NA
On peut constater ici que nous avons une table plate qui contient toute l’information jointe sur les interactions monétiques. Cela donne lieu à la présence de NA (ou null) dans certaines colonnes :
- Dans les montants lorsque l’interaction ne fait pas intervenir d’argent (saisie de code pin erroné)
- Dans des colonnes catégoriques, comme par exemple une éventuelle colonne categorie_marchand lorsqu’il n’y a tout simplement pas de marchand dans la transaction (retrait gab par exemple)
Une première uniformisation de la table
Constatons tout d’abord qu’il faut faire quelque chose des NA afin de pouvoir réaliser des agrégations qui ont du sens. Une solution simple est de remplacer les NA de colonnes numérique par 0 (l’information que c’est originellement un NA est contenu dans la colonne type_interaction).
De plus, il faut aussi enlever les NA des colonnes de catégories s’il y en avait. Pour cela, j’aime en général rajouter une catégorie “non_concerne” préfixée du nom de la colonne.
Nous obtenons ainsi le dataset suivant :
id_client | timestamp_interaction | montant_dhs | lieu_interaction | type_interaction |
---|---|---|---|---|
sgjuL0CYJf | 2019-01-01T09:01:25.000 | 1500 | GAB CASA 118C | retrait_gab |
17AbPYC2M7 | 2019-02-01T01:08:23.000 | 1207.10 | MAROC | paiement_tpe |
sgjuL0CYJf | 2019-01-03T07:01:21.000 | 200 | GAB RABAT 129B | retrait_gab |
sgjuL0CYJf | 2019-04-01T01:06:19.000 | 0 | GAB AGADIR 10 | consultation_solde |
17AbPYC2M7 | 2019-01-05T05:01:17.000 | 500 | GAB CASA 118C | retrait_gab |
17AbPYC2M7 | 2019-06-01T01:04:15.000 | 299 | INTERNATIONAL | paiement_tpe |
17AbPYC2M7 | 2019-01-07T03:01:13.000 | 0 | MAROC | pin_errone |
Gérer harmonieusement la cardinalité
Sur la base du dataset monétique précédent, on peut observer que la colonne lieu_interaction offre une cardinalité importante, probablement plus que nous ne souhaitons avoir s’il fallait convertir chaque label en une variable (on pourrait aboutir à plusieurs milliers de variables s’il fallait créer une variable par exemple pour chaque GAB d’un grand réseau bancaire).
La première alternative est de réduire l’information en catégorisant :
lieu_interaction | lieu_interaction_categorie |
---|---|
GAB CASA 118C | GAB MAROC |
MAROC | MAROC |
GAB RABAT 129B | GAB MAROC |
GAB AGADIR 10 | GAB MAROC |
GAB CASA 118C | GAB MAROC |
INTERNATIONAL | INTERNATIONAL |
MAROC | MAROC |
Une deuxième alternative lorsque la cardinalité est importante et qu’un traitement automatique peut s’avérer nécessaire, c’est d’utiliser des heuristiques pour regrouper les labels. Dans ce cas précis, on va considérer que le label GAB CASA 118C peut rester car il est suffisamment présent (supérieur ou égal à 2 fois si on part d’un nombre fixe, mais on peut aussi partir d’un ratio de présence).
lieu_interaction | lieu_interaction_categorie |
---|---|
GAB CASA 118C | GAB CASA 118C |
MAROC | MAROC |
GAB RABAT 129B | AUTRES |
GAB AGADIR 10 | AUTRES |
GAB CASA 118C | GAB CASA 118C |
INTERNATIONAL | AUTRES |
MAROC | MAROC |
En général, je préfère la première option car elle me permet de mieux contrôler la granularité de l’information présente dans le dataset même si elle demande plus de travail manuel de regroupement.
Un dataset propre prêt au feature engineering
Nous partirons de ce dataset :
id_client | timestamp_interaction | montant_dhs | lieu_interaction | type_interaction |
---|---|---|---|---|
sgjuL0CYJf | 2019-01-01T09:01:25.000 | 1500 | GAB MAROC | retrait_gab |
17AbPYC2M7 | 2019-02-01T01:08:23.000 | 1207.10 | MAROC | paiement_tpe |
sgjuL0CYJf | 2019-01-03T07:01:21.000 | 200 | GAB MAROC | retrait_gab |
sgjuL0CYJf | 2019-04-01T01:06:19.000 | 0 | GAB MAROC | consultation_solde |
17AbPYC2M7 | 2019-01-05T05:01:17.000 | 500 | GAB MAROC | retrait_gab |
17AbPYC2M7 | 2019-06-01T01:04:15.000 | 299 | INTERNATIONAL | paiement_tpe |
17AbPYC2M7 | 2019-01-07T03:01:13.000 | 0 | MAROC | pin_errone |
Qui a fait quoi ?
Un premier besoin est de représenter pour chaque client les interactions que ce client a fait avec la banque de manière a créer des variables numériques utilisables. Nous avons besoin pour cela de :
- Une colonne indiquant le client car cela sera notre clé d’agrégation : ici id_client
- Une colonne indiquant pour chaque client la référence temporelle du calcul des features. En général, on calcul des features par rapport à un moment précis de la vie du client (typiquement 3 mois avant la souscription à un crédit conso), mais pour rester simple, partons du principe que tous les clients ont le même temps t, le 1er juillet 2019 qu’on va noter Literal(“2019-07-01”)
- Une colonne indiquant le type d’interaction. Utilisons la concaténation de la colonne lieu_interaction et la colonne type_interaction. Ce sera notre colonne pivot.
- Des milestones, on prendra typiquement 1, 3 et 6 mois.
Le but est d’obtenir le dataset avec les colonnes suivantes : (nb = nombre)
- id_client
- nb_interaction_gab_maroc_retrait_gab_1_mois notée a
- nb_interaction_gab_maroc_retrait_gab_3_mois notée b
- nb_interaction_gab_maroc_retrait_gab_6_mois notée c
- nb_interaction_gab_consultation_solde_1_mois notée d
- nb_interaction_gab_consultation_solde_3_mois notée e
- nb_interaction_gab_consultation_solde_6_mois notée f
- nb_interaction_maroc_paiement_tpe_1_mois notée g
- nb_interaction_maroc_paiement_tpe_3_mois notée h
- nb_interaction_maroc_paiement_tpe_6_mois notée i
- nb_interaction_maroc_pin_errone_1_mois notée j
- nb_interaction_maroc_pin_errone_3_mois notée k
- nb_interaction_maroc_pin_errone_6_mois notée l
- nb_interaction_international_paiement_tpe_1_mois notée m
- nb_interaction_international_paiement_tpe_3_mois notée n
- nb_interaction_international_paiement_tpe_6_mois notée o
Comme vous pouvez le constater, on peut vite se retrouver avec beaucoup de variable d’où l’intérêt de faire un travail méticuleux de réduction de dimension (donc de regroupement dans notre cas) le plus en amont possible. La feature selection est en effet tout un art.
Raccourcissons le nom des nouvelles variables. Voici le résultat que nous devrions obtenir
id_client | a | b | c | d | e | f | g | h | i | j | k | l | m | n | o |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
sgjuL0CYJf | 0 | 0 | 2 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
17AbPYC2M7 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 |
Le code spark pour effectuer la transformation suivante est dans le repository : https://github.com/AshtonIzmev/spark-feature-engineering-toolkit
C’est la fonction suivante qui est utilisée
Avec les arguments :
Le dataset obtenu peut être alors un input d’algorithme de machine learning.
Qui a fait combien ?
Une autre façon de voir le problème est de mesurer non pas le nombre d’interactions mais le poids de chacun, à travers par exemple le montant qui est un bon proxy de l’importance de l’interaction. Nous allons donc sommer les montants par type d’interaction au lieu de simplement les compter.
Il nous faut donc une colonne supplémentaire :
- La valCol qui sera ici montant_dhs
Les variables obtenus en sortie suivent le même pattern que précédemment :
- montant_interaction_gab_maroc_retrait_gab_1_mois notée a
- montant_interaction_gab_maroc_retrait_gab_3_mois notée b
- montant_interaction_gab_maroc_retrait_gab_6_mois notée c
- montant_interaction_gab_consultation_solde_1_mois notée d
- montant_interaction_gab_consultation_solde_3_mois notée e
- montant_interaction_gab_consultation_solde_6_mois notée f
- montant_interaction_maroc_paiement_tpe_1_mois notée g
- montant_interaction_maroc_paiement_tpe_3_mois notée h
- montant_interaction_maroc_paiement_tpe_6_mois notée i
- montant_interaction_maroc_pin_errone_1_mois notée j
- montant_interaction_maroc_pin_errone_3_mois notée k
- montant_interaction_maroc_pin_errone_6_mois notée l
- montant_interaction_international_paiement_tpe_1_mois notée m
- montant_interaction_international_paiement_tpe_3_mois notée n
- montant_interaction_international_paiement_tpe_6_mois notée o
Ici, il faut faire attention que le résultat 0 peut avoir deux significations distinctes à cause de notre choix de politique de missing values. Il peut vouloir dire qu’une interaction a été réalisée avec 0 dhs, ou bien qu’il n’y a pas eu d’interaction de ce type. Ces deux choses là se retrouvent projetées sur le même résultat.
Voici le résultat que nous devrions obtenir
id_client | a | b | c | d | e | f | g | h | i | j | k | l | m | n | o |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
sgjuL0CYJf | 0 | 0 | 1700 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
17AbPYC2M7 | 0 | 500 | 0 | 0 | 0 | 0 | 0 | 0 | 1207.10 | 0 | 0 | 0 | 299 | 0 | 0 |
Le code suivant toujours du même repository permet de générer ce dataset.
Avec les arguments
Il est bien évidemment possible de générer les deux datasets en même temps, grâce à la syntaxe pratique de Spark/Scala :
Comment a évolué le combien a fait quoi ?
Si on veut calculer des ratios d’évolution de nombre d’interactions ou de montant, là encore cela peut donner lieu à des variables riches et intéressantes. Pour plus d’informations, je vous invite à aller regarder le code suivant github.com/AshtonIzmev/spark-feature-engineering-toolkit/…/DataFrameEngineeringImplicits.scala#L140
Les variables obtenus en sortie suivent là encore un pattern naturel:
- evolution_montant_interaction_gab_maroc_retrait_gab_entre_0_3_mois_et_3_6_mois
- evolution_montant_interaction_gab_maroc_retrait_gab_entre_0_3_mois_et_6_9_mois
- evolution_montant_interaction_gab_maroc_retrait_gab_entre_0_6_mois_et_6_12_mois
Credits
- Photo by Sergey Zolkin on Unsplash