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.

Engineering

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_clienttimestamp_interactionmontant_dhslieu_interactiontype_interaction
sgjuL0CYJf2019-01-01T09:01:25.0001500GAB CASA 118Cretrait_gab
17AbPYC2M72019-02-01T01:08:23.0001207.10MAROCpaiement_tpe
sgjuL0CYJf2019-01-03T07:01:21.000200GAB RABAT 129Bretrait_gab
sgjuL0CYJf2019-04-01T01:06:19.000NAGAB AGADIR 10consultation_solde
17AbPYC2M72019-01-05T05:01:17.000500GAB CASA 118Cretrait_gab
17AbPYC2M72019-06-01T01:04:15.000299INTERNATIONALpaiement_tpe
17AbPYC2M72019-01-07T03:01:13.000NAMAROCpin_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 :

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_clienttimestamp_interactionmontant_dhslieu_interactiontype_interaction
sgjuL0CYJf2019-01-01T09:01:25.0001500GAB CASA 118Cretrait_gab
17AbPYC2M72019-02-01T01:08:23.0001207.10MAROCpaiement_tpe
sgjuL0CYJf2019-01-03T07:01:21.000200GAB RABAT 129Bretrait_gab
sgjuL0CYJf2019-04-01T01:06:19.0000GAB AGADIR 10consultation_solde
17AbPYC2M72019-01-05T05:01:17.000500GAB CASA 118Cretrait_gab
17AbPYC2M72019-06-01T01:04:15.000299INTERNATIONALpaiement_tpe
17AbPYC2M72019-01-07T03:01:13.0000MAROCpin_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_interactionlieu_interaction_categorie
GAB CASA 118CGAB MAROC
MAROCMAROC
GAB RABAT 129BGAB MAROC
GAB AGADIR 10GAB MAROC
GAB CASA 118CGAB MAROC
INTERNATIONALINTERNATIONAL
MAROCMAROC

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_interactionlieu_interaction_categorie
GAB CASA 118CGAB CASA 118C
MAROCMAROC
GAB RABAT 129BAUTRES
GAB AGADIR 10AUTRES
GAB CASA 118CGAB CASA 118C
INTERNATIONALAUTRES
MAROCMAROC

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_clienttimestamp_interactionmontant_dhslieu_interactiontype_interaction
sgjuL0CYJf2019-01-01T09:01:25.0001500GAB MAROCretrait_gab
17AbPYC2M72019-02-01T01:08:23.0001207.10MAROCpaiement_tpe
sgjuL0CYJf2019-01-03T07:01:21.000200GAB MAROCretrait_gab
sgjuL0CYJf2019-04-01T01:06:19.0000GAB MAROCconsultation_solde
17AbPYC2M72019-01-05T05:01:17.000500GAB MAROCretrait_gab
17AbPYC2M72019-06-01T01:04:15.000299INTERNATIONALpaiement_tpe
17AbPYC2M72019-01-07T03:01:13.0000MAROCpin_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 :

Le but est d’obtenir le dataset avec les colonnes suivantes : (nb = nombre)

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_clientabcdefghijklmno
sgjuL0CYJf002010000000000
17AbPYC2M7010000001000100

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 {% highlight scala %} def getCheckPivotFeatures(dateCol:String, baseDtCol:String, grpByCol:String, pivotCol:String, lib:String, milestones:Seq[Int]) = { df // khouza3bila (خزعبلة) means a “random/funny thing” in moroccan arabic .conditionnalTransform(milestones.length == 1)(.withColumn(“khouza3bila”, lit(1))) .checkDateMonthMilestone(dateCol, baseDtCol, milestones:*) .groupBy(grpByCol) .pivot(pivotCol) .sum() .conditionnalTransform(milestones.length == 1)(.dropColsContaining(“khouza3bila”)) .renameColumnsWithAggHeuristic(lib) } {% endhighlight %} Avec les arguments : {% highlight scala %} getCheckPivotFeatures(col(“timestamp_interaction”), lit(“2019-07-01”), col(“id_client”), concat_ws("", col(“lieu_interaction”), col(“type_interaction”)), lit(“nb_interaction”), Seq(1, 3, 6) ) {% endhighlight %}

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 :

Les variables obtenus en sortie suivent le même pattern que précédemment :

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_clientabcdefghijklmno
sgjuL0CYJf001700000000000000
17AbPYC2M705000000001207.1000029900

Le code suivant toujours du même repository permet de générer ce dataset. {% highlight scala %} def getValuePivotFeatures(dateCol:String, baseDtCol:String, valCol:String, grpByCol:String, pivotCol:String, lib:String, milestones:Seq[Int]) = { df .conditionnalTransform(milestones.length == 1)(.withColumn(“khouza3bila”, lit(1))) .valDateMonthMilestone(dateCol, baseDtCol, valCol, “tmp”, milestones:*) .drop(valCol) .groupBy(grpByCol) .pivot(pivotCol) .sum() .conditionnalTransform(milestones.length == 1)(_.dropColsContaining(“khouza3bila”)) .renameColumnsWithAggHeuristic(lib) } {% endhighlight %}

Avec les arguments {% highlight scala %} getValuePivotFeatures(col(“timestamp_interaction”), lit(“2019-07-01”), col(“montant_dhs”), col(“id_client”), concat_ws(”_”, col(“lieu_interaction”), col(“type_interaction”)), lit(“nb_interaction”), Seq(1, 3, 6) ) {% endhighlight %}

Il est bien évidemment possible de générer les deux datasets en même temps, grâce à la syntaxe pratique de Spark/Scala : {% highlight scala %} def getCheckValuePivotFeatures(dateCol:String, baseDtCol:String, valCol:String, grpByCol:String, pivotCol:String, lib:String, milestones:Seq[Int]) = { df .conditionnalTransform(milestones.length == 1)(.withColumn(“khouza3bila”, lit(1))) .checkDateMonthMilestone(dateCol, baseDtCol, milestones:) .valDateMonthMilestone(dateCol, baseDtCol, valCol, “tmp”, milestones:_) .drop(valCol) .groupBy(grpByCol) .pivot(pivotCol) .sum() .conditionnalTransform(milestones.length == 1)(_.dropColsContaining(“khouza3bila”)) .renameColumnsWithAggHeuristic(lib) } {% endhighlight %}

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:

Credits

https://unsplash.com