Загрузка данных


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.')