Условие
Три задания на A1 Junior:
- По «Time series» (ежедневный ряд
series12015+) построить модель прогноза на 3 мес. Объяснить выбор, дать оценку качества. - По датасету «Training» обучить классификатор
Target, предсказать на «Validate». Пояснить выбор, привести точность, ROC-кривую, top-3 features. - По телко-данным (
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.
Подводные камни
- Outlier в Time series (Day 41 = 236.9 vs 2500 mean) — нужна detection и корректная обработка (replace with median / robust SARIMA).
- Пропуски дат: проверить, есть ли gaps (
asfreq('D')). - Train/Validate на task 2 — если они не stratified по целевому, придётся проверить распределение Target.
- Сезонность Charges: счёт за декабрь обычно выше — pre/post может быть искажено сезонностью; используйте YoY.
- Multiple migrations: один абонент мог менять тариф несколько раз; в анализе берите либо все, либо первый, либо последний — единое правило.
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; визуализация.