**Данные:** Продажи по товарам до и после запуска нового продукта. DataFrame `sales` с колонками: `date`, `product_id`, `category`, `revenue`, `units_sold`. Новый продукт (category='new') запущен в конкретную дату.
**Задание:**
1. Определите treatment group (товары той же категории, что и новый продукт) и control group (товары других категорий)
2. Реализуйте DiD-оценку эффекта каннибализации
3. Проверьте parallel trends assumption на pre-period
4. Оцените размер каннибализации в рублях и процентах
Структура для ориентира — реальные значения из эталонного решения.
import pandas as pd
import numpy as np
from scipy import stats
np.random.seed(42)
# Генерация данных
dates = pd.date_range('2024-01-01', '2024-06-30', freq='D')
launch_date = pd.Timestamp('2024-04-01')
products = {
'phone_A': ('electronics', 50000), # treatment — конкуренты нового
'phone_B': ('electronics', 40000),
'tablet_A': ('electronics', 35000),
'shirt_A': ('clothing', 20000), # control
'shirt_B': ('clothing', 15000),
'shoes_A': ('clothing', 25000),
'book_A': ('books', 5000), # control
'book_B': ('books', 8000),
}
rows = []
for date in dates:
day_idx = (date - dates[0]).days
for product, (category, base_rev) in products.items():
trend = base_rev * (1 + 0.0005 * day_idx)
seasonal = base_rev * 0.05 * np.sin(2 * np.pi * day_idx / 7)
noise = np.random.normal(0, base_rev * 0.08)
revenue = trend + seasonal + noise
# Эффект каннибализации: electronics -12% после запуска нового
if category == 'electronics' and date >= launch_date:
revenue *= 0.88
units = max(1, int(revenue / np.random.uniform(800, 2000)))
rows.append({
'date': date, 'product_id': product,
'category': category, 'revenue': round(revenue, 2),
'units_sold': units,
})
# Новый продукт
for date in dates[dates >= launch_date]:
day_idx = (date - launch_date).days
revenue = 30000 + 200 * day_idx + np.random.normal(0, 3000)
rows.append({
'date': date, 'product_id': 'phone_new',
'category': 'electronics_new', 'revenue': round(revenue, 2),
'units_sold': int(revenue / 1500),
})
df = pd.DataFrame(rows)
# --- 1. Treatment vs Control ---
# Исключаем новый продукт из анализа
analysis = df[df['category'] != 'electronics_new'].copy()
analysis['group'] = np.where(analysis['category'] == 'electronics', 'treatment', 'control')
analysis['post'] = (analysis['date'] >= launch_date).astype(int)
# Агрегация по группе и дню
daily = analysis.groupby(['date', 'group', 'post']).agg(
revenue=('revenue', 'sum'),
).reset_index()
# --- 2. DiD-оценка ---
means = daily.groupby(['group', 'post'])['revenue'].mean().unstack()
means.columns = ['pre', 'post']
means['diff'] = means['post'] - means['pre']
did_estimate = means.loc['treatment', 'diff'] - means.loc['control', 'diff']
did_pct = did_estimate / means.loc['treatment', 'pre'] * 100
print("=== Difference-in-Differences ===")
print(means.round(0))
print(f"\nDiD estimate: {did_estimate:+,.0f} руб./день")
print(f"DiD % от treatment pre: {did_pct:+.1f}%")
# --- 3. Parallel Trends Check ---
pre_data = daily[daily['post'] == 0].copy()
pre_data['day_idx'] = (pre_data['date'] - pre_data['date'].min()).dt.days
pre_data['is_treatment'] = (pre_data['group'] == 'treatment').astype(int)
pre_data['interaction'] = pre_data['day_idx'] * pre_data['is_treatment']
from numpy.linalg import lstsq
X_pt = pre_data[['day_idx', 'is_treatment', 'interaction']].values
X_pt = np.column_stack([np.ones(len(X_pt)), X_pt])
y_pt = pre_data['revenue'].values
coefs, _, _, _ = lstsq(X_pt, y_pt, rcond=None)
print(f"\n=== Parallel Trends Check (pre-period) ===")
print(f"Interaction coefficient: {coefs[3]:.2f}")
print(f"Parallel trends {'подтверждены' if abs(coefs[3]) < 50 else 'НАРУШЕНЫ'} "
f"(interaction ≈ 0)")
# --- 4. Статистическая значимость DiD ---
treat_pre = daily[(daily['group'] == 'treatment') & (daily['post'] == 0)]['revenue']
treat_post = daily[(daily['group'] == 'treatment') & (daily['post'] == 1)]['revenue']
ctrl_pre = daily[(daily['group'] == 'control') & (daily['post'] == 0)]['revenue']
ctrl_post = daily[(daily['group'] == 'control') & (daily['post'] == 1)]['revenue']
treat_diff = treat_post.values - np.mean(treat_pre.values)
ctrl_diff = ctrl_post.values - np.mean(ctrl_pre.values)
# Welch t-test на разности
t_stat, p_value = stats.ttest_ind(treat_diff[:len(ctrl_diff)], ctrl_diff, equal_var=False)
print(f"\nWelch t-test: t={t_stat:.3f}, p={p_value:.4f}")
print(f"Значимо при alpha=0.05: {'Да' if p_value < 0.05 else 'Нет'}")
# --- 5. Размер каннибализации ---
post_days = (dates[-1] - launch_date).days + 1
total_cannibalization = did_estimate * post_days
new_product_revenue = df[df['category'] == 'electronics_new']['revenue'].sum()
net_effect = new_product_revenue + total_cannibalization
print(f"\n=== Оценка каннибализации ===")
print(f"Revenue нового продукта: {new_product_revenue:>12,.0f} руб.")
print(f"Каннибализация (оценка DiD): {total_cannibalization:>12,.0f} руб.")
print(f"Чистый эффект: {net_effect:>12,.0f} руб.")
print(f"Каннибализация/New revenue: {abs(total_cannibalization)/new_product_revenue*100:.1f}%")
pandas DiD difference-in-differences каннибализация statsmodels
Это задание для уровня Senior. Senior-уровень — глубокое понимание темы, опыт решения нестандартных задач, обсуждение trade-off на собеседовании.
Подобные задания в категории «Python» регулярно дают на собеседованиях аналитика данных в Яндекс, Сбер, Ozon, Авито, Тинькофф, Wildberries, T-Bank, X5, ВТБ и других крупных IT-компаниях. Тематика: pandas, DiD, difference-in-differences, каннибализация, statsmodels.
На реальном собеседовании на подобную задачу отводится 30-60 минут с обсуждением подходов, оптимизаций и trade-off. Для тренировки рекомендуем сначала решить самостоятельно, потом сверить с эталонным решением и подсказками.
На zasqlpython.ru есть 482 Python задачи с проверкой через Pyodide, конспекты Python и pandas, AI мок-собеседование с разбором ваших ответов.
← Все задания