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.
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
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.
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.
DecisionTreeClassifier(max_depth=3)
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 %
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()
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.
Ist diese Art von Entscheidungsbaum zu erwarten gewesen? Gibt es Unterscheidungen und Prognosen, die überraschend sind? Woran liegt dies?
Antwort: …
Dieses Werk von Marvin Kastner ist lizenziert unter einer Creative Commons Namensnennung 4.0 International Lizenz.