<jupyter_start><jupyter_text>Для восстановления Male/Female соберем данные train/test:Сюда попадут только single-family. Для train отберем размеченные, для test - неразмеченные.Так же помним про троих non-single, которым надо восстановить hh_comp и kids.<jupyter_code># для восстановления Male/Female соберем данные в отдельные датасеты
sex_whole = data[~sex & single].drop(columns=['single_female']).copy()
sex_target = data.loc[~sex & single, 'single_female'].copy()
sex_test = data[sex & single].drop(columns=['single_female']).copy()

# отделим кусочек на валидацию
sex_train, sex_valid, sex_train_true, sex_valid_true = train_test_split(sex_whole, sex_target, test_size=0.25, random_state=13)

class Merger(BaseEstimator, TransformerMixin):
    """ Добавление данных о топе покупок """
    def __init__(self, k=10):
        self.k = k
        self.purchases = None

    def fit(self, X, y=None):
        dp = DataProcessor(pd.read_csv('retail_train.csv'), top_config={'k': self.k}, uim_config={'aggfunc': 'count'})
        dp.fit()
        self.purchases = ((dp.train_uim > 0) * 1).drop(columns=[0]).astype('int')
        self.purchases.columns = self.purchases.columns.astype('str')        
        return self
    
    def transform(self, X):
        return X.merge(self.purchases, on='user_id', how='left')
class Dummies(BaseEstimator, TransformerMixin):
    """ Добавление dummies из указанных фичей """
    def __init__(self, dumcol):
        self.dumcol = dumcol

    def fit(self, X, y=None):
        self.dumcol = [col for col in self.dumcol if col in X.columns]
        return self
    
    def transform(self, X):
        dum = pd.get_dummies(X[self.dumcol].astype('str'))
        df = pd.concat([X, dum], axis=1)
        return df
class ColumnsCorrector(BaseEstimator, TransformerMixin):
    """ Выброс фичей """
    def __init__(self, dropcol):
        self.dropcol = dropcol
        self.required_order = None

    def fit(self, X, y=None):
        self.dropcol = [col for col in self.dropcol if col in X.columns]
        self.required_order = [col for col in X.columns if col not in self.dropcol]
        return self
    
    def transform(self, X):
        df = X.copy()
        absence = list(set(X.columns) ^ set(self.required_order))
        df[absence] = 0
        df.drop(columns=self.dropcol, inplace=True)
        return df[self.required_order]
prepare = Pipeline([('merger', Merger(k=200)),
                    ('dummies', Dummies(['age', 'marital', 'income'])),
                    ('colcorrect', ColumnsCorrector(dropcol=['age', 'marital', 'income', 'homeowner', 'user_id'])),
                    ('classifier', SGDClassifier(early_stopping=True,
                                                 validation_fraction=0.5,
                                                 max_iter=100,
                                                #  alpha=0.01,
                                                 loss='log',
                                                 learning_rate='adaptive',
                                                 eta0=5e-4,
                                                 n_jobs=-1, random_state=2022))
                    ])

# обучаем моделечку
prepare.fit(sex_train, sex_train_true)

# считаем первое приближение метрик
print_metric(sex_train_true, prepare.predict(sex_train), 'Train')

# valivadion PR curve
beta = 1
valid_proba = prepare.predict_proba(sex_valid).T[1]
pr, rec, thr = precision_recall_curve(sex_valid_true, valid_proba)
f_beta = (1 + beta ** 2) * pr * rec / (beta ** 2 * pr + rec)
idx = f_beta.argmax()
threshold = thr[idx]
plt.suptitle('validation PR curve')
plt.xlabel('recall')
plt.ylabel('precision')
plt.ylim((0, 1.1))
plt.plot(rec, pr)

# итоговая метрика
# valid_proba = prepare.predict_proba(sex_valid).T[1]
valid_pred = (valid_proba > threshold) * 1
print_metric(sex_valid_true, valid_pred, 'Final metrics')
ConfusionMatrixDisplay.from_predictions(sex_valid_true, valid_pred);<jupyter_output><empty_output><jupyter_text>Результат: модель определяет практически всех single женщин, но часто ошибочно называет single мужчин женщинами.Учитывая, что данных для обучения очень мало, результат считаю приемлемым.<jupyter_code># заполняем исходные пропуски предиктами
test_pred = (prepare.predict_proba(sex_test) > threshold) * 1
data.loc[sex & single, 'single_female'] = test_pred.T[1]

# разбиваем на dummies
male = data['single_female'] == 0
data['single_male'] = 0
data.loc[male, 'single_male'] = 1
non_single = data['single_female'] == -1
data.loc[non_single, 'single_female'] = 0
# этот признак тоже можно попробовать починить, если будет время
data['homeowner'].value_counts()
# сразу подготовим данные для дальнейшего использования
# поскольку все переделываем в dummies, hh_single и hh_comp больше не нужны - они продублируются hh_size_1 и kids
# final_pipe = Pipeline([('dummies', Dummies(['age', 'marital', 'income', 'homeowner', 'hh_size', 'kids'])),
#                        ('colcorrect', ColumnsCorrector(dropcol=['age', 'marital', 'income', 'homeowner', 'hh_comp', 'hh_size', 'kids']))])

# final_pipe.fit(data)
# prepared = final_pipe.transform(data)
# подготовка - вариант 2
final_pipe = Pipeline([('dummies', Dummies(['age', 'marital', 'income'])),
                       ('colcorrect', ColumnsCorrector(dropcol=['age', 'marital', 'income', 'homeowner', 'hh_single']))])

final_pipe.fit(data)
prepared = final_pipe.transform(data)
# сохраняем результат
prepared.to_csv('user_features_corrected.csv', index=False)

# final overview
prepared<jupyter_output><empty_output><jupyter_text>Что НЕ сделано:1) не обработаны те трое, с неуказанными hh_comp и kids: пока непонятно, как выяснить, это 2 взрослых или single + kid2) никак не обрабатываются Unknown в homeowner: их примерно треть от всех данных и очень большой дисбаланс между размеченными классами<jupyter_code>data[data['kids'] == -1]<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.12506346251480793, 'fraction_numerical': 0.009138602132340497, 'mean_word_length': 3.317019722425128, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 16, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 14, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '3', 'dir': 'jupyter-structured-clean-dedup', 'id': '257014', 'license': 'MIT', 'n_tokens_mistral': 2087, 'n_tokens_neox': 2114, 'n_words': 441, 'path': 'hw5/user_features.ipynb', 'repo_name': 'j2cry/gb_recommends', 'size': '5909'}