Введение
Еще на этапе создания модели следует проделать работу, направленную на замедление ее устаревания.
Реализацию процесса работы с устареванием моделей в ML можно разделить на 4 шага:
-
Шаг 2: Создание надежных и долговечных моделей
-
Шаг 3: Внедрение системы мониторинга
-
Шаг 4: Переобучение и поддержание актуальности модели
В этой части мы с вами узнаем, как создать надежную и долговечную модель, а также получить много полезной информации, которая поможет нам бороться с устареванием в будущем.
Мы пройдем полный путь создания модели и работы над замедлением ее устаревания.
В данной статье частично присутствуют переработанные идеи из статьи To retrain, or not to retrain? Let’s get analytical about ML model updates. Рекомендую ее к прочтению.
Поиск оптимального периода для обучения модели и выяснение скорости устаревания модели
В реальных задачах, как правило, доступна критически важная мета-информация о данных — время их получения. Эта информация незаменима при мониторинге модели, она позволяет нам сравнивать данные за различные периоды и выявлять различные изменения. Более того, эту информацию следует использовать и при создании модели, поэтому наша работа по обеспечению устойчивости модели должна начинаться еще с момента ее создания. В этой главе мы разберем, как именно это можно сделать.
Пример такой мета информации:
Снятие денежных средств юзерами

В данном примере у нас есть информация о дате снятия средств юзером – колонка date.
Далее мы рассмотрим основные этапы при создании модели, которые помогут нам.
А также основные этапы замедления устаревания модели при её создании c примером данных, приближенных к реальности.
0 Создание синтетических данных, описание задачи
Допустим мы data scientist антифрод отдела, и нам необходимо на основании имеющихся у нас данных понять, совершено ли снятие средств мошенником или самим пользователем, то есть найти фрод. Для этой задачи мы создадим ML модель, но, как мы помним, в антифроде очень быстро происходит устаревание моделей, поэтому при создании мы постараемся минимизировать скорость ее устаревания.
Импорт необходимых библиотек и генерация синтетических данных
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from catboost import CatBoostClassifier, Pool
from sklearn.metrics import roc_auc_score
from dateutil.relativedelta import relativedelta
def generate_synthetic_data(start_date: str, end_date: str, n_users: int) -> pd.DataFrame:
"""
Генерация синтетических данных о транзакциях пользователей с постепенным добавлением новых пользователей в систему.
Params:
start_date (str): Дата начала периода генерации данных (в формате 'YYYY-MM-DD').
end_date (str): Дата окончания периода генерации данных (в формате 'YYYY-MM-DD').
n_users (int): Количество пользователей, для которых генерируются данные.
Return:
pd.DataFrame: DataFrame с синтетическими данными, включающий следующие колонки:
- user_id (int): Уникальный идентификатор пользователя.
- date (datetime): Дата транзакции.
- amount (float): Сумма транзакции.
- feature_1 (int): Случайная числовая характеристика пользователя.
- feature_2 (int): Случайная категориальная характеристика пользователя (0, 1, или 2).
- user_lifetime (int): Количество дней с момента регистрации пользователя до даты транзакции.
- target (int): Бинарный целевой признак (0 или 1)
"""
np.random.seed(42)
dates = pd.date_range(start_date, end_date)
data = []
for user_id in range(n_users):
# Случайная дата регистрации пользователя
registration_date = np.random.choice(dates)
# Генерируем данные только для дат после регистрации
for date in dates[dates >= registration_date]:
num_withdrawals = np.random.choice([0, 1, 2], p=[0.7, 0.2, 0.1])
for _ in range(num_withdrawals):
amount = round(np.random.uniform(10, 1000), 2)
feature_1 = np.random.randint(100, 200)
feature_2 = np.random.choice([0, 1, 2], p=[0.2, 0.7, 0.1])
# Возраст увеличивается с каждой датой
user_lifetime = (date - registration_date).days
# Влияние признаков на target
base_probability = 0.1
probability_boost = (amount / 1000) * 0.3 +
(feature_1 / 200) * 0.02 +
(feature_2 / 2) * 0.01
if user_lifetime < 100:
probability_boost += 0.2
elif user_lifetime < 200:
probability_boost += 0.01
else:
probability_boost += 0.005
probability = base_probability + probability_boost
target = np.random.choice([0, 1], p=[1 - probability, probability])
data.append({
'user_id': user_id,
'date': date,
'amount': amount,
'feature_1': feature_1,
'feature_2': feature_2,
'user_lifetime': user_lifetime,
'target': target
})
return pd.DataFrame(data)
start_date = '2023-01-01'
end_date = '2024-04-30'
n_users = 200
df = generate_synthetic_data(start_date, end_date, n_users)
start_date = pd.to_datetime(start_date)
df['month_number'] = df['date'].apply(lambda date: relativedelta(date, start_date).months + 12 * relativedelta(date, start_date).years + 1)
print('Shape:', df.shape)
val_counts = df['target'].value_counts()
zero_class_weight = round(100 - val_counts[1]*100/(val_counts[0]+val_counts[1]),2)
first_class_weight = round(val_counts[1]*100/(val_counts[0]+val_counts[1]),2)
print(f'nClass weightsn1: {first_class_weight}n0: {zero_class_weight}')
print(f'Month numbers: {sorted(df['month_number'].unique(), reverse=True)}')
df.head(5)

Мы получили набор данных с дисбалансом в классах, в качестве фичей в дальнейшем мы будем рассматривать только amount
, feature_1
, feature_2
, user_lifetime
.
Поле month_number
представляет собой порядковый номер месяца, начиная отсчет с первого месяца (января). Например, январь 2023 года имеет значение 1, а январь 2024 года – 13. В дальнейшем я расскажу, как мы это будем использовать.
1 Проведение Feature Engineering для замедления устаревания
Для замедления устаревания модели часто используют работу с имеющимися признаками (Feature Engineering)
Правильно спроектированные признаки могут сделать модель более устойчивой к изменениям в данных и продлить срок ее жизни.
Тут нужно сразу добавить важную деталь.
При изменении признаков вы идете на определенные риски:
-
Feature Engineering может привести к потере части информации, которую несет ваш признак, это в свою очередь также может привести как у уменьшению точности вашей модели, так и к ее увеличению. В некоторых случаях использование изначальных значений признака (то есть без Feature Engineering) может привести к более точным прогнозам. Целью Feature Engineering является улучшение обобщающей способности и, в конечном счете, повышение точности на новых данных, даже если при этом происходит некоторая потеря информации .
-
Чтобы получить наилучший результат, нужно тщательно изучить свои данные, чтобы хорошо понимать их, не бойтесь экспериментировать.
-
Даже если вы отлично подготовили данные и создали очень устойчивую модель, все равно не следует пренебрегать мониторингом ее качества.
В любом случае при создании модели следует следить за ее качеством до и после применения Feature Engineering и находить золотую середину для своих задач.
Использовать это или нет – весьма индивидуально, и, возможно, конкретно в вашем случае вам будет выгоднее просто чаще переобучать модель.
Итак, вот основные подходы, которые могут помочь в замедлении устаревания модели:
-
Создание стабильных признаков:
-
Использование относительных значений. Вместо использования абсолютных значений, которые могут сильно меняться со временем, попробуйте использовать относительные. Например, вместо “сумма депозита” используйте “процент от среднего депозита юзера”.
-
Ранги и категории. Преобразуйте числовые признаки в ранги или категории. Например, вместо конкретного возраста используйте возрастные группы (молодой, средний, пожилой). Это сделает модель менее чувствительной к изменениям в значениях.
-
Трансформации. Можно использовать различные математические трансформации (логарифмирование, возведение в степень и так далее) для стабилизации распределений признаков.
-
-
Включение признаков, отражающих время и контекст:
-
Временные признаки. Добавьте признаки, которые явно будут указывать на время. Например: год, месяц, день недели и так далее. Это позволит модели учитывать сезонные колебания и другие временные закономерности.
-
Признаки, отражающие тренды. Рассчитайте скользящие средние, экспоненциальное сглаживание или другие показатели, которые могут отобразить тренды в данных.
-
Контекстные признаки. Включите признаки, описывающие контекст, в котором используются данные. К примеру, если вы прогнозируете продажи автомобилей, то добавьте признаки, отражающие экономическую ситуацию в регионе, рекламные кампании и другие факторы, влияющие на спрос.
-
-
Использование признаков, устойчивых к выбросам:
-
Robust Scalers. Попробуйте использовать RobustScaler вместо StandardScaler. RobustScaler использует медиану и межквартильный размах, что делает его менее чувствительным к выбросам.
-
Winsorizing. Замените выбросы на заданные значения (например, 5-й и 95-й процентили).
-
-
Feature Selection и Dimensionality Reduction:
-
Отбор стабильных признаков. Используйте методы отбора признаков, которые выбирают признаки, наиболее стабильные во времени. Например, можно отдельно оценить важность признаков на разных временных отрезках и выбрать только те, которые сохраняют свою важность.
-
PCA и другие методы снижения размерности. Используйте PCA (Principal Component Analysis) или другие методы снижения размерности для создания новых признаков, которые агрегируют информацию из нескольких исходных признаков.
-
-
Взаимодействие признаков (Feature Interactions):
-
Создание взаимодействий между признаками. Изучите возможные взаимодействия между признаками. Например, вы прогнозируете покупки юзера, и у вас есть признаки “Количество просмотров товара за неделю” и “Средняя цена товара в категории”. Умножьте эти признаки между собой, и у вас получится признак, который показывает, насколько популярность товара связана с его ценой относительно аналогов.
-
Для нашей задачи проведем работу только с одним признаком.
Feature Engineering
def categorize_user_lifetime(user_lifetime: int) -> str:
"""
Категоризирует время жизни пользователя (user_lifetime) в одну из трех категорий:
'young', 'average' или 'elderly'.
Args:
user_lifetime (int): Время жизни пользователя в днях.
Returns:
str: Категория времени жизни пользователя
"""
if user_lifetime < 100:
return 'young'
elif user_lifetime < 200:
return 'average'
else:
return 'elderly'
def feature_engineering(df: pd.DataFrame) -> pd.DataFrame:
"""
Выполняет преобразование признаков, направленное на уменьшение устаревания модели.
Args:
df (pd.DataFrame): Исходный DataFrame
Returns:
pd.DataFrame:Ппереработанный DataFrame
"""
# user_lifetime
df['user_lifetime_category'] = df['user_lifetime'].apply(categorize_user_lifetime)
df.drop(['user_lifetime'], axis=1, inplace=True)
return df
df = feature_engineering(df)
print(df['user_lifetime_category'].value_counts())
df.head()

Мы перевели признак user_lifetime
в категориальный, как говорилось ранее, такие методы помогают замедлить устаревание модели. Допустим, когда мы только создавали модель, самый старый пользователь имел значение 200, через 100 дней самый старый пользователь будет иметь уже значение 300, что может негативно сказаться на качестве модели.
2 Использование различных промежутков времени при обучении модели
Зачастую можно встретить ситуацию, при которой аналитик данных для обучения своей модели берет какой-то один промежуток времени с данными, например за год, и просто обучает модель за этот период. Но что, если слишком старые данные негативно влияют на качество предсказаний для новых данных. Или если нужно наоборот взять больше данных, чтобы получить лучшее качество. А может быть один год – это слишком много, и нет смысла нагружать инфраструктуру таким количеством данных, и будет эффективнее использовать меньший объем данных.
На этапе создания модели мы можем попробовать использовать данные за разные промежутки времени. То есть нам нужно взять данные для обучения не только за какой-то один отрезок истории, но и за несколько других.
Тут следует добавить, что выбор оптимального промежутка времени – это моментный снимок. Необходимо регулярно переоценивать оптимальный промежуток времени, так как распределение данных может меняться. Стратегия выбора промежутка времени в дальнейшем должна быть частью системы непрерывного мониторинга и переобучения.
Вот так это будет выглядеть визуально

На этом графике ячейка – это некий промежуток времени, мы будем использовать месяц, именно для этого нам и нужна колонка month_number. Вы же в своей работе можете попробовать использовать другие промежутки, к примеру недельные.
При выборе промежутка времени необходимо учитывать объем доступных данных и обеспечивать достаточное количество примеров для обучения модели, особенно для дисбалансе классов.
Допустим, что самый свежий набор данных, которые у нас есть в распоряжении, обозначен зеленым цветом, именно его имеет смысл брать для тестовой выборки, так как он отражает наиболее свежую информацию.
Серым цветом обозначены промежутки времени, из которых будет состоять наш тренировочный набор данных (train set или tran & validate set).
Мы будем обучать модель за разные промежутки времени и найдем среди них оптимальный.
Для определения оптимального периода нужно построить график изменения качества в зависимости от используемых промежутков времени. Нам необходимо найти такой промежуток исторических данных, который либо находится на пике качества, либо на плато.
Ниже два примера, наиболее подходящий промежуток времени обозначен зеленой вертикальной линией.
На первом изображено плато, имеет смыл использовать не весь промежуток, а только тот участок, при котором качество впервые достигло плато, чтобы уменьшить нагрузку на инфраструктуру.

На втором мы видим пик качества, имеет смысл использовать данные до этого пика, чтобы максимизировать качество нашей модели.

При этом может возникнуть ситуация, при которой ни пик, ни плато не были обнаружены. В таком случае необходимо (по возможности) при каждом переобучении добавлять к историческим данным новые, пока не будет достигнут пик/плато.

Таким образом, реализовав этот пункт, мы получим:
-
лучшее качество модели (возможно)
-
информацию об оптимальном временном промежутке для нашей модели
-
в целом понимание как меняется качество нашей модели при различных промежутках
Тренировка модели и оценки ее качества
def split_features_target(data: pd.DataFrame) -> tuple:
"""
Разделяет входной DataFrame на матрицу признаков (X) и вектор целевой переменной (y).
Args:
data (pd.DataFrame): DataFrame, содержащий признаки и целевую переменную.
Столбец 'target' используется как целевая переменная.
Returns:
tuple: Кортеж, содержащий:
- X (pd.DataFrame): Матрица признаков.
- y (pd.Series): Вектор целевой переменной.
"""
X = data.drop(['target', 'user_id','date','month_number'], axis=1)
y = data['target']
return X,y
def train_and_evaluate(data: pd.DataFrame, cat_features: list) -> tuple:
"""
Обучает модель CatBoost и оценивает ее качество на валидационной и тестовой выборках.
Args:
data (pd.DataFrame): DataFrame, содержащий данные для обучения и оценки модели.
cat_features (list): Список названий категориальных признаков в DataFrame.
Returns:
tuple: Кортеж, содержащий:
- auc_val (float): Значение AUC на валидационной выборке.
- auc_test (float): Значение AUC на тестовой выборке.
"""
df_train_val = data[data['month_number'] < data['month_number'].max()]
df_test = data[data['month_number'] == data['month_number'].max()]
print(f'ntrain_val month numbers: {sorted(df_train_val['month_number'].unique(),reverse=True)}')
print(f'test month numbers: {sorted(df_test['month_number'].unique(),reverse=True)}')
X_train_val, y_train_val = split_features_target(df_train_val)
X_test, y_test = split_features_target(df_test)
val_counts = y_train_val.value_counts()
zero_class_weight = round(1-val_counts[1]/(val_counts[0]+val_counts[1]),2)
first_class_weight = round(val_counts[1]/(val_counts[0]+val_counts[1]),2)
class_weights = [zero_class_weight, first_class_weight]
X_train, X_val, y_train, y_val = train_test_split(X_train_val, y_train_val, test_size=0.2,
random_state=42, stratify=y_train_val)
train_pool = Pool(X_train, y_train, cat_features=cat_features)
validate_pool = Pool(X_val, y_val, cat_features=cat_features)
test_pool = Pool(X_test, y_test, cat_features=cat_features)
model = CatBoostClassifier(
random_state=42,
verbose=0,
class_weights=class_weights,
eval_metric='AUC',
early_stopping_rounds=50
)
model.fit(train_pool, eval_set=validate_pool)
y_pred_proba = model.predict_proba(X_train_val)[:, 1]
auc_val = roc_auc_score(y_train_val, y_pred_proba)
y_pred_proba = model.predict_proba(X_test)[:, 1]
auc_test = roc_auc_score(y_test, y_pred_proba)
return auc_val, auc_test
Не будем подробно останавливаться на всем коде, для нас самое интересное в нем это эти строки:
df_train_val = data[data['month_number'] < data['month_number'].max()]
df_test = data[data['month_number'] == data['month_number'].max()]
В них создаются 2 отдельных набора данных.
df_train_val используется стандартным образом, на нем мы производим обучение, настраиваем гиперпараметры (серые ячейки на графике выше)
df_test же нужен для проверки качества модели за различные периоды (зеленые ячейки)
Проверка качества за различные промежутки времени
def declension_month(number: int) -> str:
"""
Возвращает правильное склонение слова "месяц" в зависимости от числа.
Args:
число: Целое число.
Returns:
Строка, представляющая число и правильное склонение слова "месяц".
"""
if number == 0:
return f"{number} месяцев"
elif number % 10 == 1 and number % 100 != 11:
return f"{number} месяц"
elif 2 <= number % 10 <= 4 and (number % 100 < 10 or number % 100 >= 20):
return f"{number} месяца"
else:
return f"{number} месяцев"
def find_optimal_data_window(df: pd.DataFrame, window_sizes=None, cat_features=None) -> tuple:
"""
Находит оптимальный временной период (окно данных) для обучения модели,
максимизирующий метрику AUC на тестовой выборке.
Args:
df (pd.DataFrame): DataFrame, содержащий данные для анализа.
window_sizes (Optional): Список размеров окна (в месяцах) для тестирования.
cat_features (Optional): Список названий категориальных признаков.
Returns:
Tuple: Кортеж, содержащий:
- best_window (int): Оптимальный размер окна (в месяцах).
- best_auc (float): Значение AUC на тестовой выборке для оптимального окна.
- results_test (dict): Словарь, где ключи - размеры окна (в месяцах) и значения - AUC на тестовой выборке.
- results_val (dict): Словарь, где ключи - размеры окна (в месяцах) и значения - AUC на валидационной выборке.
"""
if window_sizes is None:
window_sizes = range(1,23)
results_val = {}
results_test = {}
list_windows = sorted(df['month_number'].unique())[::-1]
for i in range(2, len(list_windows) + 1):
window = list_windows[:i]
truncated_df = df[df['month_number'].isin(window)]
window = len(window)-1
if len(truncated_df) == 0:
print(f"Предупреждение: Нет данных для окна {declension_month(window)}. Пропускаем.")
results_val[f'{window}'] = -np.inf
results_test[f'{window}'] = -np.inf
continue
try:
auc_val, auc_test = train_and_evaluate(truncated_df, cat_features=cat_features)
results_val[f'{window}'] = auc_val
results_test[f'{window}'] = auc_test
print(f"Test AUC для окна {declension_month(window)}: {auc_test}")
except Exception as e:
print(f"Ошибка при обучении модели для окна {declension_month(window)}: {e}")
results_val[f'{window}'] = -np.inf
results_test[f'{window}'] = -np.inf
best_window = max(results_test, key=results_test.get)
best_auc = results_test[best_window]
print(f"nЛучший размер окна: {declension_month(int(best_window))} с AUC: {best_auc}")
return int(best_window), best_auc, results_test, results_val
# Определяем категориальные признаки
categorical_features = ['feature_2','user_lifetime_category']
# Запускаем поиск оптимального окна
best_window, best_auc, results_test, results_val = find_optimal_data_window(df, cat_features=categorical_features)
# Визуализация
def create_plot_auc(results: dict, color:str, label:str):
"""
Создает линейный график зависимости AUC (Area Under the Curve) от размера временного окна.
Args:
results (Dict): Словарь, где ключи - размеры окна (в месяцах, представлены как строки),
а значения - соответствующие значения AUC.
color (str): Цвет линии графика.
label (str): Подпись для легенды графика.
"""
window_sizes = list(results.keys())
auc_scores = list(results.values())
plt.plot(window_sizes, auc_scores, marker='o', color=color, label=label)
create_plot_auc(results_test, '#1fa999','Test')
create_plot_auc(results_val, 'grey', 'Validate')
plt.xlabel("Размер окна (месяцы)")
plt.ylabel("AUC")
plt.title("Зависимость AUC от размера окна")
plt.grid(True)
plt.legend()
plt.show()
Вывод
train_val month numbers: [15]
test month numbers: [16]
Test AUC для окна 1 месяц: 0.6377971915713347
train_val month numbers: [15, 14]
test month numbers: [16]
Test AUC для окна 2 месяца: 0.6414627655080349
train_val month numbers: [15, 14, 13]
test month numbers: [16]
Test AUC для окна 3 месяца: 0.6438457898916845
train_val month numbers: [15, 14, 13, 12]
test month numbers: [16]
Test AUC для окна 4 месяца: 0.6444067997093563
train_val month numbers: [15, 14, 13, 12, 11]
test month numbers: [16]
Test AUC для окна 5 месяцев: 0.6434845975768431
train_val month numbers: [15, 14, 13, 12, 11, 10]
test month numbers: [16]
Test AUC для окна 6 месяцев: 0.6419768836918501
train_val month numbers: [15, 14, 13, 12, 11, 10, 9]
test month numbers: [16]
Test AUC для окна 7 месяцев: 0.641621183189983
train_val month numbers: [15, 14, 13, 12, 11, 10, 9, 8]
test month numbers: [16]
Test AUC для окна 8 месяцев: 0.6432666148464828
train_val month numbers: [15, 14, 13, 12, 11, 10, 9, 8, 7]
test month numbers: [16]
Test AUC для окна 9 месяцев: 0.6436468172831579
train_val month numbers: [15, 14, 13, 12, 11, 10, 9, 8, 7, 6]
test month numbers: [16]
Test AUC для окна 10 месяцев: 0.6409545615843457
train_val month numbers: [15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5]
test month numbers: [16]
Test AUC для окна 11 месяцев: 0.6414323493131009
train_val month numbers: [15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4]
test month numbers: [16]
Test AUC для окна 12 месяцев: 0.6400243329559472
train_val month numbers: [15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3]
test month numbers: [16]
Test AUC для окна 13 месяцев: 0.6424618530221868
train_val month numbers: [15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2]
test month numbers: [16]
Test AUC для окна 14 месяцев: 0.6423727166731442
train_val month numbers: [15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
test month numbers: [16]
Test AUC для окна 15 месяцев: 0.6413089947447574
Лучший размер окна: 4 месяца с AUC: 0.6444067997093563

Отлично! Мы нашли оптимальный период, за который следует брать данные. В нашем случае это 4 месяца. Заметьте, что если бы мы использовали только валидационные данные, то получили бы лучший результат уже в первый месяц, при этом, это был бы худший результат на самых свежих данных.
3 Сдвиг окна данных
После этого будет не лишним сделать несколько сдвигов окна данных для обучения и для теста на месяц и провести повторное обучение модели.
Это нужно, для того что бы:
-
Выявить сезонность и тренды. Возможно, данные за конкретные N месяцев, которые вы выбрали изначально, содержат какую-то сезонную особенность или временный тренд, который не будет сохраняться в будущем, и вы не учли это при создании модели. Сдвигая окно, вы проверяете, является ли ваша модель стабильной в разных временных интервалах.
-
Устойчивость к изменениям. Сдвиг окна помогает понять, насколько ваша модель устойчива к небольшим изменениям в распределении данных. Если модель хорошо работает с разными окнами, это говорит о её лучшей обобщающей способности.
-
Оценка потенциального дрейфа. Сравнивая результаты обучения на разных сдвинутых окнах, вы также можете оценить, насколько сильно меняется распределение данных со временем. Это даст вам представление о потенциальном дрейфе модели в будущем.
-
Возможно, участок, который мы выбрали для тестового набора, хранит недостаточно информации (к примеру у вас огромный дисбаланс и из-за этого тест хранит данные только по одному классу)
В общем, это даст вам больше понимания, как работает ваша модель на данных приближенных к реальным, и при желании, обладая данной информации, вы можете оптимизировать вашу модель.
Выглядеть это может следующим образом

Проверка качества модели при сдвиге
results = {}
list_windows = sorted(df['month_number'].unique())[::-1]
for i in range(1, df['month_number'].max()):
window = list_windows[i:i+best_window+1]
if len(window) > best_window:
truncated_df = df[df['month_number'].isin(window)]
# Определяем категориальные признаки
categorical_features = ['feature_2','user_lifetime_category']
auc_val, auc_test = train_and_evaluate(truncated_df, cat_features=categorical_features)
results[i] = auc_test
print(f"Test AUC для окна {best_window} и сдвига {i} (месяцы): {auc_test}")
# Визуализация
window_sizes = list(results.keys())
auc_scores = list(results.values())
plt.plot(window_sizes, auc_scores, marker='o', color='#1fa999')
plt.xlabel("Размер окна (месяцы)")
plt.ylabel("AUC")
plt.title("Зависимость AUC от размера окна")
plt.xticks(window_sizes)
plt.grid(True)
plt.show()
Вывод
train_val month numbers: [14, 13, 12, 11]
test month numbers: [15]
Test AUC для окна 4 и сдвига 1 (месяцы): 0.6599089542016556
train_val month numbers: [13, 12, 11, 10]
test month numbers: [14]
Test AUC для окна 4 и сдвига 2 (месяцы): 0.6504045645215801
train_val month numbers: [12, 11, 10, 9]
test month numbers: [13]
Test AUC для окна 4 и сдвига 3 (месяцы): 0.6577949019926359
train_val month numbers: [11, 10, 9, 8]
test month numbers: [12]
Test AUC для окна 4 и сдвига 4 (месяцы): 0.6481294966715759
train_val month numbers: [10, 9, 8, 7]
test month numbers: [11]
Test AUC для окна 4 и сдвига 5 (месяцы): 0.6521873563839853
train_val month numbers: [9, 8, 7, 6]
test month numbers: [10]
Test AUC для окна 4 и сдвига 6 (месяцы): 0.6497580844955477
train_val month numbers: [8, 7, 6, 5]
test month numbers: [9]
Test AUC для окна 4 и сдвига 7 (месяцы): 0.6570639452363334
train_val month numbers: [7, 6, 5, 4]
test month numbers: [8]
Test AUC для окна 4 и сдвига 8 (месяцы): 0.612181590873574
train_val month numbers: [6, 5, 4, 3]
test month numbers: [7]
Test AUC для окна 4 и сдвига 9 (месяцы): 0.6328177409436494
train_val month numbers: [5, 4, 3, 2]
test month numbers: [6]
Test AUC для окна 4 и сдвига 10 (месяцы): 0.6273907212030965
train_val month numbers: [4, 3, 2, 1]
test month numbers: [5]
Test AUC для окна 4 и сдвига 11 (месяцы): 0.6412774688552638

Для нашей модели мы видим довольно значительную просадку для 8 месяца и далее, это в свою очередь говорит нам о том, что наша модель может иметь проблемы с устойчивостью, при желании мы могли бы попытаться оптимизировать эту кривую, к примеру, попробовать использовать промежуток не 4 месяца, а какой то другой, возможно, он будет более стабильным, хоть и с худшим качеством, но допустим сейчас нас устраивает и такой результат.
Нам осталось только выяснить как быстро может устаревать наша модель.
4 Скорость устаревания модели
На этапе создания модели мы можем выяснить примерную скорость ее устаревания.
Для этого нужно протестировать, насколько хорошо модель справляется со своей задачей через n промежутков времени.

Как видно из графика, в первый и второй месяц качество модели находится на хорошем уровне, далее качество резко снизилось, то есть можно сделать вывод, что скорость устаревание модели в данном случае – 2 месяца, после чего ее нужно будет переобучить.
Эта информация даст нам только примерное представление о том, как часто нужно переобучать нашу модель, в реальности же распределение данных может в какой то момент резко измениться, и нам придется раньше переобучить модель. То есть полученная на этом информация не отменяет необходимость дальнейшего мониторинга модели.
Вернемся к нашему примеру с фродом
Нахождение скорости устаревания модели
results = {}
list_windows = sorted(df['month_number'].unique())[::-1]
window = list_windows[-best_window:]
list_windows = list_windows[:-best_window][::-1]
count_shift = 0
for i in list_windows:
window.append(i)
truncated_df = df[df['month_number'].isin(window)]
window.pop()
count_shift += 1
# Определяем категориальные признаки
categorical_features = ['feature_2','user_lifetime_category']
auc_val, auc_test = train_and_evaluate(truncated_df, cat_features=categorical_features)
results[count_shift] = auc_test
print(f"Test AUC для окна {best_window} и сдвига {count_shift} (месяцы): {auc_test}")
# Визуализация
window_sizes = list(results.keys())
auc_scores = list(results.values())
plt.plot(window_sizes, auc_scores, marker='o', color='#1fa999')
plt.xlabel("Размер окна (месяцы)")
plt.ylabel("AUC")
plt.title("Зависимость AUC от размера окна")
plt.xticks(window_sizes)
plt.grid(True)
plt.show()
Вывод
train_val month numbers: [4, 3, 2, 1]
test month numbers: [5]
Test AUC для окна 4 и сдвига 1 (месяцы): 0.6412774688552638
train_val month numbers: [4, 3, 2, 1]
test month numbers: [6]
Test AUC для окна 4 и сдвига 2 (месяцы): 0.6333972267106001
train_val month numbers: [4, 3, 2, 1]
test month numbers: [7]
Test AUC для окна 4 и сдвига 3 (месяцы): 0.6205731495974957
train_val month numbers: [4, 3, 2, 1]
test month numbers: [8]
Test AUC для окна 4 и сдвига 4 (месяцы): 0.597177684013127
train_val month numbers: [4, 3, 2, 1]
test month numbers: [9]
Test AUC для окна 4 и сдвига 5 (месяцы): 0.6200572859223685
train_val month numbers: [4, 3, 2, 1]
test month numbers: [10]
Test AUC для окна 4 и сдвига 6 (месяцы): 0.6161048689138576
train_val month numbers: [4, 3, 2, 1]
test month numbers: [11]
Test AUC для окна 4 и сдвига 7 (месяцы): 0.6064169324970938
train_val month numbers: [4, 3, 2, 1]
test month numbers: [12]
Test AUC для окна 4 и сдвига 8 (месяцы): 0.611823717402049
train_val month numbers: [4, 3, 2, 1]
test month numbers: [13]
Test AUC для окна 4 и сдвига 9 (месяцы): 0.6155974749115588
train_val month numbers: [4, 3, 2, 1]
test month numbers: [14]
Test AUC для окна 4 и сдвига 10 (месяцы): 0.587076973092571
train_val month numbers: [4, 3, 2, 1]
test month numbers: [15]
Test AUC для окна 4 и сдвига 11 (месяцы): 0.6087017412601989
train_val month numbers: [4, 3, 2, 1]
test month numbers: [16]
Test AUC для окна 4 и сдвига 12 (месяцы): 0.5920875648456377

Как видно из графика, с первых же месяцев качество нашей модели значительно снижается, в нашем случае мы решим, что минимальное качество, которое нас устраивает – 0.62
Таким образом, периодом, за который наша модель устареет исходя из исторических данных, является 3 месяца.
Заключение
Понимание того, как в процессе разработки мы можем минимизировать риски, связанные с потерей точности, является ключом к созданию более устойчивых и эффективных моделей. В этой статье мы обсудили и применили на практике ряд подходов, которые могут помочь в борьбе с устареванием на стадии создания моделей.
Мы получили много полезной информации о нашей модели:
-
Узнали о подходах работы с признаками для уменьшения устаревания и применили один из них на практике
-
Нашли промежуток времени, на котором модель показывает наилучшее качество
-
Оценили, насколько устойчива оказалась полученная модель
-
Узнали приблизительный срок устаревания нашей модели
Однако на этом работа с устареванием не заканчивается. Далее, при эксплуатации полученной модели нам нужно будет настроить мониторинг, который будет следить за сдвигом распределений, наличием некорректных значений и качеством нашей модели, а также создать систему для переобучения.
Весь код используемый в статье: articles/model_quality_monitoring/code_for_article_part_2.ipynb at main · PavelShunkevich/articles
Автор: pavel_shunkevich