Build a Customer Churn Model

Transformez des données client en campagne de fidélisation priorisée.

Introduction

30 Second Summary

Quand un client résilie son abonnement, l’équipe de fidélisation perd une occasion de le retenir si elle intervient trop tard. Avec des milliers de clients, choisir les bonnes personnes à contacter devient vite difficile.

Dans ce projet, vous allez construire dans Google Colab un notebook reproductible qui prédit le départ des clients d’Aurore Télécom. Vous transformerez les probabilités du modèle en une liste mensuelle qui cible les 20 % de clients les plus risqués.

What You'll Build

Quand vous exécuterez le notebook, vous verrez les anomalies du jeu de données IBM, les facteurs associés au départ, les performances du modèle et un fichier CSV prêt pour une campagne de fidélisation simulée.

By the end of this project, you'll have:

  • Une carte d’identité des données qui présente les types de colonnes, les valeurs manquantes, les doublons et le journal des décisions de nettoyage.
  • Une comparaison de modèles qui révèle le piège de l’exactitude avant de mesurer la régression logistique avec la précision, le rappel et l’average precision.
  • Une campagne mensuelle simulée où vous pouvez ouvrir clients_a_contacter.csv pour présenter les 20 % de clients de test les plus risqués.
  • Secret Mission: Mesurer le churn capturé lorsque l’équipe peut contacter 10 %, 20 % ou 30 % des clients évalués.

De quoi avez-vous besoin ?

Vous avez besoin d’un compte Google, d’un navigateur et de connaissances de base en Python. Le parcours utilise Google Colab gratuitement avec pandas, scikit-learn et Matplotlib.

Before We Start

Avant toute analyse, un cadrage métier transforme le problème de fidélisation d’Aurore Télécom en une décision mesurable. Le cadre retenu priorise les 20 % de clients les plus susceptibles de partir dans un lot mensuel simulé pour une action de fidélisation mesurée par la part des départs observés capturée.

Prepare Colab and Load the Data

Customer churn results can change when a hosted runtime contains unexpected library versions. The CSV source also needs a clear identity so every later result can be traced back to the same data.

Google Colab gives you a browser-based notebook for the complete workflow. In this step, you will pin the environment before loading the IBM customer dataset.

In this step, get ready to:
  • Create the Aurore Télécom notebook with its business problem statement.
  • Pin the project libraries before importing the analysis tools.
  • Load the IBM dataset with a reproducible source URL.
Create the notebook and frame the problem

The problem statement keeps every technical decision tied to the retention campaign. It defines who receives attention before any model produces a score.

  • Visit Google Colab in your browser.
  • Complete the Google sign-in flow if Colab prompts you.
  • Create a blank notebook.
  • Rename the notebook to Aurore_Telecom_Churn using the notebook title in the upper-left corner.
  • Add a Markdown cell at the top of the notebook.
  • Write one problem statement that identifies the customer population, the churn target, the retention action, and the goal of prioritizing the 20% highest-risk customers in a simulated monthly batch.
  • Run the Markdown cell using the play icon beside it.

You should see the problem statement rendered as formatted text at the top of the notebook.

Problem statement still looks like code?

Check that the cell uses Markdown mode. A code cell tries to execute the statement as Python.

Ask for help converting the cell if needed: How do I change my problem statement cell to Markdown in Google Colab?

Pin the environment and import the tools

A pinned environment records the exact library releases behind your results. The requirements.txt file becomes the reproducible recipe for this notebook.

  • Open Colab's file area using the folder icon in the left sidebar.
  • Create a plain text file named requirements.txt in the temporary file area.
  • Record the dependency pins by replacing the blank file with the following content:
pandas==3.1.0
scikit-learn==1.9.1
matplotlib==3.11.2

What does this file control?

  • Pandas 3.1.0 supplies the table operations used throughout the notebook.
  • scikit-learn 1.9.1 supplies the splitting, preprocessing, modeling, and evaluation components.
  • Matplotlib 3.11.2 supplies the project charts.
  • Save requirements.txt.
  • Confirm that requirements.txt appears in Colab's file area.

Can't find the dependency file?

Confirm that the filename is exactly requirements.txt. Check that the editor did not append another file extension.

Ask for focused help here: Why is my requirements file missing from the Colab file area?

✔️ Awesome, I've got everything!

Your dependency manifest now records all three project pins. Keep it visible in the file area for the installation check.

ⓧ I'd like to double check the full code

pandas==3.1.0
scikit-learn==1.9.1
matplotlib==3.11.2

The installation can take a few minutes while Colab replaces any preinstalled releases. A busy cell indicator during this download is expected.

  • Add a code cell beneath the Markdown problem statement.
  • Install the dependencies from requirements.txt by running this command:
!pip install -r requirements.txt

What does this command do?

The !pip install command installs packages inside the current Colab runtime. The -r requirements.txt argument reads each exact pin from your dependency file.

  • Wait for the installation cell to finish.
  • Review the final output lines for an installation failure.

Installation cell failed?

  • Confirm that requirements.txt appears in the file area beside your notebook session.
  • Check each dependency line for the exact double equals signs shown above.
  • Reconnect the Colab runtime if the download stopped because the session disconnected.

Ask for help with the output here: Why did my pinned Colab dependency installation fail?

  • Display the installed package details by running this command in a new code cell:
!python -m pip show pandas scikit-learn matplotlib

What does this version check prove?

The command asks the active runtime to report the installed details for each project library. Its version lines reveal whether the notebook matches the dependency manifest.

  • Confirm that pandas reports 3.1.0.
  • Confirm that scikit-learn reports 1.9.1.
  • Confirm that Matplotlib reports 3.11.2.

Version details missing?

A missing package section means the installation did not complete for that library. Return to the installation output to find the first reported failure.

Ask for help comparing the outputs here: Why does my Colab package version check not match requirements.txt?

✔️ I see all three pinned versions

  • Pandas reports 3.1.0.
  • Scikit-learn reports 1.9.1.
  • Matplotlib reports 3.11.2.

Your runtime now has a reproducible foundation for the churn analysis.

ⓧ I see an older version

An older release means the runtime has not activated every requested pin. Refresh the environment before importing the libraries.

  • Return to the dependency installation cell.
  • Run the installation cell again.
  • Restart the managed runtime using Colab's runtime controls.
  • Run the package details cell again.

Still seeing an older release?

Check whether the installation output reports a dependency conflict before the success lines.

Ask for help with the conflict here: Help me activate the pinned library versions in my Colab runtime.

ⓧ A package is missing

A missing package means its installation stopped before completion. The first error in the installation output usually identifies the blocked dependency.

  • Return to the dependency installation output.
  • Locate the first line that reports a failure.
  • Reconnect the managed runtime if the session is disconnected.
  • Run the installation cell again.
  • Run the package details cell again.

Package still missing?

Confirm that the missing package has the same spelling used in requirements.txt.

Share the non-sensitive error text through this help link: Help me fix a missing package in my Colab runtime.

The imports gather every tool used across the later lifecycle stages. Loading them after installation keeps the active session aligned with the pinned releases.

  • Add a new code cell beneath the package details cell.
  • Import the project tools by pasting the following code into the new cell:
import math
from pickle import dump

import matplotlib.pyplot as plt
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.dummy import DummyClassifier
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (
    accuracy_score,
    average_precision_score,
    precision_score,
    recall_score,
)
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler

What do these imports prepare?

  • Python provides the calculation and model persistence utilities.
  • The table and plotting imports prepare data inspection plus visual output.
  • The scikit-learn imports prepare the baseline, preprocessing pipeline, predictive model, and evaluation metrics used in later steps.
  • Run the import cell.
  • Confirm that the cell completes without a traceback.

Import cell shows a traceback?

A missing-module traceback usually means the corresponding dependency did not finish installing. Compare the missing module name with the three entries in requirements.txt.

Ask for help with the traceback here: Why is my pinned Colab import cell failing?

Load and verify the IBM data

The project uses IBM's fictional telecommunications customer sample. A fixed source URL makes the origin of the loaded table visible inside the notebook.

  • Add a code cell beneath the import cell.

Before you run the next cell, what dimensions do you expect from a dataset containing several thousand customers plus their service details?

  • Load the source data and inspect its starting structure by running this cell:
DATA_URL = (
    "https://raw.githubusercontent.com/IBM/"
    "telco-customer-churn-on-icp4d/master/data/Telco-Customer-Churn.csv"
)

data = pd.read_csv(DATA_URL)
print("Dimensions:", data.shape)
print("Colonnes:", data.columns.tolist())
print("Doublons bruts:", int(data.duplicated().sum()))

What does this code do?

  • DATA_URL records the exact IBM source used by the notebook.
  • pd.read_csv() loads the remote CSV into the data DataFrame.
  • The printed dimensions confirm the number of rows plus columns.
  • The printed schema exposes the fields needed for the churn workflow.
  • The raw duplicate count records a starting value for the audit in the next step.
  • Confirm that the dimensions line shows (7043, 21).
  • Confirm that the column list includes customerID, Contract, TotalCharges, and Churn.
  • Wait for Colab to save the updated notebook to Google Drive.

You should see Dimensions: (7043, 21) above the full column list. That result confirms that the public IBM dataset is available to the notebook.

Dataset did not load?

  • Check that the two URL strings match the reference code exactly.
  • Confirm that the Colab runtime still shows a connected session.
  • Rerun the import cell if the runtime restarted after package installation.

Ask for help with the traceback here: Why can't my Colab notebook load the IBM churn CSV?

✔️ Awesome, I've got everything!

Your notebook now contains the pinned imports plus the verified IBM data-loading cell. Make sure Colab has saved the latest changes.

ⓧ I'd like to double check the full code

import math
from pickle import dump

import matplotlib.pyplot as plt
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.dummy import DummyClassifier
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (
    accuracy_score,
    average_precision_score,
    precision_score,
    recall_score,
)
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler

DATA_URL = (
    "https://raw.githubusercontent.com/IBM/"
    "telco-customer-churn-on-icp4d/master/data/Telco-Customer-Churn.csv"
)

data = pd.read_csv(DATA_URL)
print("Dimensions:", data.shape)
print("Colonnes:", data.columns.tolist())
print("Doublons bruts:", int(data.duplicated().sum()))

That is your first reproducible checkpoint complete. Next, you will audit the table, document the cleaning decisions, and turn the first churn pattern into a visible chart.

Audit, Clean, and Explore the Data

Votre notebook Google Colab charge maintenant les données clients qui alimenteront la campagne de fidélisation. Ce fichier brut reste un point de départ fragile tant que ses types et ses anomalies ne sont pas connus.

Les espaces de TotalCharges peuvent masquer des valeurs manquantes. Des doublons ou des types incohérents peuvent aussi fausser le modèle. Cette étape rend ces problèmes visibles avant de produire un premier graphique sur le churn.

In this step, get ready to:
  • Construire un tableau d’audit pour chaque colonne du jeu de données.
  • Nettoyer les anomalies connues tout en documentant les décisions de prétraitement.
  • Comparer visuellement le taux de départ selon le type de contrat.
Auditer les colonnes

Un audit de données décrit la structure réelle du fichier avant toute modélisation. Avec pandas, vous allez convertir TotalCharges en valeurs numériques afin que ses espaces deviennent des valeurs manquantes explicites.

  • Ajoutez une nouvelle cellule de code sous la cellule qui charge data.
  • Construisez l’audit des colonnes en collant ce bloc dans la nouvelle cellule:
data["TotalCharges"] = pd.to_numeric(data["TotalCharges"], errors="coerce")
audit = pd.DataFrame(
    {
        "type": data.dtypes.astype(str),
        "valeurs_manquantes": data.isna().sum(),
        "valeurs_distinctes": data.nunique(dropna=False),
    }
)
print("\nAudit des donnees:")
print(audit)

Que fait ce code ?

  • La conversion de TotalCharges transforme les contenus non numériques en valeurs manquantes explicites.
  • Le tableau audit rassemble le type de chaque colonne.
  • Les colonnes valeurs_manquantes et valeurs_distinctes révèlent les anomalies qui pourraient affecter l’analyse.
  • Exécutez la cellule avec le bouton de lecture situé à sa gauche.

Vous verrez le tableau audit avec une ligne pour chaque colonne. La ligne TotalCharges affiche désormais un type numérique ainsi que les valeurs manquantes révélées par la conversion.

L’audit ne s’affiche pas ?

  • Vérifiez que la cellule de chargement de data a été exécutée dans le runtime actuel.
  • Vérifiez que TotalCharges conserve exactement la même casse que dans le CSV.

Aidez-moi à corriger la cellule d’audit de mon notebook.

Rendre le nettoyage traçable

Le nettoyage applique une règle métier précise à chaque anomalie. Le journal de prétraitement conserve ensuite la raison de chaque décision afin que le résultat reste explicable.

  • Ajoutez une nouvelle cellule de code sous la cellule d’audit.
  • Nettoyez les doublons et les valeurs cibles en collant ce bloc:
data = data.drop_duplicates().copy()
missing_new_customers = data["TotalCharges"].isna() & data["tenure"].eq(0)
data.loc[missing_new_customers, "TotalCharges"] = 0
data["Churn"] = data["Churn"].map({"No": 0, "Yes": 1})

Comment les données sont-elles nettoyées ?

  • La suppression des doublons empêche une même observation d’être comptée plusieurs fois.
  • La variable missing_new_customers repère les nouveaux clients dont TotalCharges est manquant.
  • La valeur 0 représente l’absence de charges historiques lorsque tenure vaut 0.
  • La colonne Churn devient une cible numérique où No correspond à 0 et Yes correspond à 1.
  • Exécutez la cellule de nettoyage avec le bouton de lecture.

La cellule doit se terminer sans erreur. Le DataFrame data contient maintenant une cible numérique ainsi que des charges corrigées pour les nouveaux clients concernés.

Le nettoyage échoue ?

  • Exécutez d’abord la cellule d’audit afin que TotalCharges soit numérique.
  • Vérifiez que les noms tenure et Churn correspondent exactement aux colonnes du fichier.

Aidez-moi à diagnostiquer le nettoyage de data.

Une transformation sans trace devient difficile à justifier quelques semaines plus tard. Le prochain tableau relie chaque décision à son objectif.

  • Ajoutez une nouvelle cellule de code sous la cellule de nettoyage.
  • Créez le journal de prétraitement en collant ce bloc:
journal_pretraitement = pd.DataFrame(
    [
        {
            "decision": "Convertir TotalCharges en nombre",
            "raison": "Les espaces deviennent des valeurs manquantes explicites",
        },
        {
            "decision": "Mettre TotalCharges a 0 quand tenure vaut 0",
            "raison": "Regle metier pour les nouveaux clients sans historique",
        },
        {
            "decision": "Supprimer les doublons exacts",
            "raison": "Eviter de compter deux fois la meme observation",
        },
        {
            "decision": "Exclure customerID et gender des predicteurs",
            "raison": "Garder la tracabilite sans apprendre l'identifiant ou le genre",
        },
        {
            "decision": "Separer avant les transformations apprises",
            "raison": "Eviter la fuite d'information du test vers l'entrainement",
        },
    ]
)
print("\nJournal de pretraitement:")
print(journal_pretraitement)

Pourquoi tenir un journal ?

Le tableau journal_pretraitement associe chaque transformation à une justification. Cette trace vous aide à expliquer les choix appliqués avant l’entraînement du modèle.

  • Exécutez la cellule avec le bouton de lecture.

Bien joué. Le journal affiche cinq décisions avec leur raison respective.

Le journal est incomplet ?

  • Comparez les accolades de chaque entrée afin de vérifier que les cinq dictionnaires sont fermés.
  • Vérifiez que la liste complète reste placée à l’intérieur de pd.DataFrame().

Aidez-moi à corriger la structure de journal_pretraitement.

Explorer le churn par contrat

Un taux groupé transforme la cible individuelle en comparaison descriptive. Matplotlib rend cette comparaison visible avec une barre pour chaque type de contrat.

  • Ajoutez une nouvelle cellule de code sous le journal de prétraitement.
  • Calculez les taux de départ par contrat en collant ce bloc:
contract_churn = (
    data.groupby("Contract", observed=True)["Churn"].mean().sort_values()
)
print("\nTaux de depart par contrat:")
print(contract_churn)
plt.bar(contract_churn.index, contract_churn.values)
plt.show()

Comment lire cette exploration ?

  • Le regroupement par Contract réunit les clients ayant le même type de contrat.
  • La moyenne de Churn correspond à la proportion de départs dans chaque groupe.
  • Le tri place les taux dans l’ordre avant leur affichage sous forme de barres.

Le graphique montre une association descriptive entre le contrat et le départ. Il ne prouve pas que le type de contrat cause le churn.

Avant l’exécution, prédisez mentalement quel type de contrat affichera le taux de départ le plus élevé.

  • Exécutez la cellule avec le bouton de lecture.

Vous verrez le tableau contract_churn ainsi qu’un diagramme à barres comparant les taux de départ par type de contrat. Les différences de hauteur rendent l’association visible sans lui donner une interprétation causale.

Le graphique ne s’affiche pas ?

  • Vérifiez que la cellule de nettoyage a été exécutée afin que Churn contienne des valeurs numériques.
  • Vérifiez que la dernière ligne contient bien plt.show().
  • Relancez la cellule après avoir confirmé que Contract existe dans data.

Aidez-moi à afficher le graphique des taux de churn par contrat.

✔️ Mon audit et mon graphique sont prêts

Votre notebook contient maintenant l’audit, les données nettoyées, le journal des décisions et le graphique par contrat. Vérifiez que toutes les cellules sont enregistrées dans Aurore_Telecom_Churn.

ⓧ Je souhaite vérifier le code complet

import math
from pickle import dump

import matplotlib.pyplot as plt
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.dummy import DummyClassifier
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (
    accuracy_score,
    average_precision_score,
    precision_score,
    recall_score,
)
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler

DATA_URL = (
    "https://raw.githubusercontent.com/IBM/"
    "telco-customer-churn-on-icp4d/master/data/Telco-Customer-Churn.csv"
)

data = pd.read_csv(DATA_URL)
print("Dimensions:", data.shape)
print("Colonnes:", data.columns.tolist())
print("Doublons bruts:", int(data.duplicated().sum()))

data["TotalCharges"] = pd.to_numeric(data["TotalCharges"], errors="coerce")
audit = pd.DataFrame(
    {
        "type": data.dtypes.astype(str),
        "valeurs_manquantes": data.isna().sum(),
        "valeurs_distinctes": data.nunique(dropna=False),
    }
)
print("\nAudit des donnees:")
print(audit)

data = data.drop_duplicates().copy()
missing_new_customers = data["TotalCharges"].isna() & data["tenure"].eq(0)
data.loc[missing_new_customers, "TotalCharges"] = 0
data["Churn"] = data["Churn"].map({"No": 0, "Yes": 1})

journal_pretraitement = pd.DataFrame(
    [
        {
            "decision": "Convertir TotalCharges en nombre",
            "raison": "Les espaces deviennent des valeurs manquantes explicites",
        },
        {
            "decision": "Mettre TotalCharges a 0 quand tenure vaut 0",
            "raison": "Regle metier pour les nouveaux clients sans historique",
        },
        {
            "decision": "Supprimer les doublons exacts",
            "raison": "Eviter de compter deux fois la meme observation",
        },
        {
            "decision": "Exclure customerID et gender des predicteurs",
            "raison": "Garder la tracabilite sans apprendre l'identifiant ou le genre",
        },
        {
            "decision": "Separer avant les transformations apprises",
            "raison": "Eviter la fuite d'information du test vers l'entrainement",
        },
    ]
)
print("\nJournal de pretraitement:")
print(journal_pretraitement)

contract_churn = (
    data.groupby("Contract", observed=True)["Churn"].mean().sort_values()
)
print("\nTaux de depart par contrat:")
print(contract_churn)
plt.bar(contract_churn.index, contract_churn.values)
plt.show()

Vos données sont maintenant auditées, nettoyées et accompagnées d’une première observation visible. Dans l’étape suivante, vous construirez une référence simple qui expose le piège d’une exactitude apparemment rassurante.

Build a Baseline That Reveals the Trap

Your Google Colab notebook now holds cleaned customer data. The contract chart gives you a visible starting point for modeling.

A majority-class baseline gives every later model a minimum standard to beat. This step tests whether accuracy can hide missed departures that recall exposes.

In this step, get ready to:
  • Create a stratified training and test split.
  • Preserve the test customer identifiers.
  • Measure a majority-class reference with four metrics.
Prepare the modeling split

Model evaluation needs test rows that remain separate from training. A stratified split preserves the churn proportion across both groups.

  • Create the modeling split in a new code cell below the chart cell by pasting this code:
feature_columns = [
    column
    for column in data.columns
    if column not in {"customerID", "gender", "Churn"}
]
X = data[feature_columns]
y = data["Churn"]
customer_ids = data["customerID"]

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.20,
    random_state=42,
    stratify=y,
)
ids_test = customer_ids.loc[X_test.index]

What does this code do?

  • The feature_columns list excludes the customer identifier. It also excludes gender plus the target.
  • The X table holds predictors. The y series holds the integer churn labels.
  • The train_test_split call reserves 20 percent of the rows for testing. Its fixed random state makes the split reproducible.
  • The stratify=y setting preserves the target distribution. The ids_test series keeps each test row connected to its customer.
  • Run the new cell.

You will see the cell finish without a traceback. The training data plus the test data are now available for comparison.

Seeing a split error?

  • Confirm that the earlier cleaning cell mapped Churn to integer values.
  • Check that feature_columns uses the exact capitalization shown in the code.
  • Rerun the earlier cells in notebook order if data is unavailable.

Ask for help with the split error.

Train the majority reference

A majority reference always chooses the most frequent training label. It establishes the performance available from class frequency alone.

  • Add the majority reference to a new code cell by pasting this code:
baseline = DummyClassifier(strategy="most_frequent")
baseline.fit(X_train[["tenure"]], y_train)
baseline_predictions = baseline.predict(X_test[["tenure"]])
baseline_probabilities = baseline.predict_proba(X_test[["tenure"]])[:, 1]

How does the reference work?

  • The DummyClassifier provides a deliberately simple reference from scikit-learn.
  • The most_frequent strategy learns which target label appears most often in the training set.
  • The tenure column gives the classifier the required input shape. This strategy does not learn a relationship from that feature.
  • The baseline_probabilities values provide scores for average precision.
  • Run the baseline cell.

The cell completes without a traceback. Your reference predictions plus probabilities are now ready to evaluate.

Baseline cell not completing?

  • Confirm that the split cell ran before this cell.
  • Check that tenure appears inside double brackets in both predictor selections.
  • Rerun the import cell if DummyClassifier is unavailable.

Ask for help with the baseline cell.

Predictions become useful when every model is scored consistently. A shared function creates one metric row from labels plus probabilities.

  • Create the shared evaluation function in a new code cell by pasting this code:
def evaluation_row(model_name, true_values, predictions, probabilities):
    return {
        "modele": model_name,
        "exactitude": accuracy_score(true_values, predictions),
        "precision": precision_score(
            true_values, predictions, zero_division=0.0
        ),
        "rappel": recall_score(true_values, predictions, zero_division=0.0),
        "average_precision": average_precision_score(
            true_values, probabilities
        ),
    }

What do the metrics measure?

  • Accuracy measures the share of correct class predictions.
  • Precision measures how often a predicted departure is an observed departure.
  • Recall measures how many observed departures the model identifies.
  • Average precision summarizes how well the probabilities rank departing customers.
  • Run the evaluation function cell.

The cell completes without a traceback. The evaluation_row function is now ready to score the reference.

Evaluation function not available?

  • Check that the function name is exactly evaluation_row.
  • Confirm that every metric was imported in the first notebook cell.
  • Rerun the function cell after correcting any indentation mismatch.

Ask for help with the evaluation function.

Reveal the accuracy trap

The reference is trained. Its metric table now tests whether a plausible headline score supports the retention goal.

Before you run the next cell, do you think accuracy plus recall will describe the reference in the same way?

  • Build the baseline metrics table by running this code in a new cell:
baseline_metrics = pd.DataFrame(
    [
        evaluation_row(
            "Reference majoritaire",
            y_test,
            baseline_predictions,
            baseline_probabilities,
        )
    ]
)
print("\nObstacle volontaire:")
print(baseline_metrics.round(3))

How is the result assembled?

  • The evaluation_row function scores the reference against the held-out labels.
  • The pd.DataFrame call turns that metric row into a comparison table.
  • The round(3) call makes the four scores easier to read.

You will see a nonzero value under exactitude. The rappel value is 0.000.

You have exposed the trap. The reference earns accuracy from the majority class while identifying none of the departing customers.

Recall is not zero?

  • Confirm that the classifier uses strategy="most_frequent".
  • Check that baseline_predictions came from the test data.
  • Rerun the split cell plus every baseline cell in order.

Ask for help with an unexpected baseline result.

✔️ Awesome, I've got everything!

Your notebook now contains the stratified split plus the evaluated majority reference. The zero recall result gives your next model a clear benchmark to improve.

ⓧ I'd like to double check the full code

import math
from pickle import dump

import matplotlib.pyplot as plt
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.dummy import DummyClassifier
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (
    accuracy_score,
    average_precision_score,
    precision_score,
    recall_score,
)
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler

DATA_URL = (
    "https://raw.githubusercontent.com/IBM/"
    "telco-customer-churn-on-icp4d/master/data/Telco-Customer-Churn.csv"
)

data = pd.read_csv(DATA_URL)
print("Dimensions:", data.shape)
print("Colonnes:", data.columns.tolist())
print("Doublons bruts:", int(data.duplicated().sum()))

data["TotalCharges"] = pd.to_numeric(data["TotalCharges"], errors="coerce")
audit = pd.DataFrame(
    {
        "type": data.dtypes.astype(str),
        "valeurs_manquantes": data.isna().sum(),
        "valeurs_distinctes": data.nunique(dropna=False),
    }
)
print("\nAudit des donnees:")
print(audit)

data = data.drop_duplicates().copy()
missing_new_customers = data["TotalCharges"].isna() & data["tenure"].eq(0)
data.loc[missing_new_customers, "TotalCharges"] = 0
data["Churn"] = data["Churn"].map({"No": 0, "Yes": 1})

journal_pretraitement = pd.DataFrame(
    [
        {
            "decision": "Convertir TotalCharges en nombre",
            "raison": "Les espaces deviennent des valeurs manquantes explicites",
        },
        {
            "decision": "Mettre TotalCharges a 0 quand tenure vaut 0",
            "raison": "Regle metier pour les nouveaux clients sans historique",
        },
        {
            "decision": "Supprimer les doublons exacts",
            "raison": "Eviter de compter deux fois la meme observation",
        },
        {
            "decision": "Exclure customerID et gender des predicteurs",
            "raison": "Garder la tracabilite sans apprendre l'identifiant ou le genre",
        },
        {
            "decision": "Separer avant les transformations apprises",
            "raison": "Eviter la fuite d'information du test vers l'entrainement",
        },
    ]
)
print("\nJournal de pretraitement:")
print(journal_pretraitement)

contract_churn = (
    data.groupby("Contract", observed=True)["Churn"].mean().sort_values()
)
print("\nTaux de depart par contrat:")
print(contract_churn)
plt.bar(contract_churn.index, contract_churn.values)
plt.show()

feature_columns = [
    column
    for column in data.columns
    if column not in {"customerID", "gender", "Churn"}
]
X = data[feature_columns]
y = data["Churn"]
customer_ids = data["customerID"]

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.20,
    random_state=42,
    stratify=y,
)
ids_test = customer_ids.loc[X_test.index]

baseline = DummyClassifier(strategy="most_frequent")
baseline.fit(X_train[["tenure"]], y_train)
baseline_predictions = baseline.predict(X_test[["tenure"]])
baseline_probabilities = baseline.predict_proba(X_test[["tenure"]])[:, 1]


def evaluation_row(model_name, true_values, predictions, probabilities):
    return {
        "modele": model_name,
        "exactitude": accuracy_score(true_values, predictions),
        "precision": precision_score(
            true_values, predictions, zero_division=0.0
        ),
        "rappel": recall_score(true_values, predictions, zero_division=0.0),
        "average_precision": average_precision_score(
            true_values, probabilities
        ),
    }


baseline_metrics = pd.DataFrame(
    [
        evaluation_row(
            "Reference majoritaire",
            y_test,
            baseline_predictions,
            baseline_probabilities,
        )
    ]
)
print("\nObstacle volontaire:")
print(baseline_metrics.round(3))

Use this full-file view to compare the cumulative notebook code through this step. It matches the target artifact exactly.

Your reference has revealed the weakness the predictive model must correct. Next, you will build a preprocessing pipeline that learns from the training data and targets departing customers.

Train a Leakage-Safe Pipeline

You exposed the majority baseline's weakness in the previous step. Its nonzero accuracy hides a churn recall of zero.

A leakage-safe pipeline learns every transformation from the training split. It then evaluates logistic regression on the untouched test split.

In this step, get ready to:
  • Build separate preprocessing branches for numeric and categorical features.
  • Train a balanced logistic regression model through one combined pipeline.
  • Compare the model with the majority reference across four evaluation metrics.
Build the preprocessing branches

Numeric columns need imputation plus standardisation. Categorical columns need imputation plus encoding. Scikit-learn keeps these learned transformations inside the training workflow.

  • Add a code cell beneath the baseline comparison cell.
  • Build the numeric preprocessing branch by running this cell:
numeric_features = X_train.select_dtypes(include="number").columns.tolist()
categorical_features = X_train.select_dtypes(exclude="number").columns.tolist()

numeric_pipeline = Pipeline(
    steps=[
        ("imputation", SimpleImputer(strategy="median")),
        ("standardisation", StandardScaler()),
    ]
)

What does this code do?

  • The feature lists come from X_train. This keeps the test split outside the learned setup.
  • The numeric branch replaces missing values with the training median.
  • The StandardScaler step places numeric features on comparable scales.
  • Confirm that the cell finishes without an exception.
  • Check that numeric_pipeline is created after the cell runs.

Numeric branch not created?

Confirm that the earlier split cell created X_train. Check that the import cell includes Pipeline, SimpleImputer, and StandardScaler.

Check the capitalization of X_train if the cell reports an undefined name. Help me troubleshoot the numeric preprocessing branch.

  • Add another code cell beneath the numeric branch.
  • Build the categorical preprocessing branch by running this cell:
categorical_pipeline = Pipeline(
    steps=[
        ("imputation", SimpleImputer(strategy="most_frequent")),
        ("encodage", OneHotEncoder(handle_unknown="ignore")),
    ]
)

How are categories prepared?

  • The categorical imputer fills each missing value with the most frequent training value.
  • The encoder turns categories into numeric columns that logistic regression can use.
  • The handle_unknown="ignore" setting allows the pipeline to process a test category that did not appear during training.
  • Confirm that the cell finishes without an exception.
  • Check that categorical_pipeline is created after the cell runs.

Categorical branch not created?

Check that the import cell includes OneHotEncoder before running this cell. Confirm that every opening parenthesis has a matching closing parenthesis.

Run the numeric branch first because both branches rely on the earlier imports. Help me fix the categorical preprocessing branch.

  • Add a code cell beneath the categorical branch.
  • Combine both preprocessing branches by running this cell:
preprocessor = ColumnTransformer(
    transformers=[
        ("numerique", numeric_pipeline, numeric_features),
        ("categoriel", categorical_pipeline, categorical_features),
    ]
)

How are the branches combined?

  • The ColumnTransformer sends numeric columns through numeric_pipeline.
  • It sends categorical columns through categorical_pipeline.
  • The resulting feature matrix contains the prepared output from both branches.
  • Confirm that the cell finishes without an exception.
  • Check that preprocessor is created after the cell runs.

Preprocessor not created?

Run both preprocessing branch cells before this cell. Check that numeric_features and categorical_features use the exact names shown above.

Confirm that the import cell includes ColumnTransformer. Help me assemble the two preprocessing branches.

Assemble and train the model

The final model pipeline places preprocessing before classification. Calling fit() on this object learns every preprocessing value from X_train only.

Why logistic regression here?

Logistic regression produces probabilities that can rank customers by departure risk. Its balanced class weights also give the minority churn class more influence during training.

A random forest could model more complex patterns. Logistic regression keeps this prototype focused on leakage prevention plus business prioritisation.

  • Add a code cell beneath the combined preprocessor.
  • Assemble the preprocessing and classification stages by running this cell:
model = Pipeline(
    steps=[
        ("pretraitement", preprocessor),
        (
            "classification",
            LogisticRegression(max_iter=1000, class_weight="balanced"),
        ),
    ]
)

What does the model pipeline contain?

  • The pretraitement step applies the combined numeric and categorical transformations.
  • The classification step trains the logistic regression estimator.
  • The class_weight="balanced" setting adjusts class influence from the training labels.
  • Confirm that the cell finishes without an exception.
  • Check that model is created after the cell runs.

Model pipeline not created?

Confirm that preprocessor exists before this cell runs. Check that the import cell includes LogisticRegression.

Compare the step names with pretraitement and classification if the pipeline definition fails. Help me diagnose the model pipeline definition.

  • Add a code cell beneath the model definition.
  • Train the pipeline and score the untouched test split by running this cell:
model.fit(X_train, y_train)
model_predictions = model.predict(X_test)
model_probabilities = model.predict_proba(X_test)[:, 1]

What happens during training?

  • The fit() call learns preprocessing values plus model coefficients from the training split.
  • The predict() call produces a churn class for every test row.
  • The predict_proba() call produces a churn probability for every test row.
  • Confirm that the training cell finishes without an exception.
  • Check that model_predictions and model_probabilities are created after the cell runs.

Training cell failed?

Run the preprocessing cells in order if the notebook reports a missing pipeline object. Confirm that X_train and y_train still come from the stratified split.

Rerun the cleaning cells if TotalCharges still contains unconverted text values. Help me troubleshoot pipeline training.

Compare the model with the baseline

The majority reference set a demanding diagnostic target. The new model needs to identify some departing customers while keeping its other metrics visible.

Before you run the comparison, do you expect the logistic regression recall to remain at zero?

  • Add a code cell beneath the training cell.
  • Build the comparison table and reveal the result by running this cell:
metriques_modeles = pd.DataFrame(
    [
        evaluation_row(
            "Reference majoritaire",
            y_test,
            baseline_predictions,
            baseline_probabilities,
        ),
        evaluation_row(
            "Regression logistique",
            y_test,
            model_predictions,
            model_probabilities,
        ),
    ]
).set_index("modele")

print("\nComparaison des modeles:")
print(metriques_modeles.round(3))

How is the comparison built?

  • Both rows use the same evaluation_row() function. This makes the comparison consistent.
  • The table reports exactitude, precision, rappel, and average_precision for each model.
  • The model names become row labels through set_index("modele").

You should see rows for Reference majoritaire and Regression logistique beneath the comparison heading. The logistic regression row should show a nonzero value under rappel.

That is the key correction. Your pipeline now identifies some departing customers without learning preprocessing values from the test split.

Comparison table missing or incomplete?

Run the baseline cell again if baseline_predictions or baseline_probabilities is missing. Run the training cell again if either model output is missing.

Confirm that evaluation_row() exists before building the table. Help me fix the model comparison table.

✔️ Awesome, I've got everything!

Your leakage-safe model pipeline is trained. The two model rows now appear in one comparison table.

ⓧ I'd like to double check the full code

import math
from pickle import dump

import matplotlib.pyplot as plt
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.dummy import DummyClassifier
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (
    accuracy_score,
    average_precision_score,
    precision_score,
    recall_score,
)
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler

DATA_URL = (
    "https://raw.githubusercontent.com/IBM/"
    "telco-customer-churn-on-icp4d/master/data/Telco-Customer-Churn.csv"
)

data = pd.read_csv(DATA_URL)
print("Dimensions:", data.shape)
print("Colonnes:", data.columns.tolist())
print("Doublons bruts:", int(data.duplicated().sum()))

data["TotalCharges"] = pd.to_numeric(data["TotalCharges"], errors="coerce")
audit = pd.DataFrame(
    {
        "type": data.dtypes.astype(str),
        "valeurs_manquantes": data.isna().sum(),
        "valeurs_distinctes": data.nunique(dropna=False),
    }
)
print("\nAudit des donnees:")
print(audit)

data = data.drop_duplicates().copy()
missing_new_customers = data["TotalCharges"].isna() & data["tenure"].eq(0)
data.loc[missing_new_customers, "TotalCharges"] = 0
data["Churn"] = data["Churn"].map({"No": 0, "Yes": 1})

journal_pretraitement = pd.DataFrame(
    [
        {
            "decision": "Convertir TotalCharges en nombre",
            "raison": "Les espaces deviennent des valeurs manquantes explicites",
        },
        {
            "decision": "Mettre TotalCharges a 0 quand tenure vaut 0",
            "raison": "Regle metier pour les nouveaux clients sans historique",
        },
        {
            "decision": "Supprimer les doublons exacts",
            "raison": "Eviter de compter deux fois la meme observation",
        },
        {
            "decision": "Exclure customerID et gender des predicteurs",
            "raison": "Garder la tracabilite sans apprendre l'identifiant ou le genre",
        },
        {
            "decision": "Separer avant les transformations apprises",
            "raison": "Eviter la fuite d'information du test vers l'entrainement",
        },
    ]
)
print("\nJournal de pretraitement:")
print(journal_pretraitement)

contract_churn = (
    data.groupby("Contract", observed=True)["Churn"].mean().sort_values()
)
print("\nTaux de depart par contrat:")
print(contract_churn)
plt.bar(contract_churn.index, contract_churn.values)
plt.show()

feature_columns = [
    column
    for column in data.columns
    if column not in {"customerID", "gender", "Churn"}
]
X = data[feature_columns]
y = data["Churn"]
customer_ids = data["customerID"]

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.20,
    random_state=42,
    stratify=y,
)
ids_test = customer_ids.loc[X_test.index]

baseline = DummyClassifier(strategy="most_frequent")
baseline.fit(X_train[["tenure"]], y_train)
baseline_predictions = baseline.predict(X_test[["tenure"]])
baseline_probabilities = baseline.predict_proba(X_test[["tenure"]])[:, 1]


def evaluation_row(model_name, true_values, predictions, probabilities):
    return {
        "modele": model_name,
        "exactitude": accuracy_score(true_values, predictions),
        "precision": precision_score(
            true_values, predictions, zero_division=0.0
        ),
        "rappel": recall_score(true_values, predictions, zero_division=0.0),
        "average_precision": average_precision_score(
            true_values, probabilities
        ),
    }


baseline_metrics = pd.DataFrame(
    [
        evaluation_row(
            "Reference majoritaire",
            y_test,
            baseline_predictions,
            baseline_probabilities,
        )
    ]
)
print("\nObstacle volontaire:")
print(baseline_metrics.round(3))

numeric_features = X_train.select_dtypes(include="number").columns.tolist()
categorical_features = X_train.select_dtypes(exclude="number").columns.tolist()

numeric_pipeline = Pipeline(
    steps=[
        ("imputation", SimpleImputer(strategy="median")),
        ("standardisation", StandardScaler()),
    ]
)

categorical_pipeline = Pipeline(
    steps=[
        ("imputation", SimpleImputer(strategy="most_frequent")),
        ("encodage", OneHotEncoder(handle_unknown="ignore")),
    ]
)

preprocessor = ColumnTransformer(
    transformers=[
        ("numerique", numeric_pipeline, numeric_features),
        ("categoriel", categorical_pipeline, categorical_features),
    ]
)

model = Pipeline(
    steps=[
        ("pretraitement", preprocessor),
        (
            "classification",
            LogisticRegression(max_iter=1000, class_weight="balanced"),
        ),
    ]
)

model.fit(X_train, y_train)
model_predictions = model.predict(X_test)
model_probabilities = model.predict_proba(X_test)[:, 1]

metriques_modeles = pd.DataFrame(
    [
        evaluation_row(
            "Reference majoritaire",
            y_test,
            baseline_predictions,
            baseline_probabilities,
        ),
        evaluation_row(
            "Regression logistique",
            y_test,
            model_predictions,
            model_probabilities,
        ),
    ]
).set_index("modele")

print("\nComparaison des modeles:")
print(metriques_modeles.round(3))

You now have a trained model that improves churn recall while protecting the test split from learned preprocessing. Next, you will turn its probabilities into a ranked monthly contact list.

Prioritize Contacts and Save Deliverables

The previous step produced one churn probability for every customer in the simulated monthly test batch. A retention team still needs a clear cut-off before it can act.

The business rule reserves contact capacity for the highest-risk 20 percent. This turns model probabilities into a ranked retention campaign.

You will save the ranked contact list plus the trained pipeline. You will also preserve the evaluation evidence plus a monthly monitoring plan.

In this step, get ready to:
  • Rank the test customers by churn probability.
  • Export the contact list for the highest-risk 20 percent.
  • Save the trained model plus its supporting evidence.
Rank the highest-risk customers

A fixed contact capacity requires an ordered list. The observed churn labels remain in this simulated batch so you can measure how many departures the top group captures.

  • Add a code cell beneath the model comparison output.
  • Calculate the top 20 percent plus its captured churn by pasting the code below into the new cell:
scored_test = pd.DataFrame(
    {
        "customerID": ids_test.to_numpy(),
        "probabilite_depart": model_probabilities,
        "depart_observe": y_test.to_numpy(),
    }
)

number_to_contact = math.ceil(len(scored_test) * 0.20)
top_contacts = scored_test.nlargest(
    number_to_contact, "probabilite_depart"
).copy()

captured_churn = (
    top_contacts["depart_observe"].sum()
    / scored_test["depart_observe"].sum()
)

print(
    f"\nContacter {number_to_contact} clients, soit 20 % du lot simule, "
    f"capture {captured_churn:.1%} des departs observes dans le test."
)

What does this code do?

  • The scored_test table reconnects each test identifier with its predicted probability plus its observed outcome.
  • The number_to_contact calculation rounds the 20 percent capacity upward to a whole customer.
  • The top_contacts table keeps the rows with the highest churn probabilities.
  • The captured_churn value measures the share of observed departures found inside that priority group.
  • Run the cell.
  • Confirm the printed sentence reports a contact count plus a captured-churn percentage.

You now have a measurable campaign rule. The model has moved from prediction to a concrete contact decision.

Missing the campaign summary?

  • Confirm the model cell ran first so that model_probabilities exists.
  • Check that ids_test still uses the indexes from X_test.
  • Rerun the split plus model cells if the Colab runtime was reset.

Ask for help with the ranking calculation.

Visualize recall and export contacts

Recall shows how many observed departures each classifier detects. A bar chart makes the baseline gap visible before you hand over the prioritized CSV contact list.

  • Add a code cell beneath the campaign summary.
  • Build the recall comparison chart by pasting the code below into the new cell:
plt.bar(
    ["Reference", "Regression"],
    metriques_modeles["rappel"].values,
)
plt.show()

Why compare recall visually?

The chart places the majority reference beside the logistic regression model. The height difference shows whether the trained model finds departing customers that the reference misses.

  • Run the chart cell.
  • Confirm the regression bar rises above the reference bar.

That visible gap is the model's practical improvement. It detects members of the minority class that the majority reference ignores.

Chart missing or empty?

  • Confirm metriques_modeles contains both model rows.
  • Check that the metrics table includes the rappel column.
  • Rerun the model evaluation cell before rerunning the chart.

Ask for help with the recall chart.

  • Add another code cell beneath the recall chart.
  • Create the shareable contact file by pasting the code below into the new cell:
contact_list = top_contacts[
    ["customerID", "probabilite_depart"]
].copy()
contact_list.to_csv("clients_a_contacter.csv", index=False)

Why export only two columns?

The retention team needs a customer identifier plus a priority score. Removing the observed outcome prevents the simulated test label from becoming part of the operational contact file.

  • Run the export cell.
  • Use Colab's file pane to confirm clients_a_contacter.csv exists in the temporary runtime storage.
  • Open the file preview to confirm it contains only customerID plus probabilite_depart.

Contact file not appearing?

  • Refresh the Colab file pane after running the export cell.
  • Confirm top_contacts exists before creating contact_list.
  • Rerun the export cell if the runtime restarted.

Ask for help with the contact export.

Save the model and monitoring plan

A monthly batch process needs more than a contact list. The saved pipeline preserves preprocessing plus classification as one pickle artifact.

The monitoring plan explains how the team uses that artifact over time. It also records the business checks plus model checks that keep the campaign accountable.

  • Select the folder icon in the left sidebar of the Colab notebook.
  • Create plan_suivi.md in the temporary runtime storage by using the file pane's new-file control.
  • Open plan_suivi.md in the file editor.
  • Add the title plus monthly workflow by pasting this first section:
# Plan de suivi Aurore Télécom

## Utilisation mensuelle

1. Recevoir un nouveau fichier client avec les mêmes colonnes prédictives.
2. Appliquer le pipeline sauvegardé pour calculer une probabilité de départ.
3. Contacter le groupe prioritaire défini par la capacité de l'équipe.
4. Garder un groupe témoin comparable qui ne reçoit pas l'offre.

What does this section define?

The workflow starts with a new monthly customer file. It ends with a prioritized contact group plus a comparable control group.

  • Press Cmd+S on macOS or Ctrl+S on Windows to save plan_suivi.md.
  • Confirm the file preview shows the title plus four monthly workflow items.

Plan file not saving?

  • Confirm the filename ends with .md.
  • Check that the file editor is focused before using the save shortcut.

Ask for help creating the monitoring file.

  • Place the cursor after the monthly workflow.
  • Add one blank line.
  • Add the business plus model indicators by pasting this section:
## Indicateurs métier

- Taux de contact réussi.
- Taux d'acceptation de l'offre.
- Écart de rétention entre le groupe contacté et le groupe témoin.
- Coût par client retenu.

## Indicateurs du modèle

- Part réelle de départs dans le groupe prioritaire.
- Rappel lorsque les étiquettes du mois deviennent disponibles.
- Average precision sur les nouvelles données étiquetées.
- Évolution des valeurs manquantes et des catégories inconnues.

Why monitor two kinds of indicators?

Business indicators measure campaign execution plus retention outcomes. Model indicators reveal whether ranking quality or incoming data has changed.

  • Save plan_suivi.md.
  • Confirm the preview shows separate business plus model indicator headings.

Indicators in the wrong section?

  • Confirm each heading begins with two hash characters.
  • Check that every indicator begins on its own bullet line.

Ask for help formatting the monitoring indicators.

  • Place the cursor after the model indicators.
  • Add one blank line.
  • Complete the plan by pasting the maintenance plus limitations section:
## Maintenance

- Revoir les indicateurs après chaque campagne mensuelle.
- Déclencher une investigation si les données ou les résultats changent fortement.
- Réentraîner seulement après validation de données récentes et comparaison au modèle en place.
- Conserver les versions des dépendances, la source des données et les métriques avec chaque modèle.

## Limites

Ce prototype utilise un instantané étiqueté comme lot de test simulé. Il ne prouve pas qu'une offre cause la rétention et doit être validé par une expérimentation contrôlée avant une utilisation réelle.

Why document the limitation?

The held-out test data simulates a monthly batch. It does not prove future performance or show that a retention offer causes customers to stay.

  • Save plan_suivi.md.
  • Confirm the preview ends with the controlled-experiment limitation.

Plan content incomplete?

  • Compare the preview with all five required section headings.
  • Restore any missing blank line between sections.
  • Save the file again after completing the limitations paragraph.

Ask for help checking the complete monitoring plan.

  • Return to the Colab notebook.
  • Add a final code cell beneath the contact-list export.
  • Save the evaluation evidence plus trained pipeline by pasting the code below into the final cell:
metriques_modeles.to_csv("metriques_modeles.csv")
audit.to_csv("audit_donnees.csv")
journal_pretraitement.to_csv("journal_pretraitement.csv", index=False)
data.to_csv("telco_propre.csv", index=False)

with open("modele_churn.pkl", "wb") as model_file:
    dump(model, model_file, protocol=5)

print("\nLivrables crees:")
print("- clients_a_contacter.csv")
print("- modele_churn.pkl")
print("- metriques_modeles.csv")
print("- audit_donnees.csv")
print("- journal_pretraitement.csv")
print("- telco_propre.csv")

What do these deliverables preserve?

  • The metrics export preserves the comparison between the majority reference plus logistic regression.
  • The audit plus preprocessing journal preserve the evidence behind the cleaned dataset.
  • The clean-data export preserves the exact table used for modeling.
  • The model file preserves the fitted preprocessing plus classifier with protocol 5.

Only load trusted model files

Loading a pickle-based artifact can execute code. Only load modele_churn.pkl when you trust its source.

Keep requirements.txt beside the model. Matching dependency versions protect compatibility when you load it later.

Before you run the final cell, which evidence files do you expect the notebook to preserve?

  • Run the final cell.
  • Confirm clients_a_contacter.csv exists in the Colab file pane.
  • Confirm modele_churn.pkl exists in the Colab file pane.
  • Confirm metriques_modeles.csv exists in the Colab file pane.
  • Confirm audit_donnees.csv exists in the Colab file pane.
  • Confirm journal_pretraitement.csv exists in the Colab file pane.
  • Confirm telco_propre.csv exists in the Colab file pane.
  • Confirm plan_suivi.md remains in the Colab file pane.

The output begins with Livrables crees:. Six generated filenames appear beneath it.

Strong finish. Your notebook now produces a ranked campaign plus the evidence needed to explain how it was built.

A deliverable is missing?

  • Refresh the Colab file pane after the final cell finishes.
  • Rerun the earlier ranking cell if clients_a_contacter.csv is missing.
  • Rerun the model-training cell if model is undefined.

Ask for help recovering the missing artifact.

✔️ Awesome, I've got everything!

Your contact list, model, evidence files, and monitoring plan are ready. Keep the notebook plus any deliverables you want beyond the current Colab runtime.

ⓧ I'd like to double check the full code

Compare your notebook code with the complete cumulative aurore_telecom.py reference:

import math
from pickle import dump

import matplotlib.pyplot as plt
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.dummy import DummyClassifier
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (
    accuracy_score,
    average_precision_score,
    precision_score,
    recall_score,
)
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler

DATA_URL = (
    "https://raw.githubusercontent.com/IBM/"
    "telco-customer-churn-on-icp4d/master/data/Telco-Customer-Churn.csv"
)

data = pd.read_csv(DATA_URL)
print("Dimensions:", data.shape)
print("Colonnes:", data.columns.tolist())
print("Doublons bruts:", int(data.duplicated().sum()))

data["TotalCharges"] = pd.to_numeric(data["TotalCharges"], errors="coerce")
audit = pd.DataFrame(
    {
        "type": data.dtypes.astype(str),
        "valeurs_manquantes": data.isna().sum(),
        "valeurs_distinctes": data.nunique(dropna=False),
    }
)
print("\nAudit des donnees:")
print(audit)

data = data.drop_duplicates().copy()
missing_new_customers = data["TotalCharges"].isna() & data["tenure"].eq(0)
data.loc[missing_new_customers, "TotalCharges"] = 0
data["Churn"] = data["Churn"].map({"No": 0, "Yes": 1})

journal_pretraitement = pd.DataFrame(
    [
        {
            "decision": "Convertir TotalCharges en nombre",
            "raison": "Les espaces deviennent des valeurs manquantes explicites",
        },
        {
            "decision": "Mettre TotalCharges a 0 quand tenure vaut 0",
            "raison": "Regle metier pour les nouveaux clients sans historique",
        },
        {
            "decision": "Supprimer les doublons exacts",
            "raison": "Eviter de compter deux fois la meme observation",
        },
        {
            "decision": "Exclure customerID et gender des predicteurs",
            "raison": "Garder la tracabilite sans apprendre l'identifiant ou le genre",
        },
        {
            "decision": "Separer avant les transformations apprises",
            "raison": "Eviter la fuite d'information du test vers l'entrainement",
        },
    ]
)
print("\nJournal de pretraitement:")
print(journal_pretraitement)

contract_churn = (
    data.groupby("Contract", observed=True)["Churn"].mean().sort_values()
)
print("\nTaux de depart par contrat:")
print(contract_churn)
plt.bar(contract_churn.index, contract_churn.values)
plt.show()

feature_columns = [
    column
    for column in data.columns
    if column not in {"customerID", "gender", "Churn"}
]
X = data[feature_columns]
y = data["Churn"]
customer_ids = data["customerID"]

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.20,
    random_state=42,
    stratify=y,
)
ids_test = customer_ids.loc[X_test.index]

baseline = DummyClassifier(strategy="most_frequent")
baseline.fit(X_train[["tenure"]], y_train)
baseline_predictions = baseline.predict(X_test[["tenure"]])
baseline_probabilities = baseline.predict_proba(X_test[["tenure"]])[:, 1]


def evaluation_row(model_name, true_values, predictions, probabilities):
    return {
        "modele": model_name,
        "exactitude": accuracy_score(true_values, predictions),
        "precision": precision_score(
            true_values, predictions, zero_division=0.0
        ),
        "rappel": recall_score(true_values, predictions, zero_division=0.0),
        "average_precision": average_precision_score(
            true_values, probabilities
        ),
    }


baseline_metrics = pd.DataFrame(
    [
        evaluation_row(
            "Reference majoritaire",
            y_test,
            baseline_predictions,
            baseline_probabilities,
        )
    ]
)
print("\nObstacle volontaire:")
print(baseline_metrics.round(3))

numeric_features = X_train.select_dtypes(include="number").columns.tolist()
categorical_features = X_train.select_dtypes(exclude="number").columns.tolist()

numeric_pipeline = Pipeline(
    steps=[
        ("imputation", SimpleImputer(strategy="median")),
        ("standardisation", StandardScaler()),
    ]
)

categorical_pipeline = Pipeline(
    steps=[
        ("imputation", SimpleImputer(strategy="most_frequent")),
        ("encodage", OneHotEncoder(handle_unknown="ignore")),
    ]
)

preprocessor = ColumnTransformer(
    transformers=[
        ("numerique", numeric_pipeline, numeric_features),
        ("categoriel", categorical_pipeline, categorical_features),
    ]
)

model = Pipeline(
    steps=[
        ("pretraitement", preprocessor),
        (
            "classification",
            LogisticRegression(max_iter=1000, class_weight="balanced"),
        ),
    ]
)

model.fit(X_train, y_train)
model_predictions = model.predict(X_test)
model_probabilities = model.predict_proba(X_test)[:, 1]

metriques_modeles = pd.DataFrame(
    [
        evaluation_row(
            "Reference majoritaire",
            y_test,
            baseline_predictions,
            baseline_probabilities,
        ),
        evaluation_row(
            "Regression logistique",
            y_test,
            model_predictions,
            model_probabilities,
        ),
    ]
).set_index("modele")

print("\nComparaison des modeles:")
print(metriques_modeles.round(3))

scored_test = pd.DataFrame(
    {
        "customerID": ids_test.to_numpy(),
        "probabilite_depart": model_probabilities,
        "depart_observe": y_test.to_numpy(),
    }
)

number_to_contact = math.ceil(len(scored_test) * 0.20)
top_contacts = scored_test.nlargest(
    number_to_contact, "probabilite_depart"
).copy()

captured_churn = (
    top_contacts["depart_observe"].sum()
    / scored_test["depart_observe"].sum()
)

print(
    f"\nContacter {number_to_contact} clients, soit 20 % du lot simule, "
    f"capture {captured_churn:.1%} des departs observes dans le test."
)

plt.bar(
    ["Reference", "Regression"],
    metriques_modeles["rappel"].values,
)
plt.show()

contact_list = top_contacts[
    ["customerID", "probabilite_depart"]
].copy()
contact_list.to_csv("clients_a_contacter.csv", index=False)
metriques_modeles.to_csv("metriques_modeles.csv")
audit.to_csv("audit_donnees.csv")
journal_pretraitement.to_csv("journal_pretraitement.csv", index=False)
data.to_csv("telco_propre.csv", index=False)

with open("modele_churn.pkl", "wb") as model_file:
    dump(model, model_file, protocol=5)

print("\nLivrables crees:")
print("- clients_a_contacter.csv")
print("- modele_churn.pkl")
print("- metriques_modeles.csv")
print("- audit_donnees.csv")
print("- journal_pretraitement.csv")
print("- telco_propre.csv")

Compare your monitoring document with the complete plan_suivi.md reference:

# Plan de suivi Aurore Télécom

## Utilisation mensuelle

1. Recevoir un nouveau fichier client avec les mêmes colonnes prédictives.
2. Appliquer le pipeline sauvegardé pour calculer une probabilité de départ.
3. Contacter le groupe prioritaire défini par la capacité de l'équipe.
4. Garder un groupe témoin comparable qui ne reçoit pas l'offre.

## Indicateurs métier

- Taux de contact réussi.
- Taux d'acceptation de l'offre.
- Écart de rétention entre le groupe contacté et le groupe témoin.
- Coût par client retenu.

## Indicateurs du modèle

- Part réelle de départs dans le groupe prioritaire.
- Rappel lorsque les étiquettes du mois deviennent disponibles.
- Average precision sur les nouvelles données étiquetées.
- Évolution des valeurs manquantes et des catégories inconnues.

## Maintenance

- Revoir les indicateurs après chaque campagne mensuelle.
- Déclencher une investigation si les données ou les résultats changent fortement.
- Réentraîner seulement après validation de données récentes et comparaison au modèle en place.
- Conserver les versions des dépendances, la source des données et les métriques avec chaque modèle.

## Limites

Ce prototype utilise un instantané étiqueté comme lot de test simulé. Il ne prouve pas qu'une offre cause la rétention et doit être validé par une expérimentation contrôlée avant une utilisation réelle.

Keep these exact dependency pins in requirements.txt:

pandas==3.1.0
scikit-learn==1.9.1
matplotlib==3.11.2

Your retention prototype now runs from audited data to a prioritized monthly contact list. Its saved evidence makes the decision reproducible after the current Colab session ends.

Secret mission

Tester plusieurs capacités de contact

La capacité mensuelle de l’équipe de fidélisation peut évoluer. Cette mission compare les départs capturés lorsque l’équipe contacte 10 %, 20 % ou 30 % des clients les plus à risque.

Clean Up Your Resources

Clean Up Your Resources

Vous pouvez conserver le notebook, faire une pause ou supprimer tous les artefacts. Le projet coûte 0 $.

Chargez seulement votre modèle

Un fichier pickle provenant d'une source non fiable peut exécuter du code au chargement.

  • Chargez uniquement le fichier modele_churn.pkl que vous avez créé.
  • Conservez requirements.txt avec modele_churn.pkl pour retrouver les versions utilisées.

Resources you used:

  • Le notebook Google Colab Aurore_Telecom_Churn stocké dans Google Drive.
  • Les fichiers de code et de suivi aurore_telecom.py, cellule_secret_mission.py, plan_suivi.md et requirements.txt dans la machine virtuelle Colab.
  • Les livrables clients_a_contacter.csv, modele_churn.pkl, metriques_modeles.csv, audit_donnees.csv, journal_pretraitement.csv, telco_propre.csv et scenario_capacite.csv dans la machine virtuelle Colab.
  • Les éventuelles copies téléchargées sur Windows.

Keep everything running

Aucune action n'est requise pour conserver le notebook. Cette option convient si vous poursuivez l'analyse.

  • Téléchargez depuis le navigateur de fichiers du notebook les artefacts que vous souhaitez conserver.

Les fichiers de la machine virtuelle Colab peuvent disparaître lorsque l'environnement est supprimé. Les copies téléchargées restent disponibles sur Windows.

Pause - I'll come back to this later

Cette option conserve le notebook pour une prochaine session. Les fichiers temporaires devront être recréés si la machine virtuelle est supprimée.

  • Téléchargez les livrables que vous souhaitez garder depuis le navigateur de fichiers du notebook déjà ouvert.
  • Fermez l'onglet du notebook lorsque vos téléchargements sont terminés.

Votre code reste dans Google Drive. Vous pourrez réexécuter les cellules pour recréer les fichiers temporaires.

Delete - I don't want to use this again

La suppression est définitive, mais elle ne touche aucun service payant. Aucun serveur ne reste actif après ce projet.

  • Téléchargez tout livrable que vous souhaitez conserver.
  • Affichez le navigateur de fichiers depuis la barre latérale gauche du notebook déjà ouvert.
  • Sélectionnez aurore_telecom.py, cellule_secret_mission.py, plan_suivi.md, requirements.txt, clients_a_contacter.csv, modele_churn.pkl, metriques_modeles.csv, audit_donnees.csv, journal_pretraitement.csv, telco_propre.csv et scenario_capacite.csv.
  • Utilisez l'action de suppression du menu contextuel.
  • Vérifiez que ces noms ne figurent plus dans le navigateur de fichiers.

La partie volatile est nettoyée. Les livrables ne restent plus dans la machine virtuelle Colab.

  • Revenez à Google Drive.
  • Recherchez Aurore_Telecom_Churn.
  • Placez le notebook dans la corbeille depuis son menu contextuel.
  • Videz la corbeille de Google Drive pour supprimer définitivement le notebook.

Le notebook ne figure désormais plus dans Google Drive. Il reste à retirer les éventuelles copies enregistrées sur Windows.

  • Appuyez sur la touche Windows pour ouvrir la recherche.
  • Recherchez le gestionnaire de fichiers.
  • Ouvrez le résultat correspondant.
  • Accédez au dossier où votre navigateur enregistre les téléchargements.
  • Supprimez les copies des artefacts de ce projet.
  • Videz la corbeille Windows pour terminer le nettoyage.

Tous les artefacts du projet sont maintenant supprimés. Aucun coût ni processus actif ne subsiste.

Nice Work!

Nice Work!

Mission accomplie ! Vous avez transformé les données client d'Aurore Télécom en une campagne de fidélisation priorisée dans Google Colab.

Vous avez appris à :

  • Préparer des données fiables grâce à un audit de données, un nettoyage documenté et une exploration du départ par contrat.
  • Diagnostiquer le piège de l'exactitude avec une référence majoritaire dont le rappel nul manque tous les départs.
  • Construire un pipeline sans fuite de données autour d'une régression logistique. Vous avez converti ses probabilités en clients_a_contacter.csv pour piloter le lot mensuel simulé.
  • Secret Mission : Comparer les capacités de contact de 10 %, 20 % et 30 % avec capture_at_fraction(). Vous avez exporté les compromis dans scenario_capacite.csv.

Prêt à tester vos connaissances ?