<jupyter_start><jupyter_text>Исследование надёжности заёмщиков банка**Проектная работа №1 Яндекс.Практикум - Data Science**  Описание проектаЗаказчик — кредитный отдел банка. Нужно разобраться, влияет ли семейное положение и количество детей клиента на факт погашения кредита в срок.**Исходные данные:**Статистика о платёжеспособности клиентов и данные о клиентах.**Цели проекта:**Подготовить результаты исследования для построения модели кредитного скоринга.**Условия проекта:**Необходимо изучить и обработать полученные входные данные, и ответить на вопросы:* Есть ли зависимость между наличием детей и возвратом кредита в срок?* Есть ли зависимость между семейным положением и возвратом кредита в срок?* Есть ли зависимость между уровнем дохода и возвратом кредита в срок?* Как разные цели кредита влияют на его возврат в срок?  Структура проекта* 1. [Загрузка и изучение общей информации датасета](start)* 2. [Предобработка данных](preprocessing)    * 2.1 [Обработка пропущенных значений](handling_missing_values)    * 2.2 [Замена вещественного типа данных на целочисленный](float_to_int)    * 2.3 [Обработка дубликатов](duplicates)    * 2.4 [Выделение леммы в значениях столбца с целями получения кредита](lemma)    * 2.5 [Категоризация данных](categories)* 3. [Ответы на вопросы исследования](answers)    * 3.1 [Есть ли зависимость между наличием детей и возвратом кредита в срок?](answer_1)    * 3.2 [Есть ли зависимость между семейным положением и возвратом кредита в срок?](answer_2)    * 3.3 [Есть ли зависимость между уровнем дохода и возвратом кредита в срок?](answer_3)    * 3.4 [Как разные цели кредита влияют на его возврат в срок?](answer_4)* 4. [Общий вывод](conclusion)  1. Загрузка и изучение общей информации датасета  Импортируем необходимые библиотеки<jupyter_code>import pandas as pd
import sys
from pymystem3 import Mystem
from nltk.stem import SnowballStemmer
from collections import Counter<jupyter_output><empty_output><jupyter_text>Загрузим данные<jupyter_code>dataset = 'data.csv'

try:
    df = pd.read_csv(f'./datasets/{dataset}', sep=',')
    print(f'Прочитан файл с данными: "./datasets/{dataset}"')
except:
    try:
        df = pd.read_csv(f'/datasets/{dataset}', sep=',') # yandex.praktikum
        print(f'Прочитан файл с данными: "/datasets/{dataset}"')
    except Exception as err:
        print(repr(err))<jupyter_output>Прочитан файл с данными: "./datasets/data.csv"<jupyter_text>Узнаём типы данных, количество непустых значений, размерность и общий размер датасета<jupyter_code>print(df.info())<jupyter_output><class 'pandas.core.frame.DataFrame'>
RangeIndex: 21525 entries, 0 to 21524
Data columns (total 12 columns):
 #   Column            Non-Null Count  Dtype  
---  ------            --------------  -----  
 0   children          21525 non-null  int64  
 1   days_employed     19351 non-null  float64
 2   dob_years         21525 non-null  int64  
 3   education         21525 non-null  object 
 4   education_id      21525 non-null  int64  
 5   family_status     21525 non-null  object 
 6   family_status_id  21525 non-null  int64  
 7   gender            21525 non-null  object 
 8   income_type       21525 non-null  object 
 9   debt              21525 non-null  int64  
 10  total_income      19351 non-null  float64
 11  purpose           21525 non-null  object 
dtypes: float64(2), int64(5), object(5)
memory usage: 2.0+ MB
None<jupyter_text>Напечатаем первые 5 строк DataFrame<jupyter_code>df.head(5)<jupyter_output><empty_output><jupyter_text>Вывод:- датасет на 21525 строк по 12 значений- размер датасета 2.0+ MB- типы данных в датасете верные- в столбцах 'days_employed' и 'total_income' присутствуют пропущенные значения  2. Предобработка данных  2.1 Обработка пропущенных значений  Узнаем кол-во пропущенных значений методом isna()<jupyter_code>print('Количество пропущенных значений в столбке \'days_employed\':', sum(df['days_employed'].isna()))
print('Количество пропущенных значений в столбке \'total_income\':', sum(df['total_income'].isna()))<jupyter_output>Количество пропущенных значений в столбке 'days_employed': 2174
Количество пропущенных значений в столбке 'total_income': 2174<jupyter_text>Проверим зависимость пропущенных значений в 'days_employed' и в 'total_income' друг от друга<jupyter_code>df[(df['days_employed'].isna()) | (df['total_income'].isna())].head(5)
df[(df['days_employed'].isna()) | (df['total_income'].isna())].tail(5)
df[(df['days_employed'].isna()) ^ (df['total_income'].isna())].head(5)
print('{:.2f}% строк имеют пропущенные значения '.format((sum(df['total_income'].isna() / df.shape[0])*100)))<jupyter_output>10.10% строк имеют пропущенные значения<jupyter_text>Вывод: отсутствие значений в столбцах 'days_employed' и 'total_income' как-то зависят друг от друга, так как во всех строках в которых отсутствует одно из значений отсутствует и другое. Возможно, пропуски связанны с тем, что часть данных получены из другого источника где стаж и доход не заполняются. Используем датафрейм `df` в котором заменим пропуски средним значением  для задач:- поиска зависимости между наличием детей и возвратом кредита в срок- поиска зависимости между семейным положением и возвратом кредита в срокИспользуем датафрейм `df_cleared` в котором удалим строки с пропусками для задачи:- поиска зависимости между уровнем дохода и возвратом кредита в срок  Заполним пропуски 'total_income' как среднее значение по группам 'income_type'.<jupyter_code>mean_incomes = df[df['total_income'].notna()].groupby('income_type')['total_income'].mean()
for income_type, income  in zip(mean_incomes.index, mean_incomes):
    df.loc[(df['total_income'].isna()) & (df['income_type'] == income_type),'total_income'] = income<jupyter_output><empty_output><jupyter_text>Создадим таблицу df_cleared в которой будут отсутствовать строки с пропущенными значениями 'total_income'<jupyter_code>df_cleared = df.dropna()
print(df_cleared.shape)<jupyter_output>(19351, 12)<jupyter_text>Проверим есть ли отрицательные значения там где их быть не должно<jupyter_code>clmns = ['children', 'days_employed', 'dob_years', 'total_income']

for c in clmns:
    count = df[df[c] < 0].shape[0]
    if count > 0:
        print('Количество строк с отрицательным значением', c, ':', count)
    else:
        print('Количество строк с отрицательным значением', c, ':', '-отсутствуют-')<jupyter_output>Количество строк с отрицательным значением children : 47
Количество строк с отрицательным значением days_employed : 15906
Количество строк с отрицательным значением dob_years : -отсутствуют-
Количество строк с отрицательным значением total_income : -отсутствуют-<jupyter_text>Узнаем распределение заёмщиков по количеству детей<jupyter_code>df['children'].value_counts()<jupyter_output><empty_output><jupyter_text>'-1' ребёнок похоже не опечатку, '20' детей тоже скорее всего опечатка, так как нету заёмщиков с количеством детей больше 5  Исправим столбец со кол-вом детей. Заменим -1 на 1, а 20 на 2<jupyter_code>df.loc[df['children'] == 20, 'children'] = 2
df.loc[df['children'] == -1, 'children'] = df.loc[df['children'] == -1, 'children'].abs()
print(df['children'].value_counts())<jupyter_output>0    14149
1     4865
2     2131
3      330
4       41
5        9
Name: children, dtype: int64<jupyter_text>Узнаем распределение заёмщиков по возрасту<jupyter_code>df['dob_years'].value_counts().sort_index()<jupyter_output><empty_output><jupyter_text>Определим у каких типов заёмщиков возраст указан как '0'<jupyter_code>df[df['dob_years'] == 0]['income_type'].value_counts()<jupyter_output><empty_output><jupyter_text>Заменять строки с пропущенным возрастом на среднее значение по всем строкам было бы неправильно, так как среди пропущенных значений, например, присутствуют пенсионеры, средний возраст которых определённо выше среднего возраста по всем группам. Поэтому заменим на средний возраст по типу занятости<jupyter_code>df[df['dob_years'] != 0].groupby('income_type')['dob_years'].mean().astype('int')
mean_years = df[df['dob_years'] != 0].groupby('income_type')['dob_years'].mean().astype('int')
for income_type, dob_years  in zip(mean_years.index, mean_years):
    df.loc[(df['dob_years'] == 0) & (df['income_type'] == income_type),'dob_years'] = dob_years
print('Количество строк с нулевым стажем клиента:', df[df['dob_years'] == 0].shape[0])<jupyter_output>Количество строк с нулевым стажем клиента: 0<jupyter_text>Узнаем распределение заёмщиков по образованию<jupyter_code>df['education'].value_counts().sort_index()<jupyter_output><empty_output><jupyter_text>Приведём все значения к представлению строчными буквами<jupyter_code>df['education'] = df['education'].str.lower()
df['education'].value_counts().sort_index()<jupyter_output><empty_output><jupyter_text>Узнаем распределение заёмщиков по семейному положению<jupyter_code>df['family_status'].value_counts()<jupyter_output><empty_output><jupyter_text>В столбце 'family_status' пропусков и неправильных значений не замечено  Проверим значения в столбце 'days_employed' на наличие ошибок и аномалий<jupyter_code>print('Количество строк с отрицательным значениям стажа:', df[df['days_employed'] < 0].shape[0])
print('Средний стаж в днях:', df[df['days_employed'] > 0]['days_employed'].mean())
print('Средний \"отрицательный\" стаж в днях:', df[df['days_employed'] < 0]['days_employed'].mean())
print('Количество строк с нулевым стажем:', df[df['days_employed'] == 0]['days_employed'].count())<jupyter_output>Средний стаж в днях: 365004.30991626816
Средний "отрицательный" стаж в днях: -2353.015931998879
Количество строк с нулевым стажем: 0<jupyter_text>15906 строк имеют отрицательный стаж, а для остальным строк средний стаж около 1000 лет. Нулевого стажа нет. Определить причину искажения данных не представляется возможным<jupyter_code>print(f"Количество строк со стажем больше 10000 дней: {df[df['days_employed']>10000].shape[0]}")
print(f"Количество строк со стажем больше 300000 дней: {df[df['days_employed']>300000].shape[0]}")
df[df['days_employed']>300000]['income_type'].value_counts()<jupyter_output><empty_output><jupyter_text>Можно предположить, что модуль отрицательных значений является корректным стажем. А вот значения стажа более 300000 дней присущи пенсионерам, возможно стаж указан в часах. При рассмотрении такаго стажа как часы получается стаж около 40 лет, что похоже на правду для пенсионеров. Изменять значения не будем, так как не используем их задании.<jupyter_code>hours_in_day = 24
df.loc[df['days_employed']>300000, 'days_employed'] = df.loc[df['days_employed']>300000, 'days_employed'].apply(lambda x: x/hours_in_day)
df.loc[df['days_employed']<0, 'days_employed']      = df.loc[df['days_employed']<0, 'days_employed'].apply(lambda x: x*(-1))
print('Новый средний стаж в днях:', df['days_employed'].mean())
print('Новый средний стаж в годах:', df['days_employed'].mean() / 365)<jupyter_output>Новый средний стаж в днях: 4641.641176180646
Новый средний стаж в годах: 12.716825140220948<jupyter_text>Заполним пропуски 'days_employed' как среднее значение по группам 'income_type'.<jupyter_code>days = df[df['days_employed'].notna()].groupby('income_type')['days_employed'].mean()
for income_type, days_employed  in zip(days.index, days):
    df.loc[(df['days_employed'].isna()) & (df['income_type'] == income_type),'days_employed'] = days_employed<jupyter_output><empty_output><jupyter_text>Проверим избавились ли мы от NaN и ошибочных значений<jupyter_code>df.info()
df[['children', 'days_employed', 'total_income']].mean()<jupyter_output><empty_output><jupyter_text>Вывод:- избавились от пропущенных значений- исправили ошибочные значения- исправили нулевые значения  2.2 Замена вещественного типа данных на целочисленныйДля изменения float на int будем использовать метод astype()<jupyter_code>try:
    df[['days_employed', 'total_income']] = df[['days_employed', 'total_income']].astype('int')
except:
    print('Что-то пошло не так: ', sys.exc_info()[0])
else:
    print("Значения в столбцах 'days_employed' и 'total_income' успешно заменены с вещественных на целочисленные")<jupyter_output>Значения в столбцах 'days_employed' и 'total_income' успешно заменены с вещественных на целочисленные<jupyter_text>2.3 Обработка дубликатов<jupyter_code>df[df.duplicated(keep=False)].sort_values('dob_years')
print('Количество дубликатов', df[df.duplicated()].shape[0])
df.drop_duplicates(inplace=True)
if df[df.duplicated()].shape[0] == 0:
    print('Дубликаты успешно удалены')
else:
    print('Что-то пошло не так')<jupyter_output>Дубликаты успешно удалены<jupyter_text>В таблице нет уникальных айдишников, либо других идентификационных параметров. Такие дубликаты могут быть случаным совпадением.  Вывод- Данные были предобработаны и оставшиеся полные дубликаты были удалены методом drop_duplicates(). Было бы целесообразно удалить дубликаты до расчёта средних значений. Не исключено появление дубликатов после обработки столбца 'purpose'.- Появление дубликатов может быть связано с тем, что информация собиралась с разных источниках в которых какие-то данные совпадали  2.4 Выделение леммы в значениях столбца с целями получения кредита<jupyter_code>df['purpose'].value_counts()
m = Mystem()

purposes_all = set()
test = []
for purpose in df['purpose'].value_counts().index:
    for p in purpose.split():
        purposes_all.add(p)
lemmas = m.lemmatize(repr(purposes_all))
print(Counter(lemmas))<jupyter_output>Counter({"', '": 47, 'жилье': 4, 'автомобиль': 4, 'образование': 3, 'свадьба': 3, 'свой': 3, 'недвижимость': 3, 'высокий': 3, 'подержанный': 2, 'дополнительный': 2, 'покупка': 2, "{'": 1, 'профильный': 1, 'с': 1, 'жилой': 1, 'проведение': 1, 'сделка': 1, 'семья': 1, 'сдача': 1, 'собственный': 1, 'для': 1, 'заниматься': 1, 'операция': 1, 'со': 1, 'строительство': 1, 'ремонт': 1, 'получение': 1, 'коммерческий': 1, 'сыграть': 1, 'приобретение': 1, 'на': 1, "'}\n": 1})<jupyter_text>Вывод:- Использовалась лемматизация целей кредита с помощью библиотеки pymystem3  2.5 Категоризация данных<jupyter_code>purpose_dict = df[['purpose']]
print(purpose_dict.head(5))<jupyter_output>purpose
0               покупка жилья
1     приобретение автомобиля
2               покупка жилья
3  дополнительное образование
4             сыграть свадьбу<jupyter_text>Оставим только уникальные цели кредита. Используем для этого метод drop_duplicates.(можно было бы использовать pd.unique)<jupyter_code>purpose_dict = purpose_dict.drop_duplicates().reset_index(drop=True)
print(purpose_dict)
print('В оригинальном датасете', purpose_dict.shape[0], 'категорий целей кредита')<jupyter_output>В оригинальном датасете 38 категорий целей кредита<jupyter_text>Выделим категории целей кредита на основе результатов лемматизации<jupyter_code>puproses_new = {1:'недвижимость', 2:'ремонт', 3:'автомобиль', 4:'образование', 5:'свадьба'}<jupyter_output><empty_output><jupyter_text>Так как по результатам лемметизации видим, что одними из целей кредита являются строительство, жильё или недвижимость, то для определения цели кредита в категорию 'недвижимость' используем дополнительный словарь, чтобы объединить одинаковые по смыслу цели кредита<jupyter_code>categories = {1:['недвижимость', 'строительство', 'жилье'], 2:['ремонт'], 3:['автомобиль'], 4:['образование'], 5:['свадьба']}
russian_stemmer = SnowballStemmer('russian') 
purpose_id = []
purpose_new = []

for i in purpose_dict['purpose']:
    for word in i.split():
        stemmed_word = russian_stemmer.stem(word)
        for i in categories:
            cat_id = 0
            if stemmed_word in russian_stemmer.stem(' '.join(categories[i])):
                cat_id = i
                break
        if cat_id != 0:
            purpose_id.append(cat_id)
            purpose_new.append(puproses_new[i])
            break
purpose_dict['id'] = purpose_id
purpose_dict['purpose_new'] = purpose_new
purpose_dict<jupyter_output><empty_output><jupyter_text>Добавим категории целей кредита в исходный Датасет<jupyter_code>def set_puprose_id(row):
    return purpose_dict.loc[purpose_dict['purpose'] == row['purpose'],'id'].iloc[0]
def set_puprose_new(row):
    return purpose_dict.loc[purpose_dict['purpose'] == row['purpose'],'purpose_new'].iloc[0]
df['purpose_id'] = df.apply(set_puprose_id, axis=1)
df['purpose_new'] = df.apply(set_puprose_new, axis=1)
df.head(5)<jupyter_output><empty_output><jupyter_text>Категорируем месячный доход Определим распределение доходов, чтобы понять как разбивать доходы по категориям<jupyter_code>print('Минимальный доход:', df_cleared['total_income'].min())
print('Максимальный доход:', df_cleared['total_income'].max())
print('Средний доход:', df_cleared['total_income'].mean())
print('Медианный доход:', df_cleared['total_income'].median())<jupyter_output>Минимальный доход: 20667.26379327158
Максимальный доход: 2265604.028722744
Средний доход: 167422.3022081719
Медианный доход: 145017.93753253992<jupyter_text>Создадим функцию распределения по категориям в зависимости от доходов<jupyter_code>total_income_categories = ['очень низкий доход', 'низкий доход', 'средний доход', 'высокий доход']
total_income = [0, 40000, 80000, 150000]

def get_income_category(income):
    if(len(total_income_categories) < len(total_income)):
        return 'ошибка'
    for i in reversed(range(len(total_income_categories))):
        if income > total_income[i]:
            return total_income_categories[i]
    return 'ошибка'<jupyter_output><empty_output><jupyter_text>Создадим новый столбец 'total_income_category' с категориями дохода заёмщиков<jupyter_code>df_cleared['total_income_category'] = df_cleared['total_income'].apply(get_income_category)<jupyter_output><ipython-input-158-67c0d8f34afd>:1: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  df_cleared['total_income_category'] = df_cleared['total_income'].copy().apply(get_income_category)<jupyter_text>Проверим распределение заёмщиков по категориям доходов<jupyter_code>df_cleared['total_income_category'].value_counts()<jupyter_output><empty_output><jupyter_text>Вывод:- для целей кредита выделены новые категории: 'недвижимость', 'ремонт', 'автомобиль', 'образование', 'свадьба'- для доходов выделены новые категории: 'очень низкий доход', 'низкий доход', 'средний доход', 'высокий доход'  3. Ответы на вопросы исследования Функция возвращающая отношение кол-ва заёмщиков с просроченной задолженностью к общему кол-ву заёмщиков, использую её для форматирования вывода<jupyter_code>def get_percent_debt(a):
    return ('{:.2f}%'.format((sum(a) / len(a))*100))<jupyter_output><empty_output><jupyter_text>3.1 Есть ли зависимость между наличием детей и возвратом кредита в срок?<jupyter_code>data_pivot = df.pivot_table(index='children', values='debt', aggfunc=['count', 'sum', get_percent_debt])
data_pivot.columns = ['всего заёмщиков', 'кол-во должников', 'процент должников']
data_pivot.sort_values('процент должников')<jupyter_output><empty_output><jupyter_text>Вывод:- лучше всего возвращают кредит заёмщики без детей (кол-во заёмщиков с 5 детьми всего 9, поэтому по ним вывод сделать нельзя)  3.2 Есть ли зависимость между семейным положением и возвратом кредита в срок?<jupyter_code>data_pivot = df.pivot_table(index='family_status', values='debt', aggfunc=['count', 'sum', get_percent_debt])
data_pivot.columns = ['всего заёмщиков', 'число должников', 'процент должников']
data_pivot.sort_values('процент должников')<jupyter_output><empty_output><jupyter_text>Вывод:- лучше всего возвращают кредит вдовы- хуже всего возвращают те кто не женат или в гражданском браке  3.3 Есть ли зависимость между уровнем дохода и возвратом кредита в срок? Для поиска зависимости между уровнем дохода и возвратом кредита в срок используем таблицу очищенную от пропущенных значений дохода<jupyter_code>data_pivot = df_cleared.pivot_table(index='total_income_category', values='debt', aggfunc=['count', 'sum', get_percent_debt])
data_pivot.columns = ['всего заёмщиков', 'кол-во должников', 'процент должников']
data_pivot.sort_values('процент должников')<jupyter_output><empty_output><jupyter_text>Вывод:- зависимости возврата кредита от уровня дохода нет  3.4 Как разные цели кредита влияют на его возврат в срок?<jupyter_code>data_pivot = df.pivot_table(index='purpose_new', values='debt', aggfunc=['count', 'sum', get_percent_debt])
data_pivot.columns = ['всего заёмщиков', 'кол-во должников', 'процент должников']
data_pivot.sort_values('процент должников')<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.11542385231965023, 'fraction_numerical': 0.027058537770221033, 'mean_word_length': 6.413035649981995, 'pattern_counts': {'":': 3, '<': 148, '<?xml version=': 0, '>': 150, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '39', 'dir': 'jupyter-structured-clean-dedup', 'id': '607246', 'license': 'Unlicense', 'n_tokens_mistral': 8493, 'n_tokens_neox': 9039, 'n_words': 1741, 'path': 'Clients_debt_analysis/Clients debt research (data preprocessing).ipynb', 'repo_name': 'brut0/yandex.praktikum_ds_projects', 'size': '20585'}