Опубликовано: 15 мар 2019
Ниже описан пример построения алгоритма анализа сессий из сырых данных, сохраненных в BigQuery.
Первым делом подключим в наш Jupyter Notebook библиотеки BigQuery:
from google.cloud import bigquery
import os
os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = 'datawhbq.json'
client = bigquery.Client()
%load_ext google.cloud.bigquery
Создадим таблицу данных в качестве примера на котором мы будем строить алгоритм. В этом же певом SQL запросе с помощью функцией LAG
мы для каждой строчки добавляем предыдущие значением поля timestamp
, которое будет имитировать таймстемп хита:
%%bigquery
SELECT chunk.user, chunk.timestamp, LAG(chunk.timestamp,1) OVER(PARTITION BY chunk.user ORDER BY chunk.timestamp) previos
FROM UNNEST([STRUCT<user STRING, timestamp INT64>('user1',5),('user2',9),('user1',15),('user1',51),
('user2',12),('user1',60),('user2',92),('user2',82),('user1',90),('user2',192)]) AS chunk
ORDER BY timestamp
Взяв данный запрос за основу, постепенно начнем добавлять расчет нужных нам данных.
Вначале добавим колонку со значением разницы между текущим и предыдущим данными timestamp
и колонку в которой определим начало условной сессии на основе данного посчитанного интервала. Пусть началом сессии будет каждая строка в которой интервал между значениями timestamp
больше 15-и.
%%bigquery
WITH a AS (
SELECT chunk.user, chunk.timestamp, LAG(chunk.timestamp,1) OVER(PARTITION BY chunk.user ORDER BY chunk.timestamp) previos
FROM UNNEST([STRUCT<user STRING, timestamp INT64>('user1',5),('user2',9),('user1',15),('user1',51),
('user2',12),('user1',60),('user2',92),('user2',82),('user1',90),('user2',192)]) AS chunk
ORDER BY timestamp)
SELECT *, timestamp-previos dif,
CASE WHEN timestamp-previos>15 OR previos IS NULL THEN true
ELSE false END AS newsess
FROM a
С посощью функции COUNTIF
пронумеруем сесии для каждого пользователя:
%%bigquery
WITH a AS (
SELECT chunk.user, chunk.timestamp, LAG(chunk.timestamp,1) OVER(PARTITION BY chunk.user ORDER BY chunk.timestamp) previos
FROM UNNEST([STRUCT<user STRING, timestamp INT64>('user1',5),('user2',9),('user1',15),('user1',51),
('user2',12),('user1',60),('user2',92),('user2',82),('user1',90),('user2',192)]) AS chunk
ORDER BY timestamp)
SELECT *, COUNTIF(sess) OVER(PARTITION BY user ORDER BY timestamp) AS sessnum #number of session for a particular user
FROM (
SELECT *, timestamp-previos dif,
CASE WHEN timestamp-previos>15 OR previos IS NULL THEN true
ELSE false END AS sess
FROM a
)
Расширим алгоритм подсчета на случай с разбивкой по датам:
%%bigquery
WITH a AS (
SELECT chunk.date, chunk.user, chunk.timestamp, LAG(chunk.timestamp,1)
OVER(PARTITION BY chunk.user,chunk.date ORDER BY chunk.timestamp) previos
FROM UNNEST([STRUCT<date STRING, user STRING, timestamp INT64>('day1', 'user1',5),
('day1','user2',9),('day1','user1',15),('day1','user1',51),
('day1','user2',12),('day1','user2',15),('day2','user1',65),
('day2','user1',60),('day2','user2',92),('day2','user2',82),
('day2','user1',90),('day2','user2',192)]) AS chunk
ORDER BY timestamp)
SELECT *, COUNTIF(sess) OVER(PARTITION BY user,date ORDER BY timestamp) AS sessnum
FROM (
SELECT *, timestamp-previos dif,
CASE WHEN timestamp-previos>15 OR previos IS NULL THEN true
ELSE false END AS sess
FROM a
)
Расширим алгоритм дальше и пронумеруем хиты внутри каждой сессии:
%%bigquery
WITH a AS (
SELECT chunk.date, chunk.user, chunk.timestamp, LAG(chunk.timestamp,1)
OVER(PARTITION BY chunk.user,chunk.date ORDER BY chunk.timestamp) previos
FROM UNNEST([STRUCT<date STRING, user STRING, timestamp INT64>('day1', 'user1',5),
('day1','user2',9),('day1','user1',15),('day1','user1',16),
('day1','user1',49),('day1','user1',51),('day1','user2',12),
('day1','user2',16),('day1','user2',15),('day2','user1',65),
('day2','user1',60),('day2','user1',92),('day2','user1',91),
('day2','user2',92),('day2','user2',82), ('day2','user1',90),
('day2','user2',192),('day2','user2',193),('day2','user2',194)]) AS chunk
ORDER BY timestamp)
SELECT *, DENSE_RANK() OVER(PARTITION BY sessnum,user,date ORDER BY timestamp) AS hitnumper
FROM (
SELECT *, COUNTIF(sess) OVER(PARTITION BY user,date ORDER BY timestamp) AS sessnum
FROM (
SELECT *, timestamp-previos dif,
CASE WHEN timestamp-previos>15 OR previos IS NULL THEN true
ELSE false END AS sess
FROM a
)
)
ORDER BY user,timestamp
Базовый алгоритм готов. Теперь на основе него мы можем проводить анализ основных показателей для сессий. Например, посчитаем количество хитов вначале за каждую сессиию, а затем среднее на всем интервале:
%%bigquery
WITH a AS (
SELECT chunk.date, chunk.user, chunk.timestamp, LAG(chunk.timestamp,1)
OVER(PARTITION BY chunk.user,chunk.date ORDER BY chunk.timestamp) previos
FROM UNNEST([STRUCT<date STRING, user STRING, timestamp INT64>('day1', 'user1',5),
('day1','user2',9),('day1','user1',15),('day1','user1',16),
('day1','user1',49),('day1','user1',51),('day1','user2',12),
('day1','user2',16),('day1','user2',15),('day2','user1',65),
('day2','user1',60),('day2','user1',92),('day2','user1',91),
('day2','user2',92),('day2','user2',82), ('day2','user1',90),
('day2','user2',192),('day2','user2',193),('day2','user2',194)]) AS chunk
ORDER BY timestamp)
SELECT user,date,sessnum,MAX(hitnumper) sesshits FROM (
SELECT *, DENSE_RANK() OVER(PARTITION BY sessnum,user,date ORDER BY timestamp) AS hitnumper
FROM (
SELECT *, COUNTIF(sess) OVER(PARTITION BY user,date ORDER BY timestamp) AS sessnum
FROM (
SELECT *, timestamp-previos dif,
CASE WHEN timestamp-previos>15 OR previos IS NULL THEN true
ELSE false END AS sess
FROM a
)
)
ORDER BY user,timestamp
)
GROUP BY user,date,sessnum
ORDER BY date,user,sessnum
%%bigquery
WITH a AS (
SELECT chunk.date, chunk.user, chunk.timestamp, LAG(chunk.timestamp,1)
OVER(PARTITION BY chunk.user,chunk.date ORDER BY chunk.timestamp) previos
FROM UNNEST([STRUCT<date STRING, user STRING, timestamp INT64>('day1', 'user1',5),
('day1','user2',9),('day1','user1',15),('day1','user1',16),
('day1','user1',49),('day1','user1',51),('day1','user2',12),
('day1','user2',16),('day1','user2',15),('day2','user1',65),
('day2','user1',60),('day2','user1',92),('day2','user1',91),
('day2','user2',92),('day2','user2',82), ('day2','user1',90),
('day2','user2',192),('day2','user2',193),('day2','user2',194)]) AS chunk
ORDER BY timestamp)
SELECT CAST(ROUND(AVG(sesshits)) AS INT64) avghits FROM (
SELECT user,date,sessnum,MAX(hitnumper) sesshits FROM (
SELECT *, DENSE_RANK() OVER(PARTITION BY sessnum,user,date ORDER BY timestamp) AS hitnumper
FROM (
SELECT *, COUNTIF(sess) OVER(PARTITION BY user,date ORDER BY timestamp) AS sessnum
FROM (
SELECT *, timestamp-previos dif,
CASE WHEN timestamp-previos>15 OR previos IS NULL THEN true
ELSE false END AS sess
FROM a
)
)
ORDER BY user,timestamp
)
GROUP BY user,date,sessnum
ORDER BY date,user,sessnum
)
Вопрос целесообразности применения сессии, как аналитической единицы обсуждается давно и долго. Разные аналитические системы могут использовать не одинаковые алгоритмы для подсчета сессий, или же иметь разные установочные настройки, например временной интервал бездействия после которого начинается новая сессия. Сессия — это действительно скорее некий индивидуальный параметр, который должен соответствовать объективным особенностям конкретного проекта. И в этом свете как раз сырые неагрегированные данные позволяют вам при необходимости подсчитывать сессии очень гибко, опираясь на заданные вами параметры, которые формируются, исходя из особенностей вашего онлайн проекта.