Собесов

A1 Junior — прогноз ряда, классификация Target и анализ миграций тарифов

ML / Data ScienceTime series / classification / migration analysisСредняяJunior

Условие

Три задания на A1 Junior:

  1. По «Time series» (ежедневный ряд series1 2015+) построить модель прогноза на 3 мес. Объяснить выбор, дать оценку качества.
  2. По датасету «Training» обучить классификатор Target, предсказать на «Validate». Пояснить выбор, привести точность, ROC-кривую, top-3 features.
  3. По телко-данным (Tariff_plans_change, Charges, Suspended):
    • Какие миграции тарифов были самыми частыми (визуализация Sankey)?
    • Изменился ли средний счёт абонентов за 3 мес после смены vs 3 мес до?
    • Изменился ли уровень блокировок (по тем же периодам)?

Решение

1. Прогноз time-series

EDA: проверить стационарность (ADF), сезонность (auto-ACF), тренд.

Предполагаем недельную сезонность (типичный паттерн для telco/utility).

Модели:

  • Baseline: SARIMA(p,d,q)(P,D,Q,7).
  • ETS / Holt-Winters.
  • Prophet (хорошо для daily + multiple seasonalities).
  • LightGBM на лагах (advanced).
from statsmodels.tsa.statespace.sarimax import SARIMAX
from sklearn.metrics import mean_absolute_error, mean_absolute_percentage_error
 
train = ts['2015-01-01':'2016-12-31']
test  = ts['2017-01-01':'2017-03-31']
 
model = SARIMAX(train, order=(1,1,1), seasonal_order=(1,1,1,7))
res   = model.fit()
pred  = res.forecast(len(test))
 
mape = mean_absolute_percentage_error(test, pred)
mae  = mean_absolute_error(test, pred)

Quality: MAPE / MAE / RMSE на back-test (последние 3 мес перед прогнозом).

Pояснение выбора: SARIMA — простая, интерпретируемая, подходит для daily с weekly seasonality. Если есть exogenous variables (праздники, погода) — SARIMAX.

2. Классификация Target

from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score, roc_curve, classification_report
import lightgbm as lgb
 
train = pd.read_excel('Задания_1_2.xlsx', sheet_name='Training')
val   = pd.read_excel('Задания_1_2.xlsx', sheet_name='Validate')
 
X = train.drop(columns=['Target'])
y = train['Target']
 
X_tr, X_te, y_tr, y_te = train_test_split(X, y, stratify=y, test_size=0.2)
 
m = lgb.LGBMClassifier(n_estimators=500, learning_rate=0.05)
m.fit(X_tr, y_tr)
 
# Оценка
y_proba = m.predict_proba(X_te)[:, 1]
print('ROC-AUC:', roc_auc_score(y_te, y_proba))
 
# ROC curve
fpr, tpr, _ = roc_curve(y_te, y_proba)
plt.plot(fpr, tpr); plt.plot([0,1],[0,1],'--')
 
# Топ-3 фичи
importances = pd.Series(m.feature_importances_, index=X.columns)
print(importances.nlargest(3))
 
# Прогноз на Validate
val_pred = m.predict(val.drop(columns=['Target'], errors='ignore'))

Выбор: LightGBM — стандарт для табличных задач, не требует масштабирования, хорошо работает с пропусками.

Метрики: ROC-AUC primary (особенно при дисбалансе классов), Precision/Recall/F1 — secondary; PR-AUC если сильный disbalance.

3. Анализ миграций тарифов

# Перетоки: для каждого абонента — финальный тариф (1 полугодие) и предыдущий
tpc = pd.read_csv('Tariff_plans_change.csv')
 
# Получаем пары (from_tariff, to_tariff)
tpc = tpc.sort_values(['SUBSCRIBER_ID', 'START_DTTM'])
tpc['prev_tariff'] = tpc.groupby('SUBSCRIBER_ID')['TARIFF_PLAN_ID'].shift(1)
migrations = tpc.dropna(subset=['prev_tariff'])
 
flows = migrations.groupby(['prev_tariff', 'TARIFF_PLAN_ID']).size().reset_index(name='count')
 
# Sankey diagram (plotly)
import plotly.graph_objects as go
nodes = list(set(flows['prev_tariff']) | set(flows['TARIFF_PLAN_ID']))
node_idx = {n: i for i, n in enumerate(nodes)}
fig = go.Figure(data=[go.Sankey(
    node=dict(label=nodes),
    link=dict(
        source=flows['prev_tariff'].map(node_idx),
        target=flows['TARIFF_PLAN_ID'].map(node_idx),
        value=flows['count']
    )
)])

Изменение среднего счёта:

charges = pd.read_csv('Charges.csv')
charges['BILL_MONTH'] = pd.to_datetime(charges['BILL_MONTH'])
 
# Для каждого migrant — найти месяц смены
mig_users = migrations[['SUBSCRIBER_ID', 'prev_tariff', 'TARIFF_PLAN_ID', 'START_DTTM']]
mig_users['change_month'] = pd.to_datetime(mig_users['START_DTTM']).dt.to_period('M')
 
# Pre/post 3 months
def avg_charges_around(user, change_month, charges):
    pre  = charges[(charges['SUBSCRIBER_ID'] == user) &
                   (charges['BILL_MONTH'] >= change_month.to_timestamp() - pd.DateOffset(months=3)) &
                   (charges['BILL_MONTH'] <  change_month.to_timestamp())]['CHARGES'].mean()
    post = charges[(charges['SUBSCRIBER_ID'] == user) &
                   (charges['BILL_MONTH'] > change_month.to_timestamp()) &
                   (charges['BILL_MONTH'] <= change_month.to_timestamp() + pd.DateOffset(months=3))]['CHARGES'].mean()
    return pre, post
 
# Aggregate by direction
mig_users['pre'], mig_users['post'] = zip(*mig_users.apply(
    lambda r: avg_charges_around(r['SUBSCRIBER_ID'], r['change_month'], charges), axis=1))
 
mig_users['delta'] = mig_users['post'] - mig_users['pre']
 
direction_summary = mig_users.groupby(['prev_tariff', 'TARIFF_PLAN_ID']).agg(
    n=('SUBSCRIBER_ID', 'count'),
    mean_pre=('pre', 'mean'),
    mean_post=('post', 'mean'),
    delta=('delta', 'mean')
).sort_values('delta', ascending=False)

Блокировки (Suspended.csv) — аналогично: считаем число блокировок в (−3, 0) и (0, +3) месяцев от change_month, сравниваем mean.

Подводные камни

  1. Outlier в Time series (Day 41 = 236.9 vs 2500 mean) — нужна detection и корректная обработка (replace with median / robust SARIMA).
  2. Пропуски дат: проверить, есть ли gaps (asfreq('D')).
  3. Train/Validate на task 2 — если они не stratified по целевому, придётся проверить распределение Target.
  4. Сезонность Charges: счёт за декабрь обычно выше — pre/post может быть искажено сезонностью; используйте YoY.
  5. Multiple migrations: один абонент мог менять тариф несколько раз; в анализе берите либо все, либо первый, либо последний — единое правило.
  6. END_DTTM = NULL ($null$): текущий тариф; не путайте с unknown.

Эталонный ответ

(1) SARIMA(1,1,1)(1,1,1,7) + бэктест с MAPE. (2) LightGBM на default fields, ROC-AUC, top-3 features из feature_importances_. (3) Pairs (prev_tariff, TARIFF_PLAN_ID) → Sankey; pre/post 3 months for charges and suspensions; aggregate by direction; визуализация.

Хочешь увидеть разбор?

Зарегистрируйся бесплатно — откроется развёрнутое решение этой задачи и ещё 4 на выбор.

Зарегистрироваться и увидеть разбор
Уже есть аккаунт? Войти