В этом проекте мы будем решать задачу идентификации пользователя по его поведению в сети Интернет. Это сложная и интересная задача на стыке анализа данных и поведенческой психологии. В качестве примера, компания Яндекс решает задачу идентификации взломщика почтового ящика по его поведению. В двух словах, взломщик будет себя вести не так, как владелец ящика: он может не удалять сообщения сразу по прочтении, как это делал хозяин, он будет по-другому ставить флажки сообщениям и даже по-своему двигать мышкой. Тогда такого злоумышленника можно идентифицировать и "выкинуть" из почтового ящика, предложив хозяину войти по SMS-коду. Этот пилотный проект описан в статье на Хабрахабре. Похожие вещи делаются, например, в Google Analytics и описываются в научных статьях, найти можно многое по фразам "Traversal Pattern Mining" и "Sequential Pattern Mining".
Мы будем решать похожую задачу: по последовательности из нескольких веб-сайтов, посещенных подряд один и тем же человеком, мы будем идентифицировать этого человека. Идея такая: пользователи Интернета по-разному переходят по ссылкам, и это может помогать их идентифицировать (кто-то сначала в почту, потом про футбол почитать, затем новости, контакт, потом наконец – работать, кто-то – сразу работать).
Будем использовать данные из статьи "A Tool for Classification of Sequential Data". И хотя мы не можем рекомендовать эту статью (описанные методы делеки от state-of-the-art, лучше обращаться к книге "Frequent Pattern Mining" и последним статьям с ICDM), но данные там собраны аккуратно и представляют интерес.
Имеются данные с прокси-серверов Университета Блеза Паскаля, они имеют очень простой вид. Для каждого пользователя заведен csv-файл с названием user****.csv (где вместо звездочек – 4 цифры, соответствующие ID пользователя), а в нем посещения сайтов записаны в следующем формате:
Скачать исходные данные можно по ссылке в статье, там же описание. Для этого задания хватит данных не по всем 3000 пользователям, а по 10 и 150. Ссылка на архив capstone_user_identification (~7 Mb, в развернутом виде ~ 60 Mb).
В финальном проекте уже придется столкнуться с тем, что не все операции можно выполнить за разумное время (скажем, перебрать с кросс-валидацией 100 комбинаций параметров случайного леса на этих данных Вы вряд ли сможете), поэтому мы будем использовать параллельно 2 выборки: по 10 пользователям и по 150. Для 10 пользователей будем писать и отлаживать код, для 150 – будет рабочая версия.
Данные устроены следующем образом:
На 5 неделе будет задание по соревнованию Kaggle Inclass, которое организовано специально под Capstone проект нашей специализации. Соревнование уже открыто и, конечно, желающие могут начать уже сейчас.
Первая часть проекта посвящена подготовке данных для дальнейшего описательного анализа и построения прогнозных моделей. Надо будет написать код для предобработки данных (исходно посещенные веб-сайты указаны для каждого пользователя в отдельном файле) и формирования единой обучающей выборки. Также в этой части мы познакомимся с разреженным форматом данных (матрицы Scipy.sparse
), который хорошо подходит для данной задачи.
План 1 недели:
В этой части проекта Вам могут быть полезны видеозаписи следующих лекций 1 и 2 недели курса "Математика и Python для анализа данных":
Кроме того, в задании будут использоваться библиотеки Python glob
, pickle
и класс csr_matrix
из Scipy.sparse
.
Наконец, для лучшей воспроизводимости результатов приведем список версий основных используемых в проекте библиотек: NumPy, SciPy, Pandas, Matplotlib, Statsmodels и Scikit-learn. Для этого воспользуемся расширением watermark. Рекомендуется использовать докер-контейнер открытого курса OpenDataScience по машинному обучению, инструкции тут.
# pip install watermark
%load_ext watermark
%watermark -v -m -p numpy,scipy,pandas,matplotlib,statsmodels,sklearn -g
from __future__ import division, print_function
# отключим всякие предупреждения Anaconda
import warnings
warnings.filterwarnings('ignore')
from glob import glob
import os
import pickle
#pip install tqdm
from tqdm import tqdm_notebook
import numpy as np
import pandas as pd
from scipy.sparse import csr_matrix
Посмотрим на один из файлов с данными о посещенных пользователем (номер 31) веб-страницах.
# Поменяйте на свой путь к данным
PATH_TO_DATA = 'capstone_user_identification'
user31_data = pd.read_csv(os.path.join(PATH_TO_DATA,
'10users/user0031.csv'))
user31_data.head()
Поставим задачу классификации: идентифицировать пользователя по сессии из 10 подряд посещенных сайтов. Объектом в этой задаче будет сессия из 10 сайтов, последовательно посещенных одним и тем же пользователем, признаками – индексы этих 10 сайтов (чуть позже здесь появится "мешок" сайтов, подход Bag of Words). Целевым классом будет id пользователя.
Пусть пользователя всего 2, длина сессии – 2 сайта.
timestamp | site |
---|---|
00:00:01 | vk.com |
00:00:11 | google.com |
00:00:16 | vk.com |
00:00:20 | yandex.ru |
timestamp | site |
---|---|
00:00:02 | yandex.ru |
00:00:14 | google.com |
00:00:17 | facebook.com |
00:00:25 | yandex.ru |
Идем по 1 файлу, нумеруем сайты подряд: vk.com – 1, google.com – 2 и т.д. Далее по второму файлу.
Отображение сайтов в их индесы должно получиться таким:
site | site_id |
---|---|
vk.com | 1 |
google.com | 2 |
yandex.ru | 3 |
facebook.com | 4 |
Тогда обучающая выборка будет такой (целевой признак – user_id):
session_id | site1 | site2 | user_id |
---|---|---|---|
1 | 1 | 2 | 1 |
2 | 1 | 3 | 1 |
3 | 3 | 2 | 2 |
4 | 4 | 3 | 2 |
Здесь 1 объект – это сессия из 2 посещенных сайтов 1-ым пользователем (target=1). Это сайты vk.com и google.com (номер 1 и 2). И так далее, всего 4 сессии. Пока сессии у нас не пересекаются по сайтам, то есть посещение каждого отдельного сайта относится только к одной сессии.
Реализуйте функцию prepare_train_set, которая принимает на вход путь к каталогу с csv-файлами path_to_csv_files и параметр session_length – длину сессии, а возвращает 2 объекта:
Детали:
glob
(или аналоги) для обхода файлов в каталоге. Для определенности, отсортируйте список файлов лексикографически. Удобно использовать tqdm_notebook
(или просто tqdm
в случае python-скрипта) для отслеживания числа выполненных итераций циклаdef prepare_train_set(path_to_csv_files, session_length=10):
''' ВАШ КОД ЗДЕСЬ '''
Примените полученную функцию к игрушечному примеру, убедитесь, что все работает как надо.
!cat $PATH_TO_DATA/3users/user0001.csv
!cat $PATH_TO_DATA/3users/user0002.csv
!cat $PATH_TO_DATA/3users/user0003.csv
train_data_toy, site_freq_3users = prepare_train_set(os.path.join(PATH_TO_DATA, '3users'),
session_length=10)
train_data_toy
Частоты сайтов (второй элемент кортежа) точно должны быть такими, нумерация может быть любой (первые элементы кортежей могут отличаться).
site_freq_3users
Примените полученную функцию к данным по 10 пользователям.
Вопрос 1. Сколько уникальных сессий из 10 сайтов в выборке с 10 пользователями?
train_data_10users, site_freq_10users = ''' ВАШ КОД ЗДЕСЬ '''
Вопрос 2. Сколько всего уникальных сайтов в выборке из 10 пользователей?
''' ВАШ КОД ЗДЕСЬ '''
Примените полученную функцию к данным по 150 пользователям.
Вопрос 3. Сколько уникальных сессий из 10 сайтов в выборке с 150 пользователями?
%%time
train_data_150users, site_freq_150users = ''' ВАШ КОД ЗДЕСЬ '''
Вопрос 4. Сколько всего уникальных сайтов в выборке из 150 пользователей?
''' ВАШ КОД ЗДЕСЬ '''
Вопрос 5. Какой из этих сайтов НЕ входит в топ-10 самых популярных сайтов среди посещенных 150 пользователями?
''' ВАШ КОД ЗДЕСЬ '''
Для дальнейшего анализа запишем полученные объекты DataFrame в csv-файлы.
train_data_10users.to_csv(os.path.join(PATH_TO_DATA,
'train_data_10users.csv'),
index_label='session_id', float_format='%d')
train_data_150users.to_csv(os.path.join(PATH_TO_DATA,
'train_data_150users.csv'),
index_label='session_id', float_format='%d')
Если так подумать, то полученные признаки site1, ..., site10 смысла не имеют как признаки в задаче классификации. А вот если воспользоваться идеей мешка слов из анализа текстов – это другое дело. Создадим новые матрицы, в которых строкам будут соответствовать сессии из 10 сайтов, а столбцам – индексы сайтов. На пересечении строки $i$ и столбца $j$ будет стоять число $n_{ij}$ – cколько раз сайт $j$ встретился в сессии номер $i$. Делать это будем с помощью разреженных матриц Scipy – csr_matrix. Прочитайте документацию, разберитесь, как использовать разреженные матрицы и создайте такие матрицы для наших данных. Сначала проверьте на игрушечном примере, затем примените для 10 и 150 пользователей.
Обратите внимание, что в коротких сессиях, меньше 10 сайтов, у нас остались нули, так что первый признак (сколько раз попался 0) по смыслу отличен от остальных (сколько раз попался сайт с индексом $i$). Поэтому первый столбец разреженной матрицы надо будет удалить.
X_toy, y_toy = train_data_toy.iloc[:, :-1].values, train_data_toy.iloc[:, -1].values
X_toy
X_sparse_toy = csr_matrix ''' ВАШ КОД ЗДЕСЬ '''
Размерность разреженной матрицы должна получиться равной 11, поскольку в игрушечном примере 3 пользователя посетили 11 уникальных сайтов.
X_sparse_toy.todense()
X_10users, y_10users = train_data_10users.iloc[:, :-1].values, \
train_data_10users.iloc[:, -1].values
X_150users, y_150users = train_data_150users.iloc[:, :-1].values, \
train_data_150users.iloc[:, -1].values
X_sparse_10users = ''' ВАШ КОД ЗДЕСЬ '''
X_sparse_150users = ''' ВАШ КОД ЗДЕСЬ '''
Сохраним эти разреженные матрицы с помощью pickle (сериализация в Python), также сохраним вектора y_10users, y_150users – целевые значения (id пользователя) в выборках из 10 и 150 пользователей. То что названия этих матриц начинаются с X и y, намекает на то, что на этих данных мы будем проверять первые модели классификации. Наконец, сохраним также и частотные словари сайтов для 3, 10 и 150 пользователей.
with open(os.path.join(PATH_TO_DATA, 'X_sparse_10users.pkl'), 'wb') as X10_pkl:
pickle.dump(X_sparse_10users, X10_pkl, protocol=2)
with open(os.path.join(PATH_TO_DATA, 'y_10users.pkl'), 'wb') as y10_pkl:
pickle.dump(y_10users, y10_pkl, protocol=2)
with open(os.path.join(PATH_TO_DATA, 'X_sparse_150users.pkl'), 'wb') as X150_pkl:
pickle.dump(X_sparse_150users, X150_pkl, protocol=2)
with open(os.path.join(PATH_TO_DATA, 'y_150users.pkl'), 'wb') as y150_pkl:
pickle.dump(y_150users, y150_pkl, protocol=2)
with open(os.path.join(PATH_TO_DATA, 'site_freq_3users.pkl'), 'wb') as site_freq_3users_pkl:
pickle.dump(site_freq_3users, site_freq_3users_pkl, protocol=2)
with open(os.path.join(PATH_TO_DATA, 'site_freq_10users.pkl'), 'wb') as site_freq_10users_pkl:
pickle.dump(site_freq_10users, site_freq_10users_pkl, protocol=2)
with open(os.path.join(PATH_TO_DATA, 'site_freq_150users.pkl'), 'wb') as site_freq_150users_pkl:
pickle.dump(site_freq_150users, site_freq_150users_pkl, protocol=2)
Чисто для подстраховки проверим, что число столбцов в разреженных матрицах X_sparse_10users
и X_sparse_150users
равно ранее посчитанным числам уникальных сайтов для 10 и 150 пользователей соответственно.
assert X_sparse_10users.shape[1] == len(site_freq_10users)
assert X_sparse_150users.shape[1] == len(site_freq_150users)
CountVectorizer
, TfidfVectorizer
и т.п. Поскольку данные по сути могут быть описаны как последовательности, то можно вычислять n-граммы сайтов. Работает все это или нет, мы будем проверять в соревновании Kaggle Inclass (желающие могут начать уже сейчас).На следующей неделе мы еще немного поготовим данные и потестируем первые гипотезы, связанные с нашими наблюдениями.