Делаем сессии из лога событий

Загружаем библиотеки pandas и numpy, а также display для отображения dataframe'ов

In [1]:
import pandas as pd
import numpy as np
from IPython.display import display

Загружаем лог

Структура данных:

  • id - порядковый номер события в логе
  • user_id - уникальный идентификатор пользователя, совершившего событие (при решении реальной задачи анализа лога в качестве user_id может выступать IP-адрес пользователя или, например, уникальный идентификатор cookie-файла)
  • date_time - время совершения события
  • page - страница, на которую перешел пользователь (для решения задачи эта колонка не несет никакой пользы, я привожу её для наглядности)
In [2]:
event_df = pd.read_excel('event_log.xlsx')
display(event_df)
id user_id date_time page
0 1 36004921-2faf-45b6-bd35-7496474e6c87 2018-05-05 07:45:00 /index
1 2 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 07:46:00 /index
2 3 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 07:49:00 /catalog
3 4 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 07:50:00 /catalog2
4 5 36004921-2faf-45b6-bd35-7496474e6c87 2018-05-05 07:51:00 /contacts
5 6 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 08:21:00 /catalog
6 7 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 09:22:00 /index
7 8 58774a77-5d8d-4459-b5f3-8cb539f4917c 2018-05-05 09:25:00 /index

События сгенерированные разными пользователями идут в хронологическом порядке. Для удобства отсортируем их по user_id, тогда события каждого пользователя будут идти последовательно

In [3]:
event_df = event_df.sort_values('user_id')
display(event_df)
id user_id date_time page
0 1 36004921-2faf-45b6-bd35-7496474e6c87 2018-05-05 07:45:00 /index
4 5 36004921-2faf-45b6-bd35-7496474e6c87 2018-05-05 07:51:00 /contacts
7 8 58774a77-5d8d-4459-b5f3-8cb539f4917c 2018-05-05 09:25:00 /index
1 2 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 07:46:00 /index
2 3 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 07:49:00 /catalog
3 4 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 07:50:00 /catalog2
5 6 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 08:21:00 /catalog
6 7 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 09:22:00 /index

В колонке 'diff' для каждого события отдельного пользователя посчитаем разницу между временем посещения страницы и времененем посещения предыдущей страницы. Если страница была первой для пользователя, то значение в колонке 'diff' будет NaT, т.к. нет предыдущего значения

In [4]:
event_df['diff'] = event_df.groupby('user_id')['date_time'].diff(1)
display(event_df)
id user_id date_time page diff
0 1 36004921-2faf-45b6-bd35-7496474e6c87 2018-05-05 07:45:00 /index NaT
4 5 36004921-2faf-45b6-bd35-7496474e6c87 2018-05-05 07:51:00 /contacts 00:06:00
7 8 58774a77-5d8d-4459-b5f3-8cb539f4917c 2018-05-05 09:25:00 /index NaT
1 2 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 07:46:00 /index NaT
2 3 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 07:49:00 /catalog 00:03:00
3 4 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 07:50:00 /catalog2 00:01:00
5 6 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 08:21:00 /catalog 00:31:00
6 7 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 09:22:00 /index 01:01:00

Из основного dataframe 'event_df' создадим вспомогательный dataframe 'session_start_df'. Этот dataframe будет содержать события, которые будут считаться первыми событиями сессий. К таким событиям относятся все события, которые произошли спустя более чем 30 минут после предудыщего, либо события, которые были первыми для пользователя (NaT в колонке 'diff')

Также создадим во вспомогательном dataframe колонку 'session_id', которая будет содержать в себе id первого события сессии. Она пригодится, чтобы корректно отобразить идентификатор сессии, когда будем соединять данные из основного и вспомогательного dataframe

In [5]:
sessions_start_df = event_df[(event_df['diff'].isnull()) | (event_df['diff'] > '1800 seconds')]
sessions_start_df['session_id'] = sessions_start_df['id']
display(sessions_start_df)
/home/makarov/anaconda2/lib/python2.7/site-packages/ipykernel_launcher.py:2: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
  
id user_id date_time page diff session_id
0 1 36004921-2faf-45b6-bd35-7496474e6c87 2018-05-05 07:45:00 /index NaT 1
7 8 58774a77-5d8d-4459-b5f3-8cb539f4917c 2018-05-05 09:25:00 /index NaT 8
1 2 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 07:46:00 /index NaT 2
5 6 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 08:21:00 /catalog 00:31:00 6
6 7 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 09:22:00 /index 01:01:00 7

С помощью функции merge_asof объединим между собой данные основного и вспомогательного dataframe'ов. Эта функция позволяет объединить данные двух dataframe'ов схожим образом с левым join'ом, но не по точному соответствию ключей, а по ближайшему. Примеры и подробности в документации: https://pandas.pydata.org/pandas-docs/stable/generated/pandas.merge_asof.html

Для корректной работы этой функции оба dataframe должны быть отсортированы по ключу, на основе которого будет происходить merge_asof

In [6]:
event_df = event_df.sort_values('id')
sessions_start_df = sessions_start_df.sort_values('id')
event_df = pd.merge_asof(event_df,sessions_start_df[['id','user_id','session_id']],on='id',by='user_id')

После объединения отсортируем основной dataframe по user_id. И убедимся, что сессии корректно сопоставлены с событиями

In [7]:
event_df = event_df.sort_values(['user_id','date_time'])
display(event_df)
id user_id date_time page diff session_id
0 1 36004921-2faf-45b6-bd35-7496474e6c87 2018-05-05 07:45:00 /index NaT 1
4 5 36004921-2faf-45b6-bd35-7496474e6c87 2018-05-05 07:51:00 /contacts 00:06:00 1
7 8 58774a77-5d8d-4459-b5f3-8cb539f4917c 2018-05-05 09:25:00 /index NaT 8
1 2 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 07:46:00 /index NaT 2
2 3 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 07:49:00 /catalog 00:03:00 2
3 4 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 07:50:00 /catalog2 00:01:00 2
5 6 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 08:21:00 /catalog 00:31:00 6
6 7 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 09:22:00 /index 01:01:00 7

Что еще можно сделать?

/1. Можно найти события, которые были первыми в сессиях. Это будет полезно, если мы захотим определить страницы входа

Найти эти события предельно просто: их идентификаторы будут равны идентификаторам сессии

In [8]:
event_df['is_first_event_in_session'] = event_df['id'] == event_df['session_id']
display(event_df)
id user_id date_time page diff session_id is_first_event_in_session
0 1 36004921-2faf-45b6-bd35-7496474e6c87 2018-05-05 07:45:00 /index NaT 1 True
4 5 36004921-2faf-45b6-bd35-7496474e6c87 2018-05-05 07:51:00 /contacts 00:06:00 1 False
7 8 58774a77-5d8d-4459-b5f3-8cb539f4917c 2018-05-05 09:25:00 /index NaT 8 True
1 2 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 07:46:00 /index NaT 2 True
2 3 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 07:49:00 /catalog 00:03:00 2 False
3 4 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 07:50:00 /catalog2 00:01:00 2 False
5 6 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 08:21:00 /catalog 00:31:00 6 True
6 7 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 09:22:00 /index 01:01:00 7 True

/2. Можно вычислить время, проведенное на странице, руководствуясь временем посещения следующей страницы

Для этого сначала считаем разницу между предыдущей и следующей страницей внутри сессии

In [9]:
event_df['time_on_page'] = event_df.groupby(['session_id'])['date_time'].diff(1)
display(event_df)
id user_id date_time page diff session_id is_first_event_in_session time_on_page
0 1 36004921-2faf-45b6-bd35-7496474e6c87 2018-05-05 07:45:00 /index NaT 1 True NaT
4 5 36004921-2faf-45b6-bd35-7496474e6c87 2018-05-05 07:51:00 /contacts 00:06:00 1 False 00:06:00
7 8 58774a77-5d8d-4459-b5f3-8cb539f4917c 2018-05-05 09:25:00 /index NaT 8 True NaT
1 2 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 07:46:00 /index NaT 2 True NaT
2 3 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 07:49:00 /catalog 00:03:00 2 False 00:03:00
3 4 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 07:50:00 /catalog2 00:01:00 2 False 00:01:00
5 6 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 08:21:00 /catalog 00:31:00 6 True NaT
6 7 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 09:22:00 /index 01:01:00 7 True NaT

Затем смещаем посчитанную разницу на строку выше внутри сессии

In [10]:
event_df['time_on_page'] = event_df.groupby(['session_id'])['time_on_page'].shift(-1)
display(event_df)
id user_id date_time page diff session_id is_first_event_in_session time_on_page
0 1 36004921-2faf-45b6-bd35-7496474e6c87 2018-05-05 07:45:00 /index NaT 1 True 00:06:00
4 5 36004921-2faf-45b6-bd35-7496474e6c87 2018-05-05 07:51:00 /contacts 00:06:00 1 False NaT
7 8 58774a77-5d8d-4459-b5f3-8cb539f4917c 2018-05-05 09:25:00 /index NaT 8 True NaT
1 2 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 07:46:00 /index NaT 2 True 00:03:00
2 3 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 07:49:00 /catalog 00:03:00 2 False 00:01:00
3 4 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 07:50:00 /catalog2 00:01:00 2 False NaT
5 6 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 08:21:00 /catalog 00:31:00 6 True NaT
6 7 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 09:22:00 /index 01:01:00 7 True NaT

Для удобства дальнейших вычислений переведем 'time_on_page' в секунды

In [11]:
event_df['time_on_page'] = event_df['time_on_page'] / np.timedelta64(1, 's')
display(event_df)
id user_id date_time page diff session_id is_first_event_in_session time_on_page
0 1 36004921-2faf-45b6-bd35-7496474e6c87 2018-05-05 07:45:00 /index NaT 1 True 360.0
4 5 36004921-2faf-45b6-bd35-7496474e6c87 2018-05-05 07:51:00 /contacts 00:06:00 1 False NaN
7 8 58774a77-5d8d-4459-b5f3-8cb539f4917c 2018-05-05 09:25:00 /index NaT 8 True NaN
1 2 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 07:46:00 /index NaT 2 True 180.0
2 3 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 07:49:00 /catalog 00:03:00 2 False 60.0
3 4 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 07:50:00 /catalog2 00:01:00 2 False NaN
5 6 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 08:21:00 /catalog 00:31:00 6 True NaN
6 7 61955afd-9718-49df-825c-1b21e352807f 2018-05-05 09:22:00 /index 01:01:00 7 True NaN

На основе полученных данных мы можем посчитать простейшие показатели. А можно придумать что-нибудь по-сложнее :)

In [12]:
print u'Количество пользователей: {0}'.format(event_df['user_id'].nunique())
print u'Количество сессий: {0}'.format(event_df['session_id'].nunique())
print u'Количество просмотров страниц: {0}'.format(event_df['id'].count())
print u'Среднее время просмотра страницы: {0}'.format(event_df['time_on_page'].mean())
Количество пользователей: 3
Количество сессий: 5
Количество просмотров страниц: 8
Среднее время просмотра страницы: 200.0