Kundenabwanderung

Kundenabwanderung ist ein branchenübergreifendes Problem. Häufig wird sehr viel Energie in die Gewinnung von Kunden gesteckt, aber das Halten der Kunden wird vernachlässigt. Mit einer Prognose zur Kundenabwanderung können diejenigen Kunden identifiziert werden, welche gegebenenfalls abwandern würden. Diese kann das Unternehmen dann mit besonderen Angeboten oder anderweitigen Aufmerksamkeiten vielleicht zurückgewinnen.

[1]:
import pandas as pd
import matplotlib.pyplot as plt
/tmp/ipykernel_1192/1492400551.py:1: DeprecationWarning:
Pyarrow will become a required dependency of pandas in the next major release of pandas (pandas 3.0),
(to allow more performant data types, such as the Arrow string type, and better interoperability with other libraries)
but was not found to be installed on your system.
If this would cause problems for you,
please provide us feedback at https://github.com/pandas-dev/pandas/issues/54466

  import pandas as pd

Nun laden wir die Datei CustomerChurn.xlsx herunter. Der Code dafür ist bereits vorgegeben. IBM stellt den Datensatz in einem Blogbeitrag vor.

[2]:
import os
import requests
import shutil

file_name = "CustomerChurn.xlsx"
if file_name not in os.listdir("."):
    print(f"Lade Datei '{file_name}' herunter")
    r = requests.get(
        "https://public.dhe.ibm.com/software/data/sw-library/cognos/mobile/C11/data/CustomerChurn.xlsx",
        stream=True
    )
    if r.status_code == 200:
        with open(file_name, 'wb') as f:
            r.raw.decode_content = True
            shutil.copyfileobj(r.raw, f)
else:
    print(f"Datei '{file_name}' ist bereits heruntergeladen")
Lade Datei 'CustomerChurn.xlsx' herunter

Einlesen der Daten

Das Modul pandas liefert die Möglichkeit, Excel-Dateien mit der Funktion read_excel() einzulesen.

[3]:
df = pd.read_excel("CustomerChurn.xlsx")
df
[3]:
LoyaltyID Customer ID Senior Citizen Partner Dependents Tenure Phone Service Multiple Lines Internet Service Online Security ... Device Protection Tech Support Streaming TV Streaming Movies Contract Paperless Billing Payment Method Monthly Charges Total Charges Churn
0 318537 7590-VHVEG No Yes No 1 No No phone service DSL No ... No No No No Month-to-month Yes Electronic check 29.85 29.85 No
1 152148 5575-GNVDE No No No 34 Yes No DSL Yes ... Yes No No No One year No Mailed check 56.95 1889.5 No
2 326527 3668-QPYBK No No No 2 Yes No DSL Yes ... No No No No Month-to-month Yes Mailed check 53.85 108.15 Yes
3 845894 7795-CFOCW No No No 45 No No phone service DSL Yes ... Yes Yes No No One year No Bank transfer (automatic) 42.30 1840.75 No
4 503388 9237-HQITU No No No 2 Yes No Fiber optic No ... No No No No Month-to-month Yes Electronic check 70.70 151.65 Yes
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
7038 810338 6840-RESVB No Yes Yes 24 Yes Yes DSL Yes ... Yes Yes Yes Yes One year Yes Mailed check 84.80 1990.5 No
7039 230811 2234-XADUH No Yes Yes 72 Yes Yes Fiber optic No ... Yes No Yes Yes One year Yes Credit card (automatic) 103.20 7362.9 No
7040 155157 4801-JZAZL No Yes Yes 11 No No phone service DSL Yes ... No No No No Month-to-month Yes Electronic check 29.60 346.45 No
7041 731782 8361-LTMKD Yes Yes No 4 Yes Yes Fiber optic No ... No No No No Month-to-month Yes Mailed check 74.40 306.6 Yes
7042 353947 3186-AJIEK No No No 66 Yes No Fiber optic Yes ... Yes Yes Yes Yes Two year Yes Bank transfer (automatic) 105.65 6844.5 No

7043 rows × 21 columns

Es wurden verschiedene Skalen für Attribute vorgestellt. Die Methode df.info() zeigt an, welcher Typ innerhalb von pandas ermittelt worden ist. Eine kurze technische Erklärung der Typen gibt es in der Dokumentation von pandas.

  1. Führen Sie die nachfolgende Anweisung aus und interpretieren Sie das Ergebnis. Welche Spalten werden als nominalskaliert erkannt (object), sollten aber eine andere Skala haben?

Antwort:

[4]:
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7043 entries, 0 to 7042
Data columns (total 21 columns):
 #   Column             Non-Null Count  Dtype
---  ------             --------------  -----
 0   LoyaltyID          7043 non-null   int64
 1   Customer ID        7043 non-null   object
 2   Senior Citizen     7043 non-null   object
 3   Partner            7043 non-null   object
 4   Dependents         7043 non-null   object
 5   Tenure             7043 non-null   int64
 6   Phone Service      7043 non-null   object
 7   Multiple Lines     7043 non-null   object
 8   Internet Service   7043 non-null   object
 9   Online Security    7043 non-null   object
 10  Online Backup      7043 non-null   object
 11  Device Protection  7043 non-null   object
 12  Tech Support       7043 non-null   object
 13  Streaming TV       7043 non-null   object
 14  Streaming Movies   7043 non-null   object
 15  Contract           7043 non-null   object
 16  Paperless Billing  7043 non-null   object
 17  Payment Method     7043 non-null   object
 18  Monthly Charges    7043 non-null   float64
 19  Total Charges      7043 non-null   object
 20  Churn              7043 non-null   object
dtypes: float64(1), int64(2), object(18)
memory usage: 1.1+ MB

Umwandlung in korrekte Typen

Nun möchten wir aus dem Text in der Spalte Total Charges Zahlenwerte gewinnen, die wir weiter verwenden können.

[5]:
df["Total Charges"] = pd.to_numeric(df['Total Charges'], errors='coerce')
df
[5]:
LoyaltyID Customer ID Senior Citizen Partner Dependents Tenure Phone Service Multiple Lines Internet Service Online Security ... Device Protection Tech Support Streaming TV Streaming Movies Contract Paperless Billing Payment Method Monthly Charges Total Charges Churn
0 318537 7590-VHVEG No Yes No 1 No No phone service DSL No ... No No No No Month-to-month Yes Electronic check 29.85 29.85 No
1 152148 5575-GNVDE No No No 34 Yes No DSL Yes ... Yes No No No One year No Mailed check 56.95 1889.50 No
2 326527 3668-QPYBK No No No 2 Yes No DSL Yes ... No No No No Month-to-month Yes Mailed check 53.85 108.15 Yes
3 845894 7795-CFOCW No No No 45 No No phone service DSL Yes ... Yes Yes No No One year No Bank transfer (automatic) 42.30 1840.75 No
4 503388 9237-HQITU No No No 2 Yes No Fiber optic No ... No No No No Month-to-month Yes Electronic check 70.70 151.65 Yes
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
7038 810338 6840-RESVB No Yes Yes 24 Yes Yes DSL Yes ... Yes Yes Yes Yes One year Yes Mailed check 84.80 1990.50 No
7039 230811 2234-XADUH No Yes Yes 72 Yes Yes Fiber optic No ... Yes No Yes Yes One year Yes Credit card (automatic) 103.20 7362.90 No
7040 155157 4801-JZAZL No Yes Yes 11 No No phone service DSL Yes ... No No No No Month-to-month Yes Electronic check 29.60 346.45 No
7041 731782 8361-LTMKD Yes Yes No 4 Yes Yes Fiber optic No ... No No No No Month-to-month Yes Mailed check 74.40 306.60 Yes
7042 353947 3186-AJIEK No No No 66 Yes No Fiber optic Yes ... Yes Yes Yes Yes Two year Yes Bank transfer (automatic) 105.65 6844.50 No

7043 rows × 21 columns

  1. Führen Sie die nachfolgende Anweisung aus und interpretieren Sie das Ergebnis. Was hat sich verändert?

Antwort:

[6]:
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7043 entries, 0 to 7042
Data columns (total 21 columns):
 #   Column             Non-Null Count  Dtype
---  ------             --------------  -----
 0   LoyaltyID          7043 non-null   int64
 1   Customer ID        7043 non-null   object
 2   Senior Citizen     7043 non-null   object
 3   Partner            7043 non-null   object
 4   Dependents         7043 non-null   object
 5   Tenure             7043 non-null   int64
 6   Phone Service      7043 non-null   object
 7   Multiple Lines     7043 non-null   object
 8   Internet Service   7043 non-null   object
 9   Online Security    7043 non-null   object
 10  Online Backup      7043 non-null   object
 11  Device Protection  7043 non-null   object
 12  Tech Support       7043 non-null   object
 13  Streaming TV       7043 non-null   object
 14  Streaming Movies   7043 non-null   object
 15  Contract           7043 non-null   object
 16  Paperless Billing  7043 non-null   object
 17  Payment Method     7043 non-null   object
 18  Monthly Charges    7043 non-null   float64
 19  Total Charges      7032 non-null   float64
 20  Churn              7043 non-null   object
dtypes: float64(2), int64(2), object(17)
memory usage: 1.1+ MB

Spoiler Alert: Später im Jupyter Notebook möchten wir noch Maschinelles Lernen verwenden. Deswegen wollen wir schon mal alle „Yes“-Einträge in Einsen und alle „No“-Einträge in Nullen umwandeln.

[7]:
for attr in ["Phone Service", "Online Security", "Senior Citizen", "Partner", "Dependents",
             "Device Protection", "Paperless Billing", "Streaming Movies", "Streaming TV",
             "Tech Support", "Churn", "Multiple Lines", "Online Backup"]:
    df[attr] = df[attr].map({'Yes': 1, 'No': 0, 1: 1, 0: 0})

df
[7]:
LoyaltyID Customer ID Senior Citizen Partner Dependents Tenure Phone Service Multiple Lines Internet Service Online Security ... Device Protection Tech Support Streaming TV Streaming Movies Contract Paperless Billing Payment Method Monthly Charges Total Charges Churn
0 318537 7590-VHVEG 0 1 0 1 0 NaN DSL 0.0 ... 0.0 0.0 0.0 0.0 Month-to-month 1 Electronic check 29.85 29.85 0
1 152148 5575-GNVDE 0 0 0 34 1 0.0 DSL 1.0 ... 1.0 0.0 0.0 0.0 One year 0 Mailed check 56.95 1889.50 0
2 326527 3668-QPYBK 0 0 0 2 1 0.0 DSL 1.0 ... 0.0 0.0 0.0 0.0 Month-to-month 1 Mailed check 53.85 108.15 1
3 845894 7795-CFOCW 0 0 0 45 0 NaN DSL 1.0 ... 1.0 1.0 0.0 0.0 One year 0 Bank transfer (automatic) 42.30 1840.75 0
4 503388 9237-HQITU 0 0 0 2 1 0.0 Fiber optic 0.0 ... 0.0 0.0 0.0 0.0 Month-to-month 1 Electronic check 70.70 151.65 1
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
7038 810338 6840-RESVB 0 1 1 24 1 1.0 DSL 1.0 ... 1.0 1.0 1.0 1.0 One year 1 Mailed check 84.80 1990.50 0
7039 230811 2234-XADUH 0 1 1 72 1 1.0 Fiber optic 0.0 ... 1.0 0.0 1.0 1.0 One year 1 Credit card (automatic) 103.20 7362.90 0
7040 155157 4801-JZAZL 0 1 1 11 0 NaN DSL 1.0 ... 0.0 0.0 0.0 0.0 Month-to-month 1 Electronic check 29.60 346.45 0
7041 731782 8361-LTMKD 1 1 0 4 1 1.0 Fiber optic 0.0 ... 0.0 0.0 0.0 0.0 Month-to-month 1 Mailed check 74.40 306.60 1
7042 353947 3186-AJIEK 0 0 0 66 1 0.0 Fiber optic 1.0 ... 1.0 1.0 1.0 1.0 Two year 1 Bank transfer (automatic) 105.65 6844.50 0

7043 rows × 21 columns

Das Umwandeln der Vertragsarten und der Bezahlmethode sind komplexer. Deswegen überspringen wir das für diese Übung und löschen die Werte einfach.

[8]:
df.drop(columns=["Payment Method", "Contract", "Internet Service"], inplace=True)
df
[8]:
LoyaltyID Customer ID Senior Citizen Partner Dependents Tenure Phone Service Multiple Lines Online Security Online Backup Device Protection Tech Support Streaming TV Streaming Movies Paperless Billing Monthly Charges Total Charges Churn
0 318537 7590-VHVEG 0 1 0 1 0 NaN 0.0 1.0 0.0 0.0 0.0 0.0 1 29.85 29.85 0
1 152148 5575-GNVDE 0 0 0 34 1 0.0 1.0 0.0 1.0 0.0 0.0 0.0 0 56.95 1889.50 0
2 326527 3668-QPYBK 0 0 0 2 1 0.0 1.0 1.0 0.0 0.0 0.0 0.0 1 53.85 108.15 1
3 845894 7795-CFOCW 0 0 0 45 0 NaN 1.0 0.0 1.0 1.0 0.0 0.0 0 42.30 1840.75 0
4 503388 9237-HQITU 0 0 0 2 1 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1 70.70 151.65 1
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
7038 810338 6840-RESVB 0 1 1 24 1 1.0 1.0 0.0 1.0 1.0 1.0 1.0 1 84.80 1990.50 0
7039 230811 2234-XADUH 0 1 1 72 1 1.0 0.0 1.0 1.0 0.0 1.0 1.0 1 103.20 7362.90 0
7040 155157 4801-JZAZL 0 1 1 11 0 NaN 1.0 0.0 0.0 0.0 0.0 0.0 1 29.60 346.45 0
7041 731782 8361-LTMKD 1 1 0 4 1 1.0 0.0 0.0 0.0 0.0 0.0 0.0 1 74.40 306.60 1
7042 353947 3186-AJIEK 0 0 0 66 1 0.0 1.0 0.0 1.0 1.0 1.0 1.0 1 105.65 6844.50 0

7043 rows × 18 columns

Wahrscheinlichkeit, dass ein Kunde abspringt

Das Modul pandas stellt eine Reihe von deskriptiven Analysewerkzeugen zur Verfügung, bspw. die Berechnung von Mittelwerten, Varianzen und Standardabweichungen einzelner Spalten. Weiterführende Informationen dazu sind in der Dokumentation von pandas zu deskriptiven Statistiken zu finden.

  1. Führen Sie die nachfolgende Anweisung aus und interpretieren Sie das Ergebnis. Was hat es mit der empirischen Wahrscheinlichkeit zu tun, dass ein Kunde abspringt?

Antwort:

[9]:
df['Churn'].mean()
[9]:
0.2653698707936959

Eine ML-basierte Prognose

Nun soll ein Algorithmus entwickelt werden, der vorhersagt, ob ein bestimmter Kunde abwandern wird. Im nächsten Schritt teilen wir die Daten auf: X enthält die Eingabewerte, die uns etwas über das Ergebnis verraten sollen, und y enthält das Ziel, was wir erlernen wollen. Die Spalte y wollen wir in Zukunft mit ML prognostizieren können.

[10]:
X = df.drop(
    ['Customer ID', 'LoyaltyID', 'Churn'],  # Nenne alle Spalten, die nicht zum Lernen verwendet werden dürfen/sollen
    axis=1
).values
y = df['Churn'].values  # Das zu erlernene Ziel

Nun erstellen wir einen Entscheidungsbaum. Weitere Infos hier finden Sie in der Dokumentation von scikit-learn.

[11]:
from sklearn import tree
clf_dt = tree.DecisionTreeClassifier(max_depth=3)

In einem nächsten Schritt wird der Entscheidungsbaum anhand der Trainingsdaten trainiert. Dabei erlernt er den (bzw. einen) Zusammenhang zwischen X und y.

[12]:
clf_dt.fit(X, y)
[12]:
DecisionTreeClassifier(max_depth=3)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

Immer, wenn Daten fehlen (NaN steht für Not a Number und wird in diesem Kontext genutzt), hat der Entscheidungsbaum übrigens eine ganz eigene Strategie, die unter https://scikit-learn.org/stable/modules/tree.html#missing-values-support beschrieben wird.

Jetzt, da der Entscheidungsbaum fertig ist, sollten wir überprüfen, in wie viel Prozet der Fälle der Entscheidungsbaum richtig liegt.

[13]:
clf_dt.score(X, y)
[13]:
0.783046997018316
[14]:
print("Wahrscheinlichkeit, dass der Kunde abwandert", df['Churn'].mean() * 100, "%")
print("Wahrscheinlichkeit, dass der Kunde nicht abwandert", (1 - df['Churn'].mean()) * 100, "%")
Wahrscheinlichkeit, dass der Kunde abwandert 26.536987079369588 %
Wahrscheinlichkeit, dass der Kunde nicht abwandert 73.4630129206304 %
  1. Setzen Sie das Ergebnis in den Kontext, was wir bereits allein aufgrund des arithmetischen Mittels wissen. Würden Sie dieses Modell in der Praxis einsetzen?

Antwort:

Der folgende Code visualisierung den Entscheidungsbaum:

[15]:
fig, ax = plt.subplots(figsize=(20, 10))
tree.plot_tree(
    clf_dt,
    ax=ax,
    feature_names=df.drop(['Customer ID', 'LoyaltyID', 'Churn'], axis=1).columns
)
plt.show()
../_images/02-erste-ml-experimente_01-Kundenabwanderung_35_0.svg

Die Boxen in der untersten Zeile stellen die Blätter da, die anderen Boxen sind die Knoten des Baumes. In diesen Boxen ist die jeweils oberste Zeile die Bedingung, die überprüft wird. Ist die Bedingung wahr, so wird der linke Pfad gewählt und ist die Bedigung falsch, so wird der rechte Pfad gewählt.

  • Die Zeile mit dem gini-Wert gibt die Verteilung von abgewanderten zu beim Kunden verbliebenen Kunden beim jeweiligen Knoten bzw. Blatt an.

  • Die Zeile mit dem sample-Wert gibt an, wie viele Zeilen der Daten (ohne Testdaten) den jeweiligen Knoten bzw. das jeweilige Blatt erreicht haben.

  • Die Zeile mit dem value-Wert gibt an, wie viele Passagiere (ohne Testdaten) des jeweiligen Knotens bzw. Blattes als Kunden geblieben sind (1. Wert) bzw. abgewandert sind (2. Wert).

  • Bei den Blättern gibt der größere value-Wert an, welche Prognose der Algorithmus für neue Daten trifft.

  1. Ist diese Art von Entscheidungsbaum zu erwarten gewesen? Gibt es Unterscheidungen und Prognosen, die überraschend sind? Woran liegt dies?

Antwort:

Creative Commons Lizenzvertrag     Dieses Werk von Marvin Kastner ist lizenziert unter einer Creative Commons Namensnennung 4.0 International Lizenz.