DanielNRU's picture
Update app.py
f931c0a verified
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)
# Шаблоны для определения типа населённого пункта:
# 2 – для поселков городского типа, 3 – для поселков/сел, 1 – для городов.
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()
)
# Обработка признака settlement
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: частота займов.
"""
# Сортировка по client_id и created_at
df = df.sort_values(['client_id', 'created_at'])
# Вычисляем разницу по времени между займами для каждой группы
df['time_between_loans'] = df.groupby('client_id')['created_at'].diff().dt.days
# Группируем по всему DataFrame по 'client_id'
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']
# Объединяем агрегированные данные с исходным DataFrame
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])
# Сохранение предсказаний в CSV
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
# Создание Gradio интерфейса
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)
# Шаблоны для определения типа населённого пункта:
# 2 – для поселков городского типа, 3 – для поселков/сел, 1 – для городов.
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()
)
# Обработка признака settlement
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: частота займов.
"""
# Сортировка по client_id и created_at
df = df.sort_values(['client_id', 'created_at'])
# Вычисляем разницу по времени между займами для каждой группы
df['time_between_loans'] = df.groupby('client_id')['created_at'].diff().dt.days
# Группируем по всему DataFrame по 'client_id'
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']
# Объединяем агрегированные данные с исходным DataFrame
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)
# Сохранение предсказаний в CSV
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
# Создание Gradio интерфейса
iface = gr.Interface(
fn=predict,
inputs=gr.File(label="Загрузите CSV-файл"),
outputs=gr.File(label="Скачать предсказания"),
title="Прогнозирование оттока клиентов с использованием CatBoost",
description="Загрузите CSV-файл с данными для предсказания оттока клиентов. Модель использует CatBoost для прогнозирования оттока."
)
if __name__ == "__main__":
iface.launch()