Загрузка данных
import pandas as pd
import numpy as np
from sklearn.linear_model import LogisticRegression
def build_features(train_df, test_df):
train_len = len(train_df)
target_col = 'campaign_success'
df = pd.concat(
[train_df.drop(columns=[target_col]), test_df],
axis=0,
ignore_index=True
)
cat_cols = ['category', 'country', 'currency', 'font_header']
base_num_cols = [c for c in df.columns if c not in cat_cols]
new_cols = {}
# 1. Индикаторы пропусков и общее число пропусков в строке
for col in df.columns:
if df[col].isnull().any():
new_cols[f'{col}_isna'] = df[col].isnull().astype(float)
new_cols['missing_count'] = df.isnull().sum(axis=1).astype(float)
# 2. Доменные нелинейные признаки до заполнения пропусков
new_cols['log_goal_usd'] = np.log1p(df['goal_usd'])
new_cols['goal_per_day'] = df['goal_usd'] / (df['duration_days'] + 1.0)
new_cols['log_goal_per_day'] = np.log1p(new_cols['goal_per_day'])
new_cols['log_duration'] = np.log1p(df['duration_days'])
# 3. Заполнение числовых пропусков медианой, посчитанной строго по train
for col in base_num_cols:
if df[col].isnull().any():
med = df[col].iloc[:train_len].median()
df[col] = df[col].fillna(med)
# 4. Взаимодействия и предметные признаки после импутации
new_cols['creator_expected_success'] = (
df['creator_projects_count'] * df['creator_success_rate']
)
new_cols['social_total'] = df['social_media_shares'] + df['social_media_likes']
new_cols['social_ratio'] = df['social_media_shares'] / (df['social_media_likes'] + 1.0)
new_cols['log_social_shares'] = np.log1p(df['social_media_shares'])
new_cols['log_social_likes'] = np.log1p(df['social_media_likes'])
new_cols['media_richness'] = df['has_video'] + np.log1p(df['image_count'])
# Признаки цветового профиля (яркость и контраст каналов)
rgb = df[['color_primary_r', 'color_primary_g', 'color_primary_b']]
new_cols['color_brightness'] = rgb.mean(axis=1)
new_cols['color_spread'] = rgb.max(axis=1) - rgb.min(axis=1)
# Циклические признаки времени запуска
new_cols['month_sin'] = np.sin(2 * np.pi * df['launch_month'] / 12.0)
new_cols['month_cos'] = np.cos(2 * np.pi * df['launch_month'] / 12.0)
new_cols['dow_sin'] = np.sin(2 * np.pi * df['launch_day_of_week'] / 7.0)
new_cols['dow_cos'] = np.cos(2 * np.pi * df['launch_day_of_week'] / 7.0)
new_cols['hour_sin'] = np.sin(2 * np.pi * df['launch_hour'] / 24.0)
new_cols['hour_cos'] = np.cos(2 * np.pi * df['launch_hour'] / 24.0)
# Квантильные бины для признаков с выраженной немонотонностью
non_monotonic_cols = [
'goal_usd', 'duration_days', 'creator_success_rate', 'image_count',
'project_title_length', 'project_title_caps_ratio',
'description_word_count', 'description_sentiment_score',
'social_media_shares', 'social_media_likes',
'color_primary_r', 'color_primary_g', 'color_primary_b', 'url_length'
]
for col in non_monotonic_cols:
s_tr = train_df[col].dropna()
quantiles = np.unique(np.quantile(s_tr, np.linspace(0, 1, 11)))
for k in range(len(quantiles) - 1):
low, high = quantiles[k], quantiles[k + 1]
if k == len(quantiles) - 2:
cond = (train_df[col].reindex(df.index) if False else df[col] >= low) & (df[col] <= high)
else:
cond = (df[col] >= low) & (df[col] < high)
new_cols[f'{col}_bin_{k}'] = cond.astype(float)
df = pd.concat([df, pd.DataFrame(new_cols, index=df.index)], axis=1)
# 5. One-Hot Encoding категориальных признаков
for col in cat_cols:
df[col] = df[col].fillna('Missing')
df = pd.get_dummies(df, columns=cat_cols, drop_first=False, dtype=float)
# 6. Попарные полиномиальные взаимодействия ключевых признаков
key_vars = [
'log_goal_usd', 'duration_days', 'creator_success_rate', 'is_staff_pick',
'has_video', 'reward_tiers', 'social_media_shares', 'social_media_likes',
'description_sentiment_score', 'image_count', 'creator_projects_count', 'team_size'
]
poly_cols = {}
for i in range(len(key_vars)):
for j in range(i, len(key_vars)):
v1, v2 = key_vars[i], key_vars[j]
poly_cols[f'{v1}_x_{v2}'] = df[v1] * df[v2]
df = pd.concat([df, pd.DataFrame(poly_cols, index=df.index)], axis=1)
# 7. Стандартизация по статистикам тренировочной выборки
X_train = df.iloc[:train_len].copy()
X_test = df.iloc[train_len:].copy()
means = X_train.mean(axis=0)
stds = X_train.std(axis=0).replace(0, 1.0)
X_train = (X_train - means) / stds
X_test = (X_test - means) / stds
return X_train, X_test
def macro_f1_numpy(y_true, y_pred):
f1s = []
for cls in (0, 1):
tp = np.sum((y_true == cls) & (y_pred == cls))
fp = np.sum((y_true != cls) & (y_pred == cls))
fn = np.sum((y_true == cls) & (y_pred != cls))
denom = 2 * tp + fp + fn
f1s.append((2 * tp / denom) if denom > 0 else 0.0)
return 50.0 * (f1s[0] + f1s[1])
if __name__ == '__main__':
train = pd.read_csv('train.csv')
test = pd.read_csv('test.csv')
y_train = train['campaign_success'].values
X_train, X_test = build_features(train, test)
# Быстрый 5-fold CV (через numpy без доп. импортов) для выбора оптимального порога
rng = np.random.RandomState(67)
indices = np.arange(len(y_train))
rng.shuffle(indices)
folds = np.array_split(indices, 5)
oof_probs = np.zeros(len(y_train))
for k in range(5):
val_idx = folds[k]
tr_idx = np.setdiff1d(indices, val_idx)
clf = LogisticRegression(C=0.1, max_iter=1000, random_state=67)
clf.fit(X_train.iloc[tr_idx], y_train[tr_idx])
oof_probs[val_idx] = clf.predict_proba(X_train.iloc[val_idx])[:, 1]
best_th, best_score = 0.5, 0.0
for th in np.linspace(0.30, 0.55, 251):
score = macro_f1_numpy(y_train, (oof_probs >= th).astype(int))
if score > best_score:
best_score = score
best_th = th
print(f'CV Macro F1 * 100: {best_score:.4f} (optimal threshold = {best_th:.4f})')
# Обучение финальной модели на всём train
final_model = LogisticRegression(C=0.1, max_iter=1000, random_state=67)
final_model.fit(X_train, y_train)
test_probs = final_model.predict_proba(X_test)[:, 1]
predictions = (test_probs >= best_th).astype(int)
submission = pd.DataFrame({'campaign_success': predictions})
submission.to_csv('submission.csv', index=False)
print('Saved submission.csv successfully.')