ML-задача на 30 минут: гадаем по cookie

в 6:00, , рубрики: big data, data science, machinelearning, Блог компании МТС, данные, Занимательные задачки, машинное обучение, соревнование, хакатон, Хакатоны

«Я тебя по IP вычислю!» – помните такую угрозу из интернета времен нулевых годов? Мы в Big Data МТС решили выяснить, можно ли составить хотя бы приблизительное представление о человеке, обладая информацией о сайтах, которые он посещает. Для этого мы сгенерировали полусинтетические данные, чтобы понять, насколько смелыми можно быть в этих ваших интернетах.

ML-задача на 30 минут: гадаем по cookie - 1

Информация о посещенных сайтах доступна не по конкретному Иван Иванычу, а по обезличенной сущности cookie, используемой в качестве id пользователя при обмене данными между рекламными DSP- и SSP- площадками.

Вопрос звучит так: сможем ли мы по цифровым следам пользователя (на каких сайтах с каких IP он сидел, сколько раз заходил, какое у него устройство) понять, кто этот пользователь? Студент или пенсионер? Мужчина или женщина? Как много информации мы вообще сможем получить?

Вообще в Digital-рекламе сегмент часто включает себя пол и один из бакетов по возрасту (<18, 18-24, 25-34, 35-44, 45-54, 55-64, 65+). Эта задача особенно актуальна для рекламных DSP-площадок, которые в OpenRTB запросах получают такие данные с частотой 200 000 запросов в секунду со всех сайтов, размещающих рекламу за деньги.

Приглашаем и вас попробовать составить портрет рядового пользователя Хабра десятка почти случайных сайтов и посмотреть, насколько точным он получится.

Ниже – наш baseline решения – без кросс-валидации, подбора гиперпараметров, feature engineering и прочих приятных сердцу вещей. Такое решение можно написать в метро по дороге от Речного вокзала до Технопарка (именно здесь находится офис МТС Digital). Этот путь займет чуть больше 30 минут.

ML-задача на 30 минут: гадаем по cookie - 2

Знакомый всем DS-иконостас

Однако в этот раз мы оставили только самое нужное =) (ну почти).

import sys
import os
import warnings
os.environ['OPENBLAS_NUM_THREADS'] = '1'
warnings.filterwarnings('ignore')
import pandas as pd
import numpy as np
import time
import pyarrow.parquet as pq
import scipy
import implicit
import bisect
import sklearn.metrics as m
from catboost import CatBoostClassifier, CatBoostRegressor, Pool
from sklearn.model_selection import train_test_split
from sklearn.calibration import calibration_curve, CalibratedClassifierCV
LOCAL_DATA_PATH = './context_data/'
SPLIT_SEED = 42
DATA_FILE = 'competition_data_final_pqt'
TARGET_FILE = 'competition_target_pqt'

Прочитаем файлы

data = pq.read_table(f'{LOCAL_DATA_PATH}/{DATA_FILE}')
pd.DataFrame([(z.name, z.type) for z in data.schema], 
             columns = [['field', 'type']])
ML-задача на 30 минут: гадаем по cookie - 3

В таблице выше:

  • регион;

  • населенный пункт;

  • производитель устройства;

  • модель устройства;

  • домен, с которого пришел рекламный запрос;

  • тип устройства (смартфон или что-то другое);

  • операционка на устройстве;

  • оценка цены устройства;

  • дата;

  • время дня;

  • число запросов к домену в эту часть дня в эту дату;

  • id пользователя.

data.select(['cpe_type_cd']).to_pandas()['cpe_type_cd'].value_counts()
ML-задача на 30 минут: гадаем по cookie - 4

Для себя открыл слово фаблет.

targets = pq.read_table(f'{LOCAL_DATA_PATH}/{TARGET_FILE}')
pd.DataFrame([(z.name, z.type) for z in targets.schema], 
             columns = [['field', 'type']])
ну тут все ясно
ну тут все ясно

Ок, нам лень генерить фичи

Вместо этого посмотрим на задачу, как на рекомендательную систему: факторизуем матрицу взаимодействий Users и Items (под которыми понимаем посещенные домены) – ожидаем, что получим полезный эмбеддинг, да и придумывать ничего не придется. Понятно, что мы потеряем последовательность и временные характеристики, но что делать, мы уже почти на Белорусской.

ML-задача на 30 минут: гадаем по cookie - 6
%%time
data_agg = data.select(['user_id', 'url_host', 'request_cnt']).
    group_by(['user_id', 'url_host']).aggregate([('request_cnt', "sum")])
ML-задача на 30 минут: гадаем по cookie - 7
url_set = set(data_agg.select(['url_host']).to_pandas()['url_host'])
print(f'{len(url_set)} urls')
url_dict = {url: idurl for url, idurl in zip(url_set, range(len(url_set)))}
usr_set = set(data_agg.select(['user_id']).to_pandas()['user_id'])
print(f'{len(usr_set)} users')
usr_dict = {usr: user_id for usr, user_id in zip(usr_set, range(len(usr_set)))}
не самая большая матрица выйдет
не самая большая матрица выйдет
%%time
values = np.array(data_agg.select(['request_cnt_sum']).to_pandas()['request_cnt_sum'])
rows = np.array(data_agg.select(['user_id']).to_pandas()['user_id'].map(usr_dict))
cols = np.array(data_agg.select(['url_host']).to_pandas()['url_host'].map(url_dict))
mat = scipy.sparse.coo_matrix((values, (rows, cols)), shape=(rows.max() + 1, cols.max() + 1))
als = implicit.approximate_als.FaissAlternatingLeastSquares(factors = 50, 
      iterations = 30, use_gpu = False, calculate_training_loss = False, regularization = 0.1)
ML-задача на 30 минут: гадаем по cookie - 9
%%time
als.fit(mat)
u_factors = als.model.user_factors 
d_factors = als.model.item_factors
30 итераций за 30 сек -- спасибо FAISS!
30 итераций за 30 сек -- спасибо FAISS!

Получим оценку по полу

Раз уж у нас есть эмбеддинг пользователя, потестируем его на задаче – предсказать пол пользователя, запуская все из коробки и забывая про все остальные фичи.

%%time
inv_usr_map = {v: k for k, v in usr_dict.items()}
usr_emb = pd.DataFrame(u_factors)
usr_emb['user_id'] = usr_emb.index.map(inv_usr_map)
usr_targets = targets.to_pandas()
df = usr_targets.merge(usr_emb, how = 'inner', on = ['user_id'])
df = df[df['is_male'] != 'NA']
df = df.dropna()
df['is_male'] = df['is_male'].map(int)
df['is_male'].value_counts()
ML-задача на 30 минут: гадаем по cookie - 11
%%time
x_train, x_test, y_train, y_test = train_test_split(
    df.drop(['user_id', 'age', 'is_male'], axis = 1), df['is_male'], test_size = 0.33, random_state = SPLIT_SEED)
clf = CatBoostClassifier()
clf.fit(x_train, y_train, verbose = False)
print(f'GINI по полу {2 * m.roc_auc_score(y_test, clf.predict_proba(x_test)[:,1]) - 1:2.3f}')
ML-задача на 30 минут: гадаем по cookie - 12

Не так уж плохо! Но мы почти доехали до Павелецкой.

ML-задача на 30 минут: гадаем по cookie - 13

Получим оценку по возрасту

import seaborn as sns
import matplotlib.pyplot as plt
import plotly.express as px

%matplotlib inline
sns.set_style('darkgrid')
def age_bucket(x):
    return bisect.bisect_left([18,25,35,45,55,65], x)
df = usr_targets.merge(usr_emb, how = 'inner', on = ['user_id'])
df = df[df['age'] != 'NA']
df = df.dropna()
df['age'] = df['age'].map(age_bucket)
sns.histplot(df['age'], bins = 7)
ML-задача на 30 минут: гадаем по cookie - 14
x_train, x_test, y_train, y_test = train_test_split(
    df.drop(['user_id', 'age', 'is_male'], axis = 1), 
    df['age'], test_size = 0.33, random_state = SPLIT_SEED)

clf = CatBoostClassifier()
clf.fit(x_train, y_train, verbose = False)
print(m.classification_report(y_test, clf.predict(x_test), 
    target_names = ['<18', '18-25','25-34', '35-44', '45-54', '55-65', '65+']))
ML-задача на 30 минут: гадаем по cookie - 15

Критерии сравнения

Оказалось, что пол определить намного проще, чем возраст.
Поэтому в итоговый скор соревнования войдут Gini по полу (от 0 до 1) и f1 weighted по возрасту – с весом два:

score=Gini_{sex} + 2 weighted F1 _{age}

Итого общий скор: 0.665 + 2 * 0.41 = 1.485

Для поездки от Речного до Технопарка терпимо. =)

На этом все! Если же хочется более серьезных задач – ждем вас на соревнованиях по Machine Learning 30 января, это турнир по определению пола/возраста владельца cookie от МТС Digital. Призовой фонд MTC ML Cup – 650 000 рублей: победитель получит 350 000 рублей, обладатель серебра – 200 000 рублей, а третий призер станет богаче на 100 000 рублей. Регистрация уже открыта, простая анкета для участников и все подробности на сайте. Увидимся на соревновании!

ML-задача на 30 минут: гадаем по cookie - 17

Спасибо за уделенное время! Если у вас есть иные способы решения поставленной задачи или же вы знаете, как сократить необходимое на решение время до пары перегонов между станциями метро – обязательно расскажите об этом в комментариях!

P.S.

Возможно, вам полезно будет попробовать более новые и быстрые библиотеки по работе с табличками вместо Pandas и PyArrow:
Polars
CuDF

И библиотеки для RecSys:
MTS RecTools
LightFM
Transformers4Rec
ReChorus
RecBole

Автор: Никита Зелинский

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js