Environnement - Packages¶

In [176]:
# activer l'environnement
using Pkg
Pkg.activate("env_julia_dtc")
  Activating project at `c:\Users\ricco\Desktop\demo\env_julia_dtc`
In [177]:
# liste des packages installés
Pkg.status()
Status `C:\Users\ricco\Desktop\demo\env_julia_dtc\Project.toml`
  [324d7699] CategoricalArrays v1.1.0
  [a93c6f00] DataFrames v1.8.2
  [7806a523] DecisionTree v0.12.4
  [f526b714] GraphViz v0.2.0
  [7073ff75] IJulia v1.34.4
  [add582a8] MLJ v0.23.2
  [c6f25543] MLJDecisionTreeInterface v0.4.4
  [23777cdb] MLJTransforms v0.1.5
  [30f210dd] ScientificTypesBase v3.1.0
  [2913bbd2] StatsBase v0.34.10
  [fdbf4ff8] XLSX v0.11.4

Importation, préparation des données¶

Chargement - Inspection¶

In [178]:
# packages
import DataFrames as DFR
import XLSX

# lecture des données
df = DFR.DataFrame(XLSX.readtable("./data_marketing.xlsx"))

# premières lignes
println(DFR.size(df))
(10000, 16)
In [179]:
# description
println(DFR.describe(df))
16×7 DataFrame
 Row │ variable           mean      min               median   max           nmissing  eltype   
     │ Symbol             Union…    Any               Union…   Any           Int64     DataType 
─────┼──────────────────────────────────────────────────────────────────────────────────────────
   1 │ age                43.5394   18                43.0     69                   0  Int64
   2 │ revenu_annuel      50134.8   20000.0           49957.2  1.02936e5            0  Float64
   3 │ score_credit       679.969   500.0             680.325  850.0                0  Float64
   4 │ anciennete_client  9.4085    0                 9.0      19                   0  Int64
   5 │ panier_montant     1509.94   200.0             1494.4   4482.27              0  Float64
   6 │ sensibilite_promo  0.500272  0.0               0.49955  0.9999               0  Float64
   7 │ frequence_achat    7.504     1                 8.0      14                   0  Int64
   8 │ taux_endettement   35.1021   10.01             35.175   59.99                0  Float64
   9 │ region_risque      0.300578  0.1001            0.30215  0.4999               0  Float64
  10 │ type_produit                 automobile                 electronique         0  String
  11 │ canal_achat                  magasin                    web                  0  String
  12 │ nb_cartes_credit   2.1078    1                 2.0      6                    0  Int64
  13 │ incident_paiement  0.3168    0                 0.0      1                    0  Int64
  14 │ epargne            35066.0   4107.22           32519.9  100000.0             0  Float64
  15 │ stabilite_emploi   0.697161  0.0               0.7042   1.0                  0  Float64
  16 │ financement                  credit_classique           paiement_3x          0  String
In [180]:
# schema : types au sens de MLJ
import MLJ
MLJ.schema(df)
┌───────────────────┬────────────┬─────────┐
│ names             │ scitypes   │ types   │
├───────────────────┼────────────┼─────────┤
│ age               │ Count      │ Int64   │
│ revenu_annuel     │ Continuous │ Float64 │
│ score_credit      │ Continuous │ Float64 │
│ anciennete_client │ Count      │ Int64   │
│ panier_montant    │ Continuous │ Float64 │
│ sensibilite_promo │ Continuous │ Float64 │
│ frequence_achat   │ Count      │ Int64   │
│ taux_endettement  │ Continuous │ Float64 │
│ region_risque     │ Continuous │ Float64 │
│ type_produit      │ Textual    │ String  │
│ canal_achat       │ Textual    │ String  │
│ nb_cartes_credit  │ Count      │ Int64   │
│ incident_paiement │ Count      │ Int64   │
│ epargne           │ Continuous │ Float64 │
│ stabilite_emploi  │ Continuous │ Float64 │
│ financement       │ Textual    │ String  │
└───────────────────┴────────────┴─────────┘

Préparation des structures y vs. X¶

In [181]:
# isoler y et X dans des structures distinctes
y, X = MLJ.unpack(df,==(:financement))

# dimensions
println("Dim. de y = $(DFR.size(y))")
println("Dim. de X = $(DFR.size(X))")
Dim. de y = (10000,)
Dim. de X = (10000, 15)
In [182]:
# effectifs par classe
import StatsBase
StatsBase.countmap(y)
Dict{String, Int64} with 3 entries:
  "paiement_3x"      => 5920
  "l_o_a"            => 2613
  "credit_classique" => 1467

Typage des variables pour la modélisation¶

In [183]:
# typage de la variable cible
import CategoricalArrays as CA
y = CA.categorical(y)

# vérification des modalités
CA.levels(y)
3-element CategoricalArrays.CategoricalArray{String,1,UInt32}:
 "credit_classique"
 "l_o_a"
 "paiement_3x"
In [184]:
# typer correctement les X à modifier
# en les énumérant individuellement
# utilisation du package ScientificTypesBase
import ScientificTypesBase as STB
MLJ.coerce!(X,
            :age => STB.Continuous,
            :anciennete_client => STB.Continuous,
            :frequence_achat => STB.Continuous,
            :type_produit => STB.Multiclass,
            :canal_achat => STB.Multiclass,
            :nb_cartes_credit => STB.Continuous,
            :incident_paiement => STB.Continuous)

# schéma
MLJ.schema(X)
┌───────────────────┬───────────────┬──────────────────────────────────┐
│ names             │ scitypes      │ types                            │
├───────────────────┼───────────────┼──────────────────────────────────┤
│ age               │ Continuous    │ Float64                          │
│ revenu_annuel     │ Continuous    │ Float64                          │
│ score_credit      │ Continuous    │ Float64                          │
│ anciennete_client │ Continuous    │ Float64                          │
│ panier_montant    │ Continuous    │ Float64                          │
│ sensibilite_promo │ Continuous    │ Float64                          │
│ frequence_achat   │ Continuous    │ Float64                          │
│ taux_endettement  │ Continuous    │ Float64                          │
│ region_risque     │ Continuous    │ Float64                          │
│ type_produit      │ Multiclass{3} │ CategoricalValue{String, UInt32} │
│ canal_achat       │ Multiclass{3} │ CategoricalValue{String, UInt32} │
│ nb_cartes_credit  │ Continuous    │ Float64                          │
│ incident_paiement │ Continuous    │ Float64                          │
│ epargne           │ Continuous    │ Float64                          │
│ stabilite_emploi  │ Continuous    │ Float64                          │
└───────────────────┴───────────────┴──────────────────────────────────┘

Codage des descripteurs catégoriels¶

In [185]:
# one-hot encoder pour les descripteurscatégoriels
OneHotEncoder = @MLJ.load OneHotEncoder pkg=MLJTransforms
import MLJTransforms ✔
┌ Info: For silent loading, specify `verbosity=0`. 
└ @ Main C:\Users\ricco\.julia\packages\MLJModels\9LbNu\src\loading.jl:159
MLJTransforms.OneHotEncoder
In [186]:
# encodage (par défaut, pas de modalité exclue)
# instanciation et préparation
ohe = MLJ.machine(OneHotEncoder(),X)

# entraînement
MLJ.fit!(ohe)

# transformation
XPrim = MLJ.transform(ohe,X)
┌ Info: Training machine(OneHotEncoder(features = Symbol[], …), …).
└ @ MLJBase C:\Users\ricco\.julia\packages\MLJBase\krfwA\src\machines.jl:499
┌ Info: Spawning 3 sub-features to one-hot encode feature :type_produit.
└ @ MLJTransforms C:\Users\ricco\.julia\packages\MLJTransforms\rATN1\src\transformers\other_transformers\one_hot_encoder.jl:67
┌ Info: Spawning 3 sub-features to one-hot encode feature :canal_achat.
└ @ MLJTransforms C:\Users\ricco\.julia\packages\MLJTransforms\rATN1\src\transformers\other_transformers\one_hot_encoder.jl:67
10000×19 DataFrame
9975 rows omitted
Rowagerevenu_annuelscore_creditanciennete_clientpanier_montantsensibilite_promofrequence_achattaux_endettementregion_risquetype_produit__automobiletype_produit__electromenagertype_produit__electroniquecanal_achat__magasincanal_achat__mobilecanal_achat__webnb_cartes_creditincident_paiementepargnestabilite_emploi
Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64
156.037214.2733.3418.0674.910.60483.017.250.49080.00.01.00.01.00.02.00.022395.30.9081
269.057130.5701.213.02413.490.32716.029.220.10430.00.01.01.00.00.03.00.011986.10.7551
346.059486.8715.9816.02048.70.817412.049.620.36561.00.00.00.01.00.02.00.024541.20.6869
432.042887.3756.314.01494.530.13541.031.080.41721.00.00.00.01.00.02.01.032215.90.7385
560.038423.4671.05.0909.720.881712.014.990.35560.00.01.00.01.00.01.01.021098.70.8862
625.074142.8613.0718.01034.890.974712.014.80.20920.01.00.00.01.00.03.01.076838.10.7647
738.053412.4758.0719.02958.340.96774.021.850.42911.00.00.00.01.00.03.01.046627.20.6407
856.059539.5648.215.01642.750.14779.038.240.46530.01.00.00.01.00.03.00.021234.00.6609
936.037668.9638.585.02563.890.54962.051.510.37340.00.01.00.00.01.01.00.042762.70.8463
1040.031710.7572.233.01693.250.58492.036.690.30440.00.01.00.01.00.01.00.013997.10.4952
1128.041458.2714.280.01137.840.19492.047.30.24650.01.00.00.00.01.02.00.010809.50.8906
1228.048139.7582.7118.01967.270.546810.042.280.2320.00.01.00.00.01.02.01.033027.90.3127
1341.034036.7634.9810.03226.270.646510.036.20.38930.01.00.00.01.00.02.01.021709.40.7146
⋮⋮⋮⋮⋮⋮⋮⋮⋮⋮⋮⋮⋮⋮⋮⋮⋮⋮⋮⋮
998941.054902.5609.6619.01361.180.52813.041.170.32850.00.01.00.01.00.03.00.036557.00.6157
999023.062980.4676.7617.01133.090.414710.049.290.40740.00.01.00.01.00.02.01.045079.11.0
999144.051340.4580.8712.0943.650.919914.029.190.4240.01.00.01.00.00.02.00.019148.50.8463
999235.064768.6666.047.01626.230.40528.054.180.23281.00.00.00.01.00.03.00.026832.00.8973
999341.059211.6653.5213.0528.710.59610.046.780.34931.00.00.01.00.00.02.00.059188.20.7929
999454.044602.4671.942.02243.380.69198.040.30.27431.00.00.00.01.00.02.00.039378.80.7103
999541.063437.8646.443.0634.770.01911.019.920.13920.01.00.00.01.00.03.00.033575.50.891
999655.051372.2589.5410.0486.330.590914.043.510.35940.00.01.00.00.01.02.01.051140.10.6452
999751.048408.0658.026.01298.160.5188.051.690.3350.01.00.01.00.00.03.00.012281.50.3953
999857.042857.0735.854.01202.00.5944.046.50.1410.00.01.01.00.00.01.01.011600.20.6747
999964.052490.3755.691.01784.450.07577.038.740.12830.01.00.01.00.00.02.00.015144.00.6815
1000032.064946.9826.365.01567.870.055610.045.730.19990.01.00.01.00.00.04.01.058678.11.0
In [187]:
# description
DFR.describe(XPrim)
19×7 DataFrame
Rowvariablemeanminmedianmaxnmissingeltype
SymbolFloat64Float64Float64Float64Int64DataType
1age43.539418.043.069.00Float64
2revenu_annuel50134.820000.049957.21.02936e50Float64
3score_credit679.969500.0680.325850.00Float64
4anciennete_client9.40850.09.019.00Float64
5panier_montant1509.94200.01494.44482.270Float64
6sensibilite_promo0.5002720.00.499550.99990Float64
7frequence_achat7.5041.08.014.00Float64
8taux_endettement35.102110.0135.17559.990Float64
9region_risque0.3005780.10010.302150.49990Float64
10type_produit__automobile0.33070.00.01.00Float64
11type_produit__electromenager0.33290.00.01.00Float64
12type_produit__electronique0.33640.00.01.00Float64
13canal_achat__magasin0.32960.00.01.00Float64
14canal_achat__mobile0.33590.00.01.00Float64
15canal_achat__web0.33450.00.01.00Float64
16nb_cartes_credit2.10781.02.06.00Float64
17incident_paiement0.31680.00.01.00Float64
18epargne35066.04107.2232519.9100000.00Float64
19stabilite_emploi0.6971610.00.70421.00Float64

Partition TRAIN/TEST - Création des indices¶

In [188]:
# effectif total
n = DFR.nrow(df)
print("Taille de la base = $n")
Taille de la base = 10000
In [189]:
# indices pour partition en train (60%) et test (40%)
# stratify est possible parce y est catégorielle
idTrain, idTest = MLJ.partition(1:n,0.6,shuffle=true,rng=42,stratify=y)

# vérif. dim des identifiants
println("Effectifs TRAIN = $(length(idTrain)) et TEST = $(length(idTest))")
Effectifs TRAIN = 6000 et TEST = 4000

Arbres de décision avec DECISIONTREE (via MLJ)¶

Chargement de la classe de calcul¶

In [190]:
# chargement de la classe de calcul à partir du package
# https://juliaai.github.io/MLJ.jl/stable/models/DecisionTreeClassifier_DecisionTree/#DecisionTreeClassifier_DecisionTree
DecisionTreeClassifier = @MLJ.load DecisionTreeClassifier pkg=DecisionTree
import MLJDecisionTreeInterface ✔
┌ Info: For silent loading, specify `verbosity=0`. 
└ @ Main C:\Users\ricco\.julia\packages\MLJModels\9LbNu\src\loading.jl:159
MLJDecisionTreeInterface.DecisionTreeClassifier

Instanciation, préparation, entraînement¶

In [191]:
# instanciation avec paramétrage
# limiter la profondeur pour la lisibilité
# attention, mode de calcul de l'importance des variables
# rng pour calcul déterministe (ex. gestion des ex-aequos)
tree = DecisionTreeClassifier(max_depth=3,
                            feature_importance=:impurity,
                            rng=42)

# préparation de l'objet pour l'entraînement
# avec la méthode machine() de MLJ
# juste préparatoire => les structures de données sont passées en totalité
mach = MLJ.machine(tree,XPrim,y)

# lancer l'entraînement -> l'objet mach est màj directement avec "!"
# cf. le rôle de "rows" pour indiquer les individus réeelement en TRAIN
# les structures XPrim et y ont été passés préalablement avec machine()
MLJ.fit!(mach,rows=idTrain)
┌ Info: Training machine(DecisionTreeClassifier(max_depth = 3, …), …).
└ @ MLJBase C:\Users\ricco\.julia\packages\MLJBase\krfwA\src\machines.jl:499
trained Machine; caches model-specific representations of data
  model: DecisionTreeClassifier(max_depth = 3, …)
  args: 
    1:	Source @878 ⏎ ScientificTypesBase.Table{AbstractVector{ScientificTypesBase.Continuous}}
    2:	Source @363 ⏎ AbstractVector{ScientificTypesBase.Multiclass{3}}

Inspection - Fitted params¶

In [192]:
# récupération des résultats
fp = MLJ.fitted_params(mach)

# type de l'objet
typeof(fp)
@NamedTuple{tree::DecisionTree.InfoNode{Float64, UInt32}, raw_tree::DecisionTree.Root{Float64, UInt32}, encoding::Dict{UInt32, CategoricalArrays.CategoricalValue{String, UInt32}}, features::Vector{Symbol}}
In [193]:
# liste des infos
# keys() puisque named tuple
println(keys(fp))
(:tree, :raw_tree, :encoding, :features)
In [194]:
# les noms des variables
fp.features
19-element Vector{Symbol}:
 :age
 :revenu_annuel
 :score_credit
 :anciennete_client
 :panier_montant
 :sensibilite_promo
 :frequence_achat
 :taux_endettement
 :region_risque
 :type_produit__automobile
 :type_produit__electromenager
 :type_produit__electronique
 :canal_achat__magasin
 :canal_achat__mobile
 :canal_achat__web
 :nb_cartes_credit
 :incident_paiement
 :epargne
 :stabilite_emploi
In [195]:
# et les modalités de la variable cible
CA.levels(y)
3-element CategoricalArrays.CategoricalArray{String,1,UInt32}:
 "credit_classique"
 "l_o_a"
 "paiement_3x"
In [196]:
# caractéristiques de l'arbre
# avec println()
println(fp.tree)
DecisionTree.InfoNode{Float64, UInt32}(Decision Tree
Leaves: 8
Depth:  3, nchildren=2)
In [197]:
# affichage de l'abre avec le même champ
# on retire simplement le println()
fp.tree

# un affichage graphique avec GraphViz est possible
# mais demande pas mal de contorsions
# un peu comme lors des débuts de SKLEARN sous Python
incident_paiement < 0.5
├─ score_credit < 720.0
│  ├─ panier_montant < 1502.0
│  │  ├─ paiement_3x (1250/1543)
│  │  └─ l_o_a (598/1507)
│  └─ taux_endettement < 34.99
│     ├─ credit_classique (536/636)
│     └─ paiement_3x (230/441)
└─ sensibilite_promo < 0.6952
   ├─ epargne < 88670.0
   │  ├─ paiement_3x (1056/1305)
   │  └─ l_o_a (3/3)
   └─ revenu_annuel < 30300.0
      ├─ paiement_3x (50/56)
      └─ paiement_3x (369/509)
In [198]:
# ou autre approche
import DecisionTree as TD
DT.print_tree(fp.raw_tree,feature_names=fp.features)
Feature 17: "incident_paiement" < 0.5 ?
├─ Feature 3: "score_credit" < 720.0 ?
    ├─ Feature 5: "panier_montant" < 1502.0 ?
        ├─ 3 : 1250/1543
        └─ 2 : 598/1507
    └─ Feature 8: "taux_endettement" < 34.99 ?
        ├─ 1 : 536/636
        └─ 3 : 230/441
└─ Feature 6: "sensibilite_promo" < 0.6952 ?
    ├─ Feature 18: "epargne" < 88670.0 ?
        ├─ 3 : 1056/1305
        └─ 2 : 3/3
    └─ Feature 2: "revenu_annuel" < 30300.0 ?
        ├─ 3 : 50/56
        └─ 3 : 369/509

Importance des variables¶

In [199]:
# importance des variables
MLJ.feature_importances(mach)
19-element Vector{Pair{Symbol, Float64}}:
                 :score_credit => 0.25640648078933176
            :incident_paiement => 0.2531296205468259
             :taux_endettement => 0.2517768495042364
               :panier_montant => 0.23019784848167227
            :sensibilite_promo => 0.0029514050112553997
                      :epargne => 0.0029451420461343953
                :revenu_annuel => 0.0025926536205439
                          :age => 0.0
            :anciennete_client => 0.0
              :frequence_achat => 0.0
                :region_risque => 0.0
     :type_produit__automobile => 0.0
 :type_produit__electromenager => 0.0
   :type_produit__electronique => 0.0
         :canal_achat__magasin => 0.0
          :canal_achat__mobile => 0.0
             :canal_achat__web => 0.0
             :nb_cartes_credit => 0.0
             :stabilite_emploi => 0.0

Inspection - Report¶

In [200]:
# autre manière d'accéder aux résultats avec report()
rm = MLJ.report(mach)

# type de l'object
typeof(rm)
@NamedTuple{classes_seen::CategoricalArrays.CategoricalVector{String, UInt32, String, CategoricalArrays.CategoricalValue{String, UInt32}, Union{}}, print_tree::MLJDecisionTreeInterface.TreePrinter{DecisionTree.Root{Float64, UInt32}}, features::Vector{Symbol}}
In [201]:
# liste des infos
keys(rm)
(:classes_seen, :print_tree, :features)
In [202]:
# autre solution pour affichage de l'arbre
# sans limitation de profondeur (-1)
rm.print_tree(-1)
Feature 17: "incident_paiement" < 0.5 ?
├─ Feature 3: "score_credit" < 720.0 ?
    ├─ Feature 5: "panier_montant" < 1502.0 ?
        ├─ 3 : 1250/1543
        └─ 2 : 598/1507
    └─ Feature 8: "taux_endettement" < 34.99 ?
        ├─ 1 : 536/636
        └─ 3 : 230/441
└─ Feature 6: "sensibilite_promo" < 0.6952 ?
    ├─ Feature 18: "epargne" < 88670.0 ?
        ├─ 3 : 1056/1305
        └─ 2 : 3/3
    └─ Feature 2: "revenu_annuel" < 30300.0 ?
        ├─ 3 : 50/56
        └─ 3 : 369/509

Evaluation en test¶

Prédiction en test¶

In [203]:
# prédiction en test => directement classes prédites
# XPrim a déjà été passé plus haut
# reste à indiquer les indices des individus en test => idTest
pred = MLJ.predict_mode(mach,rows=idTest)

# premières valeurs
pred[1:10]
10-element CategoricalArrays.CategoricalArray{String,1,UInt32}:
 "credit_classique"
 "l_o_a"
 "paiement_3x"
 "paiement_3x"
 "l_o_a"
 "paiement_3x"
 "paiement_3x"
 "paiement_3x"
 "l_o_a"
 "credit_classique"

Matrice de confusion¶

In [204]:
# matrice de confusion
mc = MLJ.confusion_matrix(pred,y[idTest])
mc
                 ┌──────────────────────────────────────────────────┐
                 │                   Ground Truth                   │
┌────────────────┼────────────────┬────────────────┬────────────────┤
│   Predicted    │   credit_c…    │     l_o_a      │   paiement…    │
├────────────────┼────────────────┼────────────────┼────────────────┤
│   credit_c…    │      324       │       65       │       0        │
├────────────────┼────────────────┼────────────────┼────────────────┤
│     l_o_a      │      247       │      430       │      340       │
├────────────────┼────────────────┼────────────────┼────────────────┤
│   paiement…    │       16       │      550       │      2028      │
└────────────────┴────────────────┴────────────────┴────────────────┘

Indicateurs¶

In [205]:
# indicateurs de performance
acc = MLJ.accuracy(pred,y[idTest])
err = MLJ.misclassification_rate(pred,y[idTest])

# affichage
println("Accuracy = $acc ; Error Rate = $err")
Accuracy = 0.6955 ; Error Rate = 0.3045

Encapsuler les tâches dans un Pipeline¶

In [206]:
# de nouveau, on peut passer par un pipeline
pipe = OneHotEncoder() |> DecisionTreeClassifier(max_depth=3,rng=42)

# machine -> on passe X natif et non plus XPrim transformé
mach_pipe = MLJ.machine(pipe,X,y)

# entraînement
MLJ.fit!(mach_pipe,rows=idTrain)

# prédiction
pred_pipe = MLJ.predict_mode(mach_pipe,rows=idTest)

# et comparons les approches (pas-à-pas vs. pipeline)
MLJ.confusion_matrix(pred_pipe,pred)
┌ Info: Training machine(ProbabilisticPipeline(one_hot_encoder = OneHotEncoder(features = Symbol[], …), …), …).
└ @ MLJBase C:\Users\ricco\.julia\packages\MLJBase\krfwA\src\machines.jl:499
┌ Info: Training machine(:one_hot_encoder, …).
└ @ MLJBase C:\Users\ricco\.julia\packages\MLJBase\krfwA\src\machines.jl:499
┌ Info: Spawning 3 sub-features to one-hot encode feature :type_produit.
└ @ MLJTransforms C:\Users\ricco\.julia\packages\MLJTransforms\rATN1\src\transformers\other_transformers\one_hot_encoder.jl:67
┌ Info: Spawning 3 sub-features to one-hot encode feature :canal_achat.
└ @ MLJTransforms C:\Users\ricco\.julia\packages\MLJTransforms\rATN1\src\transformers\other_transformers\one_hot_encoder.jl:67
┌ Info: Training machine(:decision_tree_classifier, …).
└ @ MLJBase C:\Users\ricco\.julia\packages\MLJBase\krfwA\src\machines.jl:499
                 ┌──────────────────────────────────────────────────┐
                 │                   Ground Truth                   │
┌────────────────┼────────────────┬────────────────┬────────────────┤
│   Predicted    │   credit_c…    │     l_o_a      │   paiement…    │
├────────────────┼────────────────┼────────────────┼────────────────┤
│   credit_c…    │      389       │       0        │       0        │
├────────────────┼────────────────┼────────────────┼────────────────┤
│     l_o_a      │       0        │      1017      │       0        │
├────────────────┼────────────────┼────────────────┼────────────────┤
│   paiement…    │       0        │       0        │      2594      │
└────────────────┴────────────────┴────────────────┴────────────────┘