Анализ оттока клиентов банка¶
Описание проекта
Заказчики — менеджмент банка «Метанпром». Банк располагается в Ярославле и областных городах: Ростов Великий и Рыбинск.
Цель исследования — проанализировать клиентов банка и выделить сегменты клиентов, которые склонны уходить из банка.
Содержание:
- Исследовательский анализ
- Анализ сегментов с высокой отточностью
- Проверка статистических гипотез
- Презентация
Описание данных
USERID
— идентификатор пользователя,score
— баллы кредитного скоринга,city
— город,gender
— пол,age
— возраст,equity
— количество баллов собственностиbalance
— баланс на счёте,products
— количество продуктов, которыми пользуется клиент,credit_card
— есть ли кредитная карта,last_activity
— активный клиент,EST_SALARY
— оценочный доход клиента,сhurn
— признак оттока.
Общая информация о данных¶
# импорт библиотек
import pandas as pd
import matplotlib.pyplot as plt
from IPython.display import display, Markdown
import os
import seaborn as sns
import numpy as np
import matplotlib.ticker as mticker
import sys
import scipy.stats as st
from statsmodels.stats.proportion import proportions_ztest
try:
import phik
except ImportError:
!"{sys.executable}" -m pip install phik
import phik
# проверка на возможность выполнения Markdown
if 'ipykernel' in sys.modules:
flag_md = 1
else:
flag_md = 0
# чтение файла с данными и сохранение в датафрейм
dir1 = '/datasets/'
dir2 = '/Users/alexslobodskoj/Data_Analyst/'
places_pth = 'bank_scrooge.csv'
# создание датафрейма с данными
if os.path.exists(dir1):
df_bank = pd.read_csv(dir1 + places_pth)
elif os.path.exists(dir2):
df_bank = pd.read_csv(dir2 + places_pth)
else:
print('Something is wrong')
# датафрейм о клиентах банка
# вывод первых 5 строчек
display(df_bank.head())
# вывод основной информации
df_bank.info()
USERID | score | city | gender | age | equity | balance | products | credit_card | last_activity | EST_SALARY | churn | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 183012 | 850.0 | Рыбинск | Ж | 25.0 | 1 | 59214.82 | 2 | 0 | 1 | 75719.14 | 1 |
1 | 146556 | 861.0 | Рыбинск | Ж | 37.0 | 5 | 850594.33 | 3 | 1 | 0 | 86621.77 | 0 |
2 | 120722 | 892.0 | Рыбинск | Ж | 30.0 | 0 | NaN | 1 | 1 | 1 | 107683.34 | 0 |
3 | 225363 | 866.0 | Ярославль | Ж | 51.0 | 5 | 1524746.26 | 2 | 0 | 1 | 174423.53 | 1 |
4 | 157978 | 730.0 | Ярославль | М | 34.0 | 5 | 174.00 | 1 | 1 | 0 | 67353.16 | 1 |
<class 'pandas.core.frame.DataFrame'> RangeIndex: 10000 entries, 0 to 9999 Data columns (total 12 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 USERID 10000 non-null int64 1 score 10000 non-null float64 2 city 10000 non-null object 3 gender 10000 non-null object 4 age 9974 non-null float64 5 equity 10000 non-null int64 6 balance 7705 non-null float64 7 products 10000 non-null int64 8 credit_card 10000 non-null int64 9 last_activity 10000 non-null int64 10 EST_SALARY 10000 non-null float64 11 churn 10000 non-null int64 dtypes: float64(4), int64(6), object(2) memory usage: 937.6+ KB
# распределение основных числовых характеристик
# задание размера области графиков
plt.figure(figsize=(12, 8))
# гистограмма для столбца 'score'
plt.subplot(2, 2, 1)
sns.histplot(df_bank['score'])
plt.title('Распределение клиентов по кредитному скорингу')
plt.ylabel('Количество')
plt.xlabel('Кредитный скоринг')
# гистограмма для столбца 'age'
plt.subplot(2, 2, 2)
sns.histplot(df_bank['age'])
plt.title('Распределение клиентов возрасту')
plt.ylabel('Количество')
plt.xlabel('Возраст')
# гистограмма для столбца 'balance'
plt.subplot(2, 2, 3)
sns.histplot(df_bank['balance'])
plt.title('Распределение клиентов по балансу')
plt.ylabel('Количество')
plt.xlabel('Баланс на счете')
# гистограмма для столбца 'EST_SALARY'
plt.subplot(2, 2, 4)
sns.histplot(df_bank['EST_SALARY'])
plt.title('Распределение клиентов по оценочному доходу')
plt.ylabel('Количество')
plt.xlabel('Оценочный доход')
plt.tight_layout()
plt.show()
Выводы:
В таблице данные о 10000 клиентах
Необходимо переименовать столбцы (нижний регистр)
Необходимо изменить тип столбцов:
age
иscore
на целочисленный,credit_card
,last_activity
,churn
на булевый
Таблицу необходимо проверить на наличие дубликатов
В столбцах
balance
,EST_SALARY
есть выбросыВ таблице есть пропущенные значения - нужно определить возможность их заполнения/исключения из анализа
Предобработка данных¶
Переименование столбцов¶
df_bank.columns = [x.lower() for x in df_bank.columns.values]
Изменение типа столбцов¶
Изменим тип столбцов age
и score
на целочисленный.
# запись названий столбцов в список
columns_to_convert = ['age', 'score']
# изменение типа данных в столбцах
for col in columns_to_convert:
df_bank[col] = \
(
df_bank[col]
.astype(pd.Int64Dtype())
)
Изменим тип столбцов credit_card
, last_activity
, churn
на булевый.
# запись названий столбцов в список
columns_to_convert = ['credit_card', 'last_activity', 'churn']
# изменение типа данных в столбцах
for col in columns_to_convert:
df_bank[col] = \
(
df_bank[col]
.astype(bool)
)
Изменим тип столбцов gender
, city
на категориальный.
# запись названий столбцов в список
columns_to_convert = ['gender', 'city']
# изменение типа данных в столбцах
for col in columns_to_convert:
df_bank[col] = \
(
df_bank[col]
.astype('category')
)
Проверка наличия дубликатов¶
Проверим наличие явных дубликатов
# вывод названий таблиц и количество явных дубликатов в них
message = (
f'Удалим **{df_bank.duplicated().sum()}**'
f' явных дубликатов строк'
) if df_bank.duplicated().sum() > 0 else f'Нет явных дубликатов в таблице'
if flag_md == 1:
display(Markdown(message))
else:
print(message.replace("*", ""))
Нет явных дубликатов в таблице
Проверим наличие неявных дубликатов в столбцах city
и gender
# запись названий столбцов в список
columns_to_convert = ['city', 'gender']
# # вывод значений каждого столбца
for col in columns_to_convert:
display(df_bank[col].value_counts(sort=True, ascending=False, dropna=True))
print()
city Ярославль 5888 Рыбинск 2695 Ростов 1417 Name: count, dtype: int64
gender М 5005 Ж 4995 Name: count, dtype: int64
Неявных дубликатов в столбцах city
и gender
не обнаружено.
Проверим наличие дубликатов userid
в одном городе.
# поиск дублей
df_agg = \
(
df_bank
.groupby(['userid', 'city'])
.size()
.reset_index(name='count')
.query('count > 1')
)
# проверка на дубли
if (df_agg['count'] > 1).any():
message = (
f'Есть **{len(df_agg)}** дубликатов `userid` в одном городе.\n\n'
f'Оставим первое вхождение дубликата для дальнейшего анализа.'
)
else:
message = (
f'Нет дубликатов `userid` в одном городе.'
)
# вывод сообщения про дубликаты
if flag_md == 1:
display(Markdown(message))
else:
print(message.replace("*", ""))
Нет дубликатов userid
в одном городе.
Обработка пропущенных значений¶
Рассчитаем процент пропущенных значений по филиалам банка.
# вывод информации о доле пропущенных значений
for city in df_bank['city'].sort_values(ascending=False).unique():
city_data = df_bank.query('city == @city')
na_series = city_data.isna().sum() > 0
df_na = pd.DataFrame(city_data.isna().mean()[na_series], columns = [city])
display(df_na.style.format("{:.2%}").background_gradient('coolwarm'))
Ярославль | |
---|---|
age | 0.22% |
balance | 24.92% |
Рыбинск | |
---|---|
age | 0.45% |
balance | 22.00% |
Ростов | |
---|---|
age | 0.07% |
balance | 16.58% |
Доля пропусков по филиалам примерно одинаковая, учитывая размеры городов.
Рассчитаем процент пропущенных значений по ушедшим/оставшимся клиентам.
# вывод информации о доле пропущенных значений
for churn in df_bank['churn'].unique():
churn_data = df_bank.query('churn == @churn')
na_series = churn_data.isna().sum() > 0
df_na = pd.DataFrame(
churn_data.isna()
.mean()[na_series],
columns = ["Ушедшие" if churn == True else "Оставшиеся"])
display(df_na.style.format("{:.2%}").background_gradient('coolwarm'))
Ушедшие | |
---|---|
age | 0.05% |
balance | 0.71% |
Оставшиеся | |
---|---|
age | 0.31% |
balance | 27.90% |
Пропуски не связаны с признаком отточности клиентов.
Заполнение пропусков нулевыми или другими значениями может исказить результаты исследования. Оставим пропуски без изменения.
Выводы:
Переименовали названия столбцов в нижний регистр
Изменили тип столбцов
age
иscore
на целочисленный, столбцовcredit_card
,last_activity
,churn
на булевыйЯвные и неявные дубликаты в таблицах не обнаружены
Обработали пропущенные значения
Исследовательский анализ данных¶
Цель: выявить потенциальные сегменты клиентов, склоных к уходу из банка.
Распределение клиентов по признаку оттока¶
Оценим числовое и графическое распределение отточности клиентов в целом по банку.
# группировка по признаку оттока
df_agg = df_bank.groupby('churn', as_index=False).size()
# функция для отображения подписей
def label_pct(pct, allusers):
absolute = round(pct / 100 * allusers.sum())
return f'Количество: {absolute}\n доля: {pct:.0f}%'
# вывод круговой диаграммы
plt.figure(figsize=(12, 6))
plt.pie(
df_agg['size'],
autopct=lambda pct: label_pct(pct, df_agg['size']),
labels=[
f'{"Ушедшие" if group == True else "Оставшиеся"}'
for group in df_agg['churn']
],
startangle=90,
textprops={'fontsize': 10},
counterclock=False,
wedgeprops={'alpha': .75}
)
plt.title('Соотношение ушедших и оставшихся клиентов')
plt.axis('equal')
plt.tight_layout()
plt.show()
Выводы:
Оставшихся клиентов: 82%
Ушедших клиентов: 18%
Анализ баллов кредитного скоринга¶
Оценим распределение баллов кредитного скоринга в числовом виде.
df_bank['score'].describe()
count 10000.0 mean 848.6994 std 65.448519 min 642.0 25% 802.0 50% 853.0 75% 900.0 max 1000.0 Name: score, dtype: Float64
Оценим графически изменение вероятности ухода клиентов в зависимости от их баллов кредитного скоринга.
# задание размера графика
plt.figure(figsize=(12, 6))
# вывод графика
sns.histplot(
df_bank,
x="score",
hue="churn",
edgecolor=".3",
binwidth=10,
linewidth=.5,
stat='density',
common_norm=False,
shrink=.9,
alpha=.75
)
plt.legend(
title='',
loc='upper right',
labels=[
f'{"Ушедшие" if group == True else "Оставшиеся"}'
for group in df_bank['churn'].unique()
]
)
plt.title('Распределение отточности клиентов по кредитному скорингу')
plt.ylabel('Плотность вероятности')
plt.xlabel('Баллы скоринга')
plt.tight_layout()
plt.show()
Выводы
Баллы кредитного скоринга распределены в пределах от 642 до 1000
Больше всего клиентов с кредитным скорингом около 900
Потенциальный сегмент клиентов склонных к уходу: имеющие баллы кредитного скоринга от 830 до 900
Анализ количества продуктов в использовании¶
Оценим графически изменение вероятности ухода клиентов при увеличении количества используемых продуктов банка.
# задание размера графика
plt.figure(figsize=(12, 6))
# вывод графика
sns.histplot(
df_bank,
x="products",
hue="churn",
edgecolor=".3",
linewidth=.5,
binwidth=1,
discrete=True,
stat='density',
common_norm=False,
shrink=.9,
alpha=.75
)
plt.legend(
title='',
labels=[
f'{"Ушедшие" if group == True else "Оставшиеся"}'
for group in df_bank['churn'].unique()
]
)
plt.title('Распределение отточности клиентов по количеству продуктов банка')
plt.ylabel('Плотность вероятности')
plt.xlabel('Количество продуктов')
plt.tight_layout()
plt.show()
Выводы:
Чаще всего клиенты используют 1 или 2 продукта банка
С увеличением количества используемых продуктов увеличивается доля ушедших клиентов
Возможно ушедшие клиенты хотели получить дополнительные услуги, но их состав/качество оказалось неудовлетворительным
Потенциальный сегмент клиентов, склонных к уходу: использовавшие от 3-х продуктов банка
Анализ клиентов по городам¶
Оценим числовое и графическое распределение клиентов по филиалам.
# группировка по городам
df_agg = df_bank.groupby('city', as_index=False).size()
# функция для отображения подписей
def label_pct(pct, allusers):
absolute = round(pct / 100 * allusers.sum())
return f'Количество: {absolute}\n доля: {pct:.0f}%'
# вывод круговой диаграммы
colors = sns.color_palette("Set2")
plt.figure(figsize=(12, 6))
plt.pie(
df_agg['size'],
autopct=lambda pct: label_pct(pct, df_agg['size']),
labels=df_agg['city'],
startangle=90,
colors=colors,
textprops={'fontsize': 10},
counterclock=False
)
plt.title('Распределение клиентов по городам')
plt.axis('equal')
plt.tight_layout()
plt.show()
Оценим графически изменение вероятности ухода клиентов в зависимости от филиала банка.
# задание размера графика
plt.figure(figsize=(12, 6))
# вывод графика
ax = sns.histplot(
df_bank,
x=pd.Categorical(
df_bank['city'],
categories=df_bank['city'].value_counts().index,
ordered=True
),
hue="churn",
edgecolor=".3",
linewidth=.5,
discrete=True,
stat='density',
common_norm=False,
shrink=.9,
alpha=.75
)
plt.legend(
title='',
labels=[
f'{"Ушедшие" if group == True else "Оставшиеся"}'
for group in df_bank['churn'].unique()
]
)
plt.title('Распределение отточности клиентов по городам')
plt.ylabel('Плотность вероятности')
plt.xlabel('Город')
plt.tight_layout()
plt.show()
Вывод:
Больше всего клиентов из Ярославля 59%, затем из Рыбинска 27% и Ростова 14%
Влияние города клиента на потенциальную возможность оказа от услуг банка незначительное.
Анализ пола и возраста клиентов¶
Оценим графически распределение клиентов по полу и возрасту.
# задание размера графика
plt.figure(figsize=(12, 6))
# вывод графика
palette = sns.color_palette("Set2",n_colors=2)
sns.histplot(
df_bank,
x="age",
hue="gender",
multiple="dodge",
edgecolor=".3",
linewidth=.5,
palette=palette,
shrink=.8,
alpha=1,
binwidth=5,
)
plt.legend(
title="",
loc='upper right',
labels=['Мужчины', 'Женщины']
)
plt.title('Распределение клиентов по полу и возрасту')
plt.ylabel('Количество клиентов')
plt.xlabel('Возраст')
plt.tight_layout()
plt.show()
Оценим графически изменение вероятности ухода клиента в зависимости от его пола.
# задание размера графика
plt.figure(figsize=(12, 6))
# вывод графика
sns.histplot(
df_bank,
x="gender",
hue="churn",
edgecolor=".3",
linewidth=.5,
discrete=True,
stat='density',
common_norm=False,
shrink=.9,
alpha=.75
)
plt.legend(
title='',
loc='upper left',
labels=[
f'{"Ушедшие" if group == True else "Оставшиеся"}'
for group in df_bank['churn'].unique()
]
)
plt.title('Распределение отточности клиентов по полу клиентов')
plt.ylabel('Плотность вероятности')
plt.xlabel('Пол')
plt.xticks(ticks=[0, 1], labels=['Женщины', 'Мужчины'])
plt.tight_layout()
plt.show()
Оценим графически изменение вероятности ухода клиента в зависимости от его возраста.
# задание размера графика
plt.figure(figsize=(12, 6))
# вывод графика - 1 столбик = 1 год
sns.histplot(
df_bank,
x="age",
hue="churn",
edgecolor=".3",
linewidth=.5,
binwidth=1,
stat='density',
common_norm=False,
shrink=.8,
alpha=.75
)
plt.legend(
title='',
labels=[
f'{"Ушедшие" if group == True else "Оставшиеся"}'
for group in df_bank['churn'].unique()
]
)
plt.title('Распределение отточности клиентов по возрасту')
plt.ylabel('Плотность вероятности')
plt.xlabel('Возраст')
plt.tight_layout()
plt.show()
Вывод
До 50 лет больше клиентов - мужчин, а старше 50 лет больше клиентов - женщин
Потенциально мужчины более склонны к отказу от услуг банка
Потенциальные возрастные сегменты клиентов, склонных к уходу:
- 26 - 30 лет
- 32, 33, 35 лет
- 52 - 61 лет
Анализ количества баллов собственности¶
Оценим графически изменение вероятности ухода клиента в зависимости от наличия собственности.
# задание размера графика
plt.figure(figsize=(12, 6))
# вывод графика - 1 столбик = 1 год
sns.histplot(
df_bank,
x="equity",
hue="churn",
edgecolor=".3",
linewidth=.5,
stat='density',
discrete=True,
common_norm=False,
shrink=.8,
alpha=.75
)
plt.legend(
title='',
labels=[
f'{"Ушедшие" if group == True else "Оставшиеся"}'
for group in df_bank['churn'].unique()
]
)
plt.title('Распределение отточности клиентов по баллам собственности')
plt.ylabel('Плотность вероятности')
plt.xlabel('Баллы')
plt.xticks(df_bank['equity'].unique())
plt.tight_layout()
plt.show()
Выводы:
Больше всего у клиентов 0 или 5 баллов, оценивающих наличие собственности
Потенциальный сегмент клиентов, склонных к уходу: от 4-х баллов собственности
Анализ баланса на счете¶
Оценим графически распределение баланса на счете у клиентов.
# вывод графика распределения среднего чека
plt.figure(figsize=(12, 4))
sns.boxplot(
data=df_bank,
x='balance',
linewidth=.7
)
plt.title('Распределение величины баланса')
plt.ylabel('')
plt.xlabel('Баланс')
plt.tight_layout()
plt.show()
Оценим графически изменение вероятности ухода клиента в зависимости от баланса на счете. Для более точной оценки исключим отображение выбросов на графике.
# задание размера графика
plt.figure(figsize=(12, 6))
# вывод графика
sns.histplot(
df_bank,
x="balance",
hue="churn",
edgecolor=".3",
linewidth=.5,
binwidth=100000,
stat='density',
common_norm=False,
shrink=.8,
alpha=.75
)
plt.legend(
title='',
loc='upper right',
labels=[
f'{"Ушедшие" if group == True else "Оставшиеся"}'
for group in df_bank['churn'].unique()
]
)
plt.title('Распределение отточности клиентов по балансу на счете')
plt.ylabel('Плотность вероятности')
plt.xlabel('Баланс, млн руб.')
plt.xlim(0,5000000)
plt.tight_layout()
plt.show()
Выводы:
Чаще всего у клиентов на счете 300-500 тыс. руб.
Потенциальный сегмент клиентов, склонных к уходу: от 1,2 до 4 млн руб. на счете
Анализ наличия кредитной карты¶
Оценим графически изменение вероятности ухода клиента в зависимости от наличия кредитной карты.
# задание размера графика
plt.figure(figsize=(12, 6))
# вывод графика
sns.histplot(
df_bank,
x='credit_card',
hue='churn',
edgecolor=".3",
linewidth=.5,
discrete=True,
stat='density',
common_norm=False,
shrink=.9,
alpha=.75
)
plt.legend(
title='',
loc='upper left',
labels=[
f'{"Ушедшие" if group == True else "Оставшиеся"}'
for group in df_bank['churn'].unique()
]
)
plt.title('Распределение отточности клиентов по наличию кредитной карты')
plt.ylabel('Плотность вероятности')
plt.xlabel('')
plt.xticks(ticks=[0, 1], labels=['Нет кредитной карты', 'Есть кредитная карта'])
plt.tight_layout()
plt.show()
Вывод:
Клиентов с кредитной картой больше
Потенциальный сегмент клиентов, склонных к уходу: без наличия кредитной карты
Анализ активности клиентов¶
Оценим графически изменение вероятности ухода клиента в зависимости от активности клиентов.
# задание размера графика
plt.figure(figsize=(12, 6))
# вывод графика
sns.histplot(
df_bank,
x='last_activity',
hue="churn",
edgecolor=".3",
linewidth=.5,
discrete=True,
stat='density',
common_norm=False,
shrink=.9,
alpha=.75
)
plt.legend(
title='',
loc='upper left',
labels=[
f'{"Ушедшие" if group == True else "Оставшиеся"}'
for group in df_bank['churn'].unique()
]
)
plt.title('Распределение отточности клиентов по их активности')
plt.ylabel('Плотность вероятности')
plt.xlabel('')
plt.xticks(ticks=[0, 1], labels=['Неактивные клиенты', 'Активные клиенты'])
plt.tight_layout()
plt.show()
Выводы:
Больше активных клиентов
Потенциальный сегмент клиентов, склонных к уходу: активные клиенты
Анализ оценочного дохода¶
Оценим графически распределение дохода клиентов.
# вывод графика распределения среднего чека
plt.figure(figsize=(12, 4))
sns.boxplot(
data=df_bank,
x='est_salary',
linewidth=.7
)
plt.title('Распределение величины дохода')
plt.ylabel('')
plt.xlabel('Доход')
plt.tight_layout()
plt.show()
Оценим графически изменение вероятности ухода клиента в зависимости от величины оценочного дохода клиентов. Для более точной оценки исключим отображение выбросов на графике.
# задание размера графика
plt.figure(figsize=(12, 6))
# вывод графика
sns.histplot(
df_bank,
x="est_salary",
hue="churn",
edgecolor=".3",
linewidth=.5,
binwidth=10000,
stat='density',
common_norm=False,
shrink=.8,
alpha=.75
)
plt.legend(
title='',
loc='upper right',
labels=[
f'{"Ушедшие" if group == True else "Оставшиеся"}'
for group in df_bank['churn'].unique()
]
)
plt.title('Распределение отточности клиентов по оценочному доходу')
plt.ylabel('Плотность вероятности')
plt.xlabel('Доход, руб.')
plt.xlim(0,400000)
plt.tight_layout()
plt.show()
Выводы:
Чаще всего доход клиентов оценивается в 100 тыс. руб.
Потенциальный сегмент клиентов, склонных к уходу: с доходом от 100 до 210 тыс. руб.
Матрица корреляции между характеристками клиентов¶
Оценим взаимосвязь характеристик с попомощью коэффициента корреляции phi_k
, адаптированного к учету категориальных, порядковых и интервальных переменных. Корреляцию характеристик покажем на тепловой карте.
# подготовка датафрейма
df_phik = df_bank.drop(columns=['userid'])
# указание столбцов с интервальными переменными
interval_cols = ['score', 'age', 'balance', 'est_salary']
# формирование матрицы корреляции
phik_corr = df_phik.phik_matrix(interval_cols=interval_cols)
# задание размера графика
plt.figure(figsize=(12, 6))
# скрытие верхнего трегольника
mask = np.triu(np.ones_like(phik_corr, dtype=bool))
# вывод графика
ax = sns.heatmap(
data=phik_corr,
vmax=1,
annot=True,
mask=mask,
cmap='coolwarm',
center=0,
linewidths=.5,
fmt=".2f",
annot_kws={'size':12},
xticklabels=[
'Скоринг',
'Город',
'Пол',
'Возраст',
'Собственность',
'Баланс',
'Продукты',
'Карта',
'Активность',
'Доход'],
yticklabels=[
'',
'Город',
'Пол',
'Возраст',
'Собственность',
'Баланс',
'Продукты',
'Карта',
'Активность',
'Доход',
'Отток'],
)
plt.title('Тепловая карта корреляции характеристик')
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
Выводы по умеренной положительной корреляции между характеристиками:
Отток клиентов связан с количеством используемых банковских продуктов и наличием собственности
Кредитный скоринг клиента связан с наличием собственности и оценочным уровень дохода
Количество используемых продуктов связано с наличием собственности и кредитной карты
Уровень баланса связан с доходом клиента, который связан с возрастом
Анализ сегментов с высокой отточностью¶
Критерий: от 500 клиентов, доля ушедших от 30%.
Создадим функцию для расчета высокоотточных сегментов.
# создание функции для расчета сегментов
def segment_churn(unique_values, interval_value):
results = []
# Перебор характеристик
for feature1 in unique_values.keys():
for feature2 in unique_values.keys():
if feature1 != feature2:
if feature1 in interval_value:
query_feature1 = (
f"({unique_values[feature1][0]} <= {feature1} "
f"<= {unique_values[feature1][1]})"
)
else:
query_feature1 = f"({feature1} == {repr(unique_values[feature1])})"
if feature2 in interval_value:
query_feature2 = (
f"({unique_values[feature2][0]} <= {feature2} "
f"<= {unique_values[feature2][1]})"
)
else:
query_feature2 = f"({feature2} == {repr(unique_values[feature2])})"
query_string = f"{query_feature1} & {query_feature2}"
query_string = query_string.replace("age1", "age").replace("age2", "age")
filtered_df = df_bank.query(query_string)
total_clients = len(filtered_df)
churn_clients = filtered_df['churn'].sum()
churn_rate = filtered_df['churn'].mean() if total_clients > 0 else None
results.append({
'feature1': f"{feature1} {unique_values[feature1]}",
'feature2': f"{feature2} {unique_values[feature2]}",
'total_clients': total_clients,
'churn_clients': churn_clients,
'churn_rate': churn_rate
})
# формирование датафрейма сегментов
results_df = pd.DataFrame(results)
results_df = \
(
results_df
.drop_duplicates(subset=['total_clients', 'churn_rate'])
.query('(500 <= total_clients) & (churn_rate >= 0.3)')
.sort_values(by='churn_rate', ascending=False)
)
results_df['churn_rate'] = round(results_df['churn_rate'], 2)
pd.set_option('display.max_colwidth', 30)
# вывод Топ-10 сегментов по количеству ушедших клиентов
message = (
f"\n\n**Топ-10 сегментов по доле ушедших клиентов**"
)
if flag_md == 1:
display(Markdown(message))
else:
print(message.replace("*", ""))
display(results_df.head(10))
Создадим словарь отточных интервалов характеристик, выявленных в результате исследовательского анализа. И сформируем таблицу отточных сегментов, отсортированную по убыванию количества ушедших клиентов.
# создание словаря интервалов характеристик с повышенным оттоком
unique_values = {
'score': (830, 900),
'products': (3,5),
'age1': (26, 35),
'age2': (52, 61),
'equity': (4, 9),
'balance': (1200000, 4000000),
'est_salary': (100000, 210000),
'credit_card': False,
'last_activity': True,
'gender': "М"
}
interval_value = ['score', 'products', 'age', 'age', 'equity', 'balance', 'est_salary']
# формирование высокоотточных сегментов
segment_churn(unique_values, interval_value)
Топ-10 сегментов по доле ушедших клиентов
feature1 | feature2 | total_clients | churn_clients | churn_rate | |
---|---|---|---|---|---|
52 | balance (1200000, 4000000) | last_activity True | 642 | 367 | 0.57 |
53 | balance (1200000, 4000000) | gender М | 657 | 350 | 0.53 |
17 | products (3, 5) | gender М | 752 | 376 | 0.50 |
51 | balance (1200000, 4000000) | credit_card False | 627 | 303 | 0.48 |
50 | balance (1200000, 4000000) | est_salary (100000, 210000) | 674 | 323 | 0.48 |
16 | products (3, 5) | last_activity True | 824 | 391 | 0.47 |
14 | products (3, 5) | est_salary (100000, 210000) | 659 | 304 | 0.46 |
12 | products (3, 5) | equity (4, 9) | 953 | 426 | 0.45 |
40 | equity (4, 9) | balance (1200000, 4000000) | 945 | 416 | 0.44 |
0 | score (830, 900) | products (3, 5) | 629 | 267 | 0.42 |
Выводы:
Топ-3 выскоотточных сегментов:
- Активные клиенты с балансом на счете от 1,2 до 4 млн руб.
- Мужчины с балансом на счете от 1,2 до 4 млн руб.
- Мужчины, использующие от 3 до 5 продуктов банка
Портрет типичного клиента, склонного к уходу из банка:
- С балансом на счете от 1,2 до 4 млн руб.
- Активно пользуется услугами банка
- Чаще мужчина, чем женщина
- Использующий от 3 до 5 продуктов банка
- Не имеет кредитной карты
- С доходом от 100 до 200 тыс руб.
Рекомендации по удержанию клиентов из высокоотточных сегментов:
- регулярно собирать отзывы клиентов для адаптации предложений и продуктов банка
- специальная ставка по депозитам - например, повышенный процент при открытии депозита на крупную сумму
- предлагать премиальные банковские карты с бонусными программами, кэшбэком за покупки в определенных категориях
- разработать специальные кредитные продукты для клиентов с высоким кредитным скорингом и не имеющих кредитной карты
- возможность быстро получить консультацию специалиста банка, не посещая отделение
- разработать маркетинговые кампании, ориентированные на их интересы, например, в области инвестиций или кредитования
Проверка статистических гипотез¶
Гипотеза о различии дохода между ушедшими и оставшимися клиентами¶
Сформулируем гипотезы:
H0 - нулевая гипотеза:
- различий в доходе между ушедшими и оставшимися клиентами нет
H1 - альтернативная двусторонняя гипотеза
- различия в доходе между ушедшими и оставшимися клиентами есть
Поскольку в величине дохода есть выбросы - проверим гипотезу с помощью двустороннего теста «U-критерия Манна-Уитни» для независимых выборок. Если вероятность ошибочно отвергнуть нулевую гипотезу p-value
окажется меньше уровня статистической значимости alpha = 5%
, то отвергнем нулевую гипотезу в пользу альтернативной.
Оценим графически распределение среднего дохода клиентов, например по количеству баллов собственности.
# задание размера графика
plt.figure(figsize=(12, 6))
# вывод графика
sns.barplot(
data=df_bank,
x="equity",
y="est_salary",
edgecolor=".3",
linewidth=.5,
hue='churn',
errorbar=None
)
plt.legend(
title='',
loc='upper right',
labels=[
f'{"Ушедшие" if group == True else "Оставшиеся"}'
for group in sorted(df_bank['churn'].unique())
]
)
plt.title('Распределение среднего дохода по баллам собственности')
plt.ylabel('Величина дохода')
plt.xlabel('Баллы собственности')
plt.tight_layout()
plt.show()
Визуальная оценка - средние доходы ушедших и оставшихся различаются. Проверим статистическую значимость различия.
# датафрейм ушедших клиентов
df_true = df_bank.query('churn == True')
# датафрейм оставшихся клиентов
df_false = df_bank.query('churn == False')
# задание уровня статистической значимости
alpha = 0.05
# выполнение u-теста с двусторонней альтернативной гипотезой
p_value = st.mannwhitneyu(
df_true['est_salary'],
df_false['est_salary'],
alternative='two-sided'
)[1]
# проверка гипотезы
if p_value < alpha:
message = (
f'p_value=**{p_value:.2e}** < {alpha:.2%}\n\n'
f' Отвергаем нулевую гипотезу: \
есть статистически значимые различия в доходе между ушедшими и оставшимися клиентами.'
)
else:
message = (
f'p_value=**{p_value:.2%}** >= {alpha:.2%}\n\n'
f' Не удалось отвергнуть нулевую гипотезу:\
нет статистически значимых различий в доходе между ушедшими и оставшимися клиентами.'
)
if flag_md == 1:
display(Markdown(message))
else:
print(message.replace("*", ""))
p_value=2.44e-04 < 5.00%
Отвергаем нулевую гипотезу: есть статистически значимые различия в доходе между ушедшими и оставшимися клиентами.
Вывод:
- Различия в доходе между ушедшими и оставшимися клиентами являются статистически значимыми
Гипотеза о разном возрасте ушедших и оставшихся клиентов¶
Сформулируем гипотезы:
H0 - нулевая гипотеза:
- различий в возрасте между ушедшими и оставшимися клиентами нет
H1 - альтернативная двусторонняя гипотеза
- различия в возрасте между ушедшими и оставшимися клиентами есть
Поскольку распределение возраста близко к нормальному и нет выбросов - проверим гипотезу с помощью Т-критерия Стьюдента для независимых выборок. Если вероятность ошибочно отвергнуть нулевую гипотезу p-value
окажется меньше уровня статистической значимости alpha = 5%
, то отвергнем нулевую гипотезу в пользу альтернативной.
Оценим графически распределение среднего возраста клиентов, например по количеству баллов собственности.
# задание размера графика
plt.figure(figsize=(12, 6))
# вывод графика
sns.barplot(
data=df_bank,
x="equity",
y="age",
edgecolor=".3",
linewidth=.5,
hue='churn',
errorbar=None
)
plt.legend(
title='',
loc='upper right',
labels=[
f'{"Ушедшие" if group == True else "Оставшиеся"}'
for group in sorted(df_bank['churn'].unique())
]
)
plt.title('Распределение среднего возраста по баллам собственности')
plt.ylabel('Возраст клиентов')
plt.xlabel('Баллы собственности')
plt.tight_layout()
plt.show()
Визуальная оценка - средний возраст ушедших и оставшихся различается. Проверим статистическую значимость различия.
# датафрейм ушедших клиентов
df_true = df_bank.query('churn == True').dropna(subset=['age'])
# датафрейм оставшихся клиентов
df_false = df_bank.query('churn == False').dropna(subset=['age'])
# задание уровня статистической значимости
alpha = 0.05
# выполнение t-теста с двусторонней альтернативной гипотезой
p_value = st.ttest_ind(
df_true['age'],
df_false['age'],
alternative='two-sided'
)[1]
# проверка гипотезы
if p_value < alpha:
message = (
f'p_value=**{p_value:.2e}** < {alpha:.2%}\n\n'
f' Отвергаем нулевую гипотезу: \
есть статистически значимые различия в возрасте между ушедшими и оставшимися клиентами.'
)
else:
message = (
f'p_value=**{p_value:.2%}** >= {alpha:.2%}\n\n'
f' Не удалось отвергнуть нулевую гипотезу:\
нет статистически значимых различий в возрасте между ушедшими и оставшимися клиентами.'
)
if flag_md == 1:
display(Markdown(message))
else:
print(message.replace("*", ""))
p_value=6.50e-07 < 5.00%
Отвергаем нулевую гипотезу: есть статистически значимые различия в возрасте между ушедшими и оставшимися клиентами.
Вывод:
- Различия в возрасте между ушедшими и оставшимися клиентами являются статистически значимыми
Гипотеза о влиянии активности на вероятность ухода клиентов¶
Сформулируем гипотезы:
H0 - нулевая гипотеза:
- нет разницы в пропорции ушедших клиентов между активными и неактивными клиентами
H1 - альтернативная двусторонняя гипотеза
- существует разница в пропорции ушедших клиентов между активными и неактивными клиентами
Проверим гипотезу с помощью Z-теста для пропорций. Если вероятность ошибочно отвергнуть нулевую гипотезу p-value
окажется меньше уровня статистической значимости alpha = 5%
, то отвергнем нулевую гипотезу в пользу альтернативной.
Оценим графически распределение доли ушедших клиентов, например по городам.
# задание размера графика
plt.figure(figsize=(12, 6))
# вывод графика
palette = sns.color_palette("Set2",n_colors=2)
sns.barplot(
data=df_bank,
x="city",
y="churn",
edgecolor=".3",
linewidth=.5,
hue='last_activity',
palette=palette,
errorbar=None
)
plt.legend(
title='',
loc='upper left',
labels=[
f'{"Активные" if group == True else "Неактивные"}'
for group in sorted(df_bank['churn'].unique())
]
)
plt.title('Распределение доли ушедших по городам')
plt.ylabel('Доля ушедших клиентов')
plt.xlabel('Город')
plt.tight_layout()
plt.show()
Визуальная оценка - доля ушедших клиентов для активных и неактивных клиентов различается. Проверим статистическую значимость различия.
# количество активных клиентов
trials1 = df_bank.query('last_activity == True')['userid'].count()
# количество ушедших и активных
successes1 = df_bank.query('churn == True & last_activity == True')['userid'].count()
# количество неактивных клиентов
trials2 = df_bank.query('last_activity == False')['userid'].count()
# количество ушедших и неактивных
successes2 = df_bank.query('churn == True & last_activity == False')['userid'].count()
# доля ушедших активных
p1 = successes1/trials1
# доля ушедших неактивных
p2 = successes2/trials2
# расчет вероятности
p_value = proportions_ztest([successes1,successes2],[trials1,trials2])[1]
# вывод информации о долях ушедших
message = (
f'В группе активных клиентов доля ушедших: **{p1:.2%}**\n\n'
f'В группе неактивных клиентов доля ушедших: **{p2:.2%}**\n'
)
# проверка гипотезы
message1 = (
f'p_value=**{p_value:.2e}** < {alpha:.2%}\n\n'
f' Отвергаем нулевую гипотезу: между долями есть статистически значимая разница.\n\n'
) if p_value < alpha else (
f'p_value=**{p_value:.2%}** >= {alpha:.2%}\n\n'
f'Не удалось отвергнуть нулевую гипотезу: '
f'нет статистически значимых оснований считать доли разными')
# проверка на использование Markdown
if flag_md == 1:
display(Markdown(message))
display(Markdown(message1))
else:
print(message.replace("*", ""))
print(message1.replace("*", ""))
В группе активных клиентов доля ушедших: 24.41%
В группе неактивных клиентов доля ушедших: 11.37%
p_value=6.67e-64 < 5.00%
Отвергаем нулевую гипотезу: между долями есть статистически значимая разница.
Вывод:
- Влияние активности клиентов на вероятность отказа от услуг банка является статистически значимой
Итоги исследования¶
Результаты исследования помогут менеджменту банка «Метанпром» сократить отток клиентов.
Презентация результатов исследования
В среднем по банку - 18% клиентов склонны к уходу из банка
Портрет типичного клиента, склонного к отказу от услуг банка:
- Наличие собственности - от 4 баллов
- Активно пользуется услугами банка
- Чаще мужчина, чем женщина
- С высоким кредитным рейтингом от 830
- Не имеет кредитной карты
- С доходом от 100 до 200 тыс руб.
Топ-3 выскоотточных сегментов:
- Активные клиенты с баллами собственности от 4
- Мужчины с баллами собственности от 4
- Активные клиенты с кредитным скорингом от 830
Дополнительные потенциальные сегменты клиентов, склонных к уходу:
- использовавшие от 3-х продуктов банка
- в возрасте: 26 - 35 лет и 52 - 61 лет
- с балансом на счете от 1,2 до 4 млн руб.
Существует умеренная положительная корреляция между характеристиками:
- отток клиентов связан с количеством используемых банковских продуктов и наличием собственности
- кредитный скоринг клиента связан с наличием собственности и оценочным уровень дохода
- количество используемых продуктов связано с наличием собственности и кредитной карты
- уровень баланса связан с доходом клиента, который связан с возрастом
Обнаружены статистически значимые различия:
- в доходе между ушедшими и оставшимися клиентами
- в возрасте между ушедшими и оставшимися клиентами
- в доле ушедших между активным и неактивными клиентоами
Рекомендации по удержанию клиентов:
- Регулярно собирать отзывы клиентов для адаптации предложений и продуктов банка
- Специальная ставка по депозитам - например, повышенный процент при открытии депозита на крупную сумму
- Предлагать премиальные банковские карты с бонусными программами, кэшбэком за покупки в определенных категориях
- Разработать специальные кредитные продукты для клиентов с высоким кредитным скорингом и не имеющих кредитной карты
- Возможность быстро получить консультацию специалиста банка, не посещая отделение
- Разработать маркетинговые кампании, ориентированные на их интересы, например, в области инвестиций или кредитования