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_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

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)
    }

Avec les arguments :

 
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) )

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.

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)
    }

Avec les arguments

 
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) )

Il est bien évidemment possible de générer les deux datasets en même temps, grâce à la syntaxe pratique de Spark/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)
    }

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

https://unsplash.com

  • Photo by Sergey Zolkin on Unsplash