|
import pandas as pd |
|
import gradio as gr |
|
import gc |
|
import re |
|
from sklearn.compose import ColumnTransformer |
|
from sklearn.impute import SimpleImputer |
|
from sklearn.pipeline import Pipeline |
|
from catboost import CatBoostClassifier |
|
import joblib |
|
import numpy as np |
|
from tqdm import tqdm |
|
|
|
from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder |
|
|
|
|
|
pipeline = joblib.load('pipeline_with_model.joblib') |
|
|
|
class DataPreprocessorOptimized: |
|
""" |
|
Класс для предобработки данных. |
|
Выполняет: |
|
- Очистку текстовых признаков |
|
- Обработку временных и финансовых признаков |
|
""" |
|
def __init__(self): |
|
|
|
self._init_regex_patterns() |
|
self._init_encoders() |
|
|
|
def _init_regex_patterns(self): |
|
""" |
|
Инициализирует регулярные выражения для очистки текстовых признаков, |
|
таких как 'region' и 'settlement'. Здесь задаются слова, которые необходимо удалить, |
|
а также шаблоны для классификации населённых пунктов. |
|
""" |
|
remove_words = [ |
|
'обл', 'область', 'край', 'народная', 'респ', 'г', |
|
'республика', 'аобл', 'район', 'ао', 'автономный округ', |
|
'югра', 'якутия', 'кузбасс', 'алания', 'чувашия' |
|
] |
|
self.pattern_remove_words = re.compile(r'\b(?:' + '|'.join(remove_words) + r')\b', re.I) |
|
self.pattern_non_word = re.compile(r'[^\w\s]', re.I) |
|
self.pattern_sakhalin = re.compile(r'\bсахалин\b', re.I) |
|
|
|
|
|
self.settlement_patterns = { |
|
2: re.compile(r'\b(пгт|посёлок городского типа|поселок городского типа)\b', re.I), |
|
3: re.compile(r'\b(п|рп|поселок|посёлок|с|сп|село)\b', re.I), |
|
1: re.compile(r'\b(г|город)\b', re.I) |
|
} |
|
|
|
def _init_encoders(self): |
|
""" |
|
Инициализирует энкодеры для категориальных признаков: |
|
- onehot_encoder для столбцов с небольшим числом уникальных значений. |
|
- ordinal_encoder для остальных. |
|
""" |
|
self.onehot_encoder = OneHotEncoder(sparse_output=False, drop='first') |
|
self.ordinal_encoder = OrdinalEncoder() |
|
|
|
def _process_text_features(self, df: pd.DataFrame) -> pd.DataFrame: |
|
""" |
|
Обрабатывает текстовые признаки: |
|
- Нормализует столбец 'region': приводит к нижнему регистру, удаляет лишние слова, спецсимволы, |
|
убирает дублирование слов. |
|
- Обрабатывает 'settlement': нормализует, создает категорию населённого пункта, а также |
|
вычисляет региональную популярность и индикатор для Москвы/СПБ. |
|
""" |
|
|
|
if 'region' in df.columns: |
|
df['region'] = ( |
|
df['region'] |
|
.str.lower() |
|
.str.replace(self.pattern_remove_words, '', regex=True) |
|
.str.replace(self.pattern_non_word, '', regex=True) |
|
.str.replace(r'\s+', ' ', regex=True) |
|
.str.replace(self.pattern_sakhalin, 'сахалинская', regex=True) |
|
.apply(lambda x: ' '.join(dict.fromkeys(x.split()))) |
|
.str.strip() |
|
) |
|
|
|
|
|
if 'settlement' in df.columns: |
|
s = df['settlement'].str.lower() |
|
conditions = [ |
|
s.str.contains(self.settlement_patterns[2], regex=True), |
|
s.str.contains(self.settlement_patterns[3], regex=True), |
|
s.str.contains(self.settlement_patterns[1], regex=True) |
|
] |
|
df['settlement_category'] = np.select(conditions, [2, 3, 1], default=4) |
|
|
|
return df |
|
|
|
def _process_time_features(self, df: pd.DataFrame) -> pd.DataFrame: |
|
""" |
|
Обрабатывает временные признаки: |
|
- Извлекает базовые признаки из 'created_at': created_month, created_dayofweek. |
|
- Использует групповые преобразования (transform) для вычисления: |
|
* time_between_loans: разница в днях между последовательными займами. |
|
* time_between_loans_mean: среднее значение time_between_loans по клиенту. |
|
* first_loan, last_loan, loan_count: минимальная и максимальная дата займа и количество займов. |
|
* time_since_first_loan и time_since_last_loan: производные признаки. |
|
* loan_frequency: частота займов. |
|
""" |
|
|
|
df = df.sort_values(['client_id', 'created_at']) |
|
|
|
df['time_between_loans'] = df.groupby('client_id')['created_at'].diff().dt.days |
|
|
|
|
|
g = df.groupby('client_id') |
|
|
|
agg_df = g.agg({ |
|
'time_between_loans': 'mean', |
|
'created_at': ['min', 'max'], |
|
'loan_id': 'size' |
|
}).reset_index() |
|
|
|
agg_df.columns = ['client_id', 'time_between_loans_mean', 'first_loan', 'last_loan', 'loan_count'] |
|
|
|
|
|
df = df.merge(agg_df, on='client_id', how='left') |
|
df['time_since_first_loan'] = (df['created_at'] - df['first_loan']).dt.days |
|
df['time_since_last_loan'] = (df['last_loan'] - df['created_at']).dt.days |
|
df['loan_frequency'] = df['loan_count'] / (((df['last_loan'] - df['first_loan']).dt.days / 365) + 1e-6) |
|
df.drop(columns=['first_loan', 'last_loan'], inplace=True) |
|
|
|
df['created_month'] = df['created_at'].dt.month |
|
df['created_dayofweek'] = df['created_at'].dt.dayofweek |
|
return df |
|
|
|
def _process_financial_features(self, df: pd.DataFrame) -> pd.DataFrame: |
|
""" |
|
Обрабатывает финансовые признаки: |
|
- Вычисляет коэффициенты: |
|
approved_requested_ratio, |
|
""" |
|
if 'approved_amount' in df.columns and 'requested_amount' in df.columns: |
|
df['approved_requested_ratio'] = df['approved_amount'] / (df['requested_amount'] + 1e-6) |
|
return df |
|
|
|
def preprocess_data(self, df: pd.DataFrame) -> pd.DataFrame: |
|
""" |
|
Выполняет последовательную предобработку данных. |
|
Шаги: |
|
1. Обработка текстовых признаков (region, settlement). |
|
2. Обработка временных признаков (создание базовых временных признаков и групповых статистик). |
|
3. Обработка финансовых признаков (расчет коэффициентов и производных признаков). |
|
|
|
После каждого шага вызывается сборщик мусора для оптимизации памяти. |
|
""" |
|
processing_steps = [ |
|
self._process_text_features, |
|
self._process_time_features, |
|
self._process_financial_features, |
|
] |
|
|
|
with tqdm(processing_steps, desc="Обработка данных") as pbar: |
|
for step in pbar: |
|
df = step(df).copy() |
|
pbar.set_postfix(shape=df.shape) |
|
gc.collect() |
|
|
|
return df |
|
|
|
|
|
def predict(file): |
|
USECOLS = [ |
|
"monthly_income", "work_experience", "requested_sum", |
|
"main_agreement_amount", "main_agreement_term", "requested_period_days", |
|
"requested_amount", "req_app_amount", "approved_amount", "period_days", |
|
"days_finish_loan", "ag", "cnt_ext", "term", "price", "elecs_sum", |
|
"recurents_sum", "tamount", "issues", "principal", "interest", |
|
"overdue_interest", "overdue_fee", "nbki_score", "payment_frequency", |
|
"status", "loan_id", "client_id", "source", "first_source", "interface", |
|
"type", "repayment_type", "client_type", "settlement", "region", |
|
"gender", "loan_order", "have_extension", "contact_cases", "created_at", |
|
"start_dt" |
|
] |
|
DTYPES = { |
|
"payment_frequency": "int8", |
|
"status": "int8", |
|
"loan_id": "int32", |
|
"client_id": "int32", |
|
"source": "int8", |
|
"first_source": "int8", |
|
"interface": "int8", |
|
"type": "int8", |
|
"repayment_type": "int8", |
|
"client_type": "bool", |
|
"loan_order": "int16", |
|
"have_extension": "bool", |
|
"settlement": "category", |
|
"region": "category", |
|
"gender": "category" |
|
} |
|
|
|
df = pd.read_csv(file.name, dtype=DTYPES, usecols=USECOLS, parse_dates=["created_at", "start_dt"]) |
|
|
|
|
|
print('Start preprocessor') |
|
preprocessor = DataPreprocessorOptimized() |
|
processed_df = preprocessor.preprocess_data(df) |
|
processed_df = processed_df.drop(columns=['created_at', 'start_dt'], errors='ignore') |
|
print('Finish preprocessor') |
|
|
|
features = [ |
|
'payment_frequency', 'status', 'source', 'first_source', 'interface', 'type', 'repayment_type', |
|
'settlement', 'region', 'gender', 'loan_order', 'settlement_category', 'client_type', |
|
'have_extension', 'contact_cases', 'time_between_loans', 'time_between_loans_mean', |
|
'time_since_first_loan', 'time_since_last_loan', 'loan_frequency', 'created_month', |
|
'created_dayofweek', 'loan_count', 'approved_requested_ratio' |
|
] |
|
|
|
print('Start pipeline') |
|
preds = pipeline.predict(processed_df[features]) |
|
|
|
|
|
submission = pd.DataFrame({ |
|
"loan_id": processed_df["loan_id"], |
|
"churn": preds |
|
}) |
|
submission_path = "submission_catboost.csv" |
|
submission.to_csv(submission_path, index=False) |
|
|
|
return submission_path |
|
|
|
|
|
iface = gr.Interface( |
|
fn=predict, |
|
inputs=gr.File(label="Загрузите CSV-файл"), |
|
outputs=gr.File(label="Скачать предсказания"), |
|
title="Прогнозирование оттока клиентов с использованием CatBoost", |
|
description="Загрузите CSV-файл с данными для предсказания оттока клиентов. Модель использует CatBoost для прогнозирования оттока." |
|
) |
|
|
|
if __name__ == "__main__": |
|
iface.launch() |
|
import pandas as pd |
|
import gradio as gr |
|
import gc |
|
import re |
|
from sklearn.compose import ColumnTransformer |
|
from sklearn.impute import SimpleImputer |
|
from sklearn.pipeline import Pipeline |
|
from catboost import CatBoostClassifier |
|
import joblib |
|
import numpy as np |
|
from tqdm import tqdm |
|
|
|
from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder |
|
|
|
|
|
pipeline = joblib.load('pipeline_with_model.joblib') |
|
|
|
class DataPreprocessorOptimized: |
|
""" |
|
Класс для предобработки данных. |
|
Выполняет: |
|
- Очистку текстовых признаков |
|
- Обработку временных и финансовых признаков |
|
""" |
|
def __init__(self): |
|
|
|
self._init_regex_patterns() |
|
self._init_encoders() |
|
|
|
def _init_regex_patterns(self): |
|
""" |
|
Инициализирует регулярные выражения для очистки текстовых признаков, |
|
таких как 'region' и 'settlement'. Здесь задаются слова, которые необходимо удалить, |
|
а также шаблоны для классификации населённых пунктов. |
|
""" |
|
remove_words = [ |
|
'обл', 'область', 'край', 'народная', 'респ', 'г', |
|
'республика', 'аобл', 'район', 'ао', 'автономный округ', |
|
'югра', 'якутия', 'кузбасс', 'алания', 'чувашия' |
|
] |
|
self.pattern_remove_words = re.compile(r'\b(?:' + '|'.join(remove_words) + r')\b', re.I) |
|
self.pattern_non_word = re.compile(r'[^\w\s]', re.I) |
|
self.pattern_sakhalin = re.compile(r'\bсахалин\b', re.I) |
|
|
|
|
|
self.settlement_patterns = { |
|
2: re.compile(r'\b(пгт|посёлок городского типа|поселок городского типа)\b', re.I), |
|
3: re.compile(r'\b(п|рп|поселок|посёлок|с|сп|село)\b', re.I), |
|
1: re.compile(r'\b(г|город)\b', re.I) |
|
} |
|
|
|
def _init_encoders(self): |
|
""" |
|
Инициализирует энкодеры для категориальных признаков: |
|
- onehot_encoder для столбцов с небольшим числом уникальных значений. |
|
- ordinal_encoder для остальных. |
|
""" |
|
self.onehot_encoder = OneHotEncoder(sparse_output=False, drop='first') |
|
self.ordinal_encoder = OrdinalEncoder() |
|
|
|
def _process_text_features(self, df: pd.DataFrame) -> pd.DataFrame: |
|
""" |
|
Обрабатывает текстовые признаки: |
|
- Нормализует столбец 'region': приводит к нижнему регистру, удаляет лишние слова, спецсимволы, |
|
убирает дублирование слов. |
|
- Обрабатывает 'settlement': нормализует, создает категорию населённого пункта, а также |
|
вычисляет региональную популярность и индикатор для Москвы/СПБ. |
|
""" |
|
|
|
if 'region' in df.columns: |
|
df['region'] = ( |
|
df['region'] |
|
.str.lower() |
|
.str.replace(self.pattern_remove_words, '', regex=True) |
|
.str.replace(self.pattern_non_word, '', regex=True) |
|
.str.replace(r'\s+', ' ', regex=True) |
|
.str.replace(self.pattern_sakhalin, 'сахалинская', regex=True) |
|
.apply(lambda x: ' '.join(dict.fromkeys(x.split()))) |
|
.str.strip() |
|
) |
|
|
|
|
|
if 'settlement' in df.columns: |
|
s = df['settlement'].str.lower() |
|
conditions = [ |
|
s.str.contains(self.settlement_patterns[2], regex=True), |
|
s.str.contains(self.settlement_patterns[3], regex=True), |
|
s.str.contains(self.settlement_patterns[1], regex=True) |
|
] |
|
df['settlement_category'] = np.select(conditions, [2, 3, 1], default=4) |
|
|
|
return df |
|
|
|
def _process_time_features(self, df: pd.DataFrame) -> pd.DataFrame: |
|
""" |
|
Обрабатывает временные признаки: |
|
- Извлекает базовые признаки из 'created_at': created_month, created_dayofweek. |
|
- Использует групповые преобразования (transform) для вычисления: |
|
* time_between_loans: разница в днях между последовательными займами. |
|
* time_between_loans_mean: среднее значение time_between_loans по клиенту. |
|
* first_loan, last_loan, loan_count: минимальная и максимальная дата займа и количество займов. |
|
* time_since_first_loan и time_since_last_loan: производные признаки. |
|
* loan_frequency: частота займов. |
|
""" |
|
|
|
df = df.sort_values(['client_id', 'created_at']) |
|
|
|
df['time_between_loans'] = df.groupby('client_id')['created_at'].diff().dt.days |
|
|
|
|
|
g = df.groupby('client_id') |
|
|
|
agg_df = g.agg({ |
|
'time_between_loans': 'mean', |
|
'created_at': ['min', 'max'], |
|
'loan_id': 'size' |
|
}).reset_index() |
|
|
|
agg_df.columns = ['client_id', 'time_between_loans_mean', 'first_loan', 'last_loan', 'loan_count'] |
|
|
|
|
|
df = df.merge(agg_df, on='client_id', how='left') |
|
df['time_since_first_loan'] = (df['created_at'] - df['first_loan']).dt.days |
|
df['time_since_last_loan'] = (df['last_loan'] - df['created_at']).dt.days |
|
df['loan_frequency'] = df['loan_count'] / (((df['last_loan'] - df['first_loan']).dt.days / 365) + 1e-6) |
|
df.drop(columns=['first_loan', 'last_loan'], inplace=True) |
|
|
|
df['created_month'] = df['created_at'].dt.month |
|
df['created_dayofweek'] = df['created_at'].dt.dayofweek |
|
return df |
|
|
|
def _process_financial_features(self, df: pd.DataFrame) -> pd.DataFrame: |
|
""" |
|
Обрабатывает финансовые признаки: |
|
- Вычисляет коэффициенты: |
|
approved_requested_ratio, |
|
""" |
|
if 'approved_amount' in df.columns and 'requested_amount' in df.columns: |
|
df['approved_requested_ratio'] = df['approved_amount'] / (df['requested_amount'] + 1e-6) |
|
return df |
|
|
|
def preprocess_data(self, df: pd.DataFrame) -> pd.DataFrame: |
|
""" |
|
Выполняет последовательную предобработку данных. |
|
Шаги: |
|
1. Обработка текстовых признаков (region, settlement). |
|
2. Обработка временных признаков (создание базовых временных признаков и групповых статистик). |
|
3. Обработка финансовых признаков (расчет коэффициентов и производных признаков). |
|
|
|
После каждого шага вызывается сборщик мусора для оптимизации памяти. |
|
""" |
|
processing_steps = [ |
|
self._process_text_features, |
|
self._process_time_features, |
|
self._process_financial_features, |
|
] |
|
|
|
with tqdm(processing_steps, desc="Обработка данных") as pbar: |
|
for step in pbar: |
|
df = step(df).copy() |
|
pbar.set_postfix(shape=df.shape) |
|
gc.collect() |
|
|
|
return df |
|
|
|
|
|
def predict(file): |
|
USECOLS = [ |
|
"monthly_income", "work_experience", "requested_sum", |
|
"main_agreement_amount", "main_agreement_term", "requested_period_days", |
|
"requested_amount", "req_app_amount", "approved_amount", "period_days", |
|
"days_finish_loan", "ag", "cnt_ext", "term", "price", "elecs_sum", |
|
"recurents_sum", "tamount", "issues", "principal", "interest", |
|
"overdue_interest", "overdue_fee", "nbki_score", "payment_frequency", |
|
"status", "loan_id", "client_id", "source", "first_source", "interface", |
|
"type", "repayment_type", "client_type", "settlement", "region", |
|
"gender", "loan_order", "have_extension", "contact_cases", "created_at", |
|
"start_dt" |
|
] |
|
DTYPES = { |
|
"payment_frequency": "int8", |
|
"status": "int8", |
|
"loan_id": "int32", |
|
"client_id": "int32", |
|
"source": "int8", |
|
"first_source": "int8", |
|
"interface": "int8", |
|
"type": "int8", |
|
"repayment_type": "int8", |
|
"client_type": "bool", |
|
"loan_order": "int16", |
|
"have_extension": "bool", |
|
"settlement": "category", |
|
"region": "category", |
|
"gender": "category" |
|
} |
|
|
|
df = pd.read_csv(file.name, dtype=DTYPES, usecols=USECOLS, parse_dates=["created_at", "start_dt"]) |
|
|
|
|
|
print('Start preprocessor') |
|
preprocessor = DataPreprocessorOptimized() |
|
processed_df = preprocessor.preprocess_data(df) |
|
processed_df = processed_df.drop(columns=['created_at', 'start_dt'], errors='ignore') |
|
print('Finish preprocessor') |
|
|
|
cat_features = ['payment_frequency', 'status', 'source', 'first_source', 'interface', 'type', 'repayment_type', 'settlement', 'region', 'gender', 'loan_order', 'settlement_category'] |
|
X = processed_df[cat_features + ['time_between_loans', 'time_between_loans_mean', 'time_since_first_loan', 'time_since_last_loan', 'loan_frequency', 'created_month', 'created_dayofweek']] |
|
print('Start pipeline') |
|
preds = pipeline.predict(X) |
|
|
|
|
|
submission = pd.DataFrame({ |
|
"loan_id": processed_df["loan_id"], |
|
"churn": preds |
|
}) |
|
submission_path = "submission_catboost.csv" |
|
submission.to_csv(submission_path, index=False) |
|
|
|
return submission_path |
|
|
|
|
|
iface = gr.Interface( |
|
fn=predict, |
|
inputs=gr.File(label="Загрузите CSV-файл"), |
|
outputs=gr.File(label="Скачать предсказания"), |
|
title="Прогнозирование оттока клиентов с использованием CatBoost", |
|
description="Загрузите CSV-файл с данными для предсказания оттока клиентов. Модель использует CatBoost для прогнозирования оттока." |
|
) |
|
|
|
if __name__ == "__main__": |
|
iface.launch() |
|
|