Feb 27: data.table pt.3
Разбор домашней работы
level 2 (HNTR)
library(data.table)
# таблица привлечённых пользователей и их стоимости
users <- fread('https://gitlab.com/upravitelev/mar201s/raw/master/data/users.csv')
str(users)
## Classes 'data.table' and 'data.frame': 100 obs. of 3 variables:
## $ user_id : chr "user_1" "user_2" "user_3" "user_4" ...
## $ media_source: chr "FbAds" "FbAds" "AdWords" "Ironsource" ...
## $ cost : num 2.61 1.51 2.95 0.97 1.44 1.07 1.88 2.22 1.7 2.94 ...
## - attr(*, ".internal.selfref")=<externalptr>
# таблица платежей привлечённых пользователей
payments <- fread('https://gitlab.com/upravitelev/mar201s/raw/master/data/payments.csv')
str(payments)
## Classes 'data.table' and 'data.frame': 221 obs. of 2 variables:
## $ user_id: chr "user_28" "user_28" "user_28" "user_28" ...
## $ gross : num 0.755 0.37 0.38 0.33 0.286 ...
## - attr(*, ".internal.selfref")=<externalptr>
Задание
- в таблице payments разметьте пользователей, у которых больше 5 платежей и сумма за пять платежей превышает 20 - их обозначьте как whales
, прочих как other
.
Решение
Здесь необходимо разбить задачу на этапы. Первый этап — посчитать, сколько платежей сделали пользователи и сколько суммарно заплатили. Условие сумма за пять платежей
усложняет задачу, его рассмотрим отдельно (смотрите комментарии в коде).
# считаем количество платежей и сумму платежей по пользователям
payments_stat <- payments[, list(
n_purchases = .N,
gross = sum(gross),
# сумма платежей за первые 5 платежей
gross5 = sum(gross[1:5])
), by = user_id]
payments_stat[1:5]
## user_id n_purchases gross gross5
## 1: user_28 10 4.374968 2.121308
## 2: user_80 11 30.122404 8.625715
## 3: user_22 2 6.905017 NA
## 4: user_9 5 10.433389 10.433389
## 5: user_5 15 21.905107 12.024310
Второй шаг — выделить пользователей по условию (больше пяти платежей и сумма платежей больше 20).
# выделяем пользователей. так как user_id не обернуто в list()
# то в результате мы получаем вектор, а не таблицу (подробнее смотрите в разделе `Выбор колонки в data.table`)
tg_users <- payments_stat[n_purchases > 5 & gross > 20, user_id]
tg_users[1:5]
## [1] "user_80" "user_5" "user_78" "user_21" "user_40"
Третий этап — разметить пользователей в таблице платежей, whale
или нет.
# с помощью ifelse() размечаем пользователей в таблице платежей
payments[, is_whale := ifelse(user_id %in% tg_users, 'whales', 'other')]
payments_stat[1:5]
## user_id n_purchases gross gross5
## 1: user_28 10 4.374968 2.121308
## 2: user_80 11 30.122404 8.625715
## 3: user_22 2 6.905017 NA
## 4: user_9 5 10.433389 10.433389
## 5: user_5 15 21.905107 12.024310
Задание - Сделайте сводные статистики (количество пользователей, средний и максимальный платежи) в каждой из этих двух групп.
Решение Решение типовое, обычные агрегатные статистики по группе:
payments[, list(
n_users = uniqueN(user_id),
gross_mn = mean(gross),
gross_max = max(gross)
), by = is_whale]
## is_whale n_users gross_mn gross_max
## 1: other 23 1.669462 18.52535
## 2: whales 7 2.061005 20.98412
level 3 (HMP)
Задание
- замените в payments
все значения gross
, которые выше 90%, на NA
.
Решение
90% — это обозначение девяностого перцентиля распределения (то есть, если упорядочить данные по возрастанию, то 90% обозначает значение, выше которого 10% наблюдений с самыми большими значениями). Медиана - это 50% перцентиль (делит ряд упорядоченных значений на две равные по количеству наблюдений части). Для вычисления перцентилей (а также децилей, квантилей и любых других долей) используется функция quantile()
с аргументом probs
.
Так как дальше нам потребуется статистика по gross
с незамещёнными значениями, то проще создать вторую колонку и в неё записать gross
, если значения меньше 90%, и NA
, если больше.
# высчитаем отдельно 90%, это операция над колонкой
gross90 <- payments[, quantile(gross, .90)]
gross90
## 90%
## 4.101129
# создаем колонку, аналогичную gross, с заменёнными значениями
payments[, gross2 := ifelse(gross > gross90, NA, gross)]
Задание - Замените все полученные NA на медиану по ряду. Создайте отдельно колонку is_imputed, в которой будет промаркировано, замещено пропущенное значение гросса медианой или нет.
Решение
Можно сначала заместить значения, а потом создать метку (по равенству с gross90
), но проще сделать в обратном порядке:
# создаем колонку с меткой, есть ли в этой строке в gross2 пропуск или нет
payments[, is_imputed := ifelse(is.na(gross2), 'imputed', 'not-imputed')]
# заменяем пропущенные значения gross2 медианой по всему ряду
payments[, gross2 := ifelse(is.na(gross2), median(gross2, na.rm = TRUE), gross2)]
Задание - Соберите статистики, сколько пользователей платящих пользователей каждого канала, сколько исходно было у них платежей, среднее значение платежей, количество и долю импутированных от общего количества платежей, насколько среднее по импутированным платежам отличаются от исходных. Дробные значения округляйте до 2 знака.
Решение
Первым делом надо присоединить метку канала пользователя. Делаем это с помощью merge()
:
payments <- merge(payments, users[, list(user_id, media_source)], by = 'user_id', all.x = TRUE)
payments[1:5]
## user_id gross is_whale gross2 is_imputed media_source
## 1: user_14 2.7557520 other 2.7557520 not-imputed FbAds
## 2: user_16 5.5066916 other 0.9492291 imputed FbAds
## 3: user_21 0.6769709 whales 0.6769709 not-imputed Ironsource
## 4: user_21 0.4285491 whales 0.4285491 not-imputed Ironsource
## 5: user_21 0.7705587 whales 0.7705587 not-imputed Ironsource
Считаем статистики по каждому каналу:
payments_imp <- payments[, list(
payers = uniqueN(user_id),
transactions = .N,
gross_mn = mean(gross),
imputed = sum(is_imputed == 'imputed'),
gross2 = mean(gross2),
delta = mean(gross) - mean(gross2)
),
by = media_source]
Округляем до второго знака. Это можно было сделать при вычислении статистик по каналам, но я, для наглядности, сделал отдельно:
payments_imp[, gross_mn := round(gross_mn, 2)]
payments_imp[, gross2 := round(gross2, 2)]
payments_imp[, delta := round(delta, 2)]
payments_imp
## media_source payers transactions gross_mn imputed gross2 delta
## 1: FbAds 9 51 1.92 5 1.32 0.60
## 2: Ironsource 10 88 1.73 10 1.08 0.65
## 3: AdWords 11 82 1.88 7 1.12 0.76
merge()
Одна из самых, наверное, важных операций при работе с таблицами — построчное слияние двух или нескольких таблиц. При использовании функции merge()
каждому значению в ключевой колонке первой таблицы сопоставляется строка параметров наблюдения другой таблицы, с таким же значением в ключевой колонке, как и в первой таблице. В других языках программирования, в SQL, в частности, аналогичная функция может называться join
.
Несмотря на сложность формулировки, выглядит это достаточно просто:
# создаем датасет 1, в синтасисе data.table
dt1 <- data.table(key_col = c('r1', 'r2', 'r3'),
col_num = seq_len(3))
# создаем датасет 2, в синтасисе data.table
dt2 <- data.table(key_col = c('r3', 'r1', 'r2'),
col_char = c('c', 'a', 'b'))
# сливаем построчно по значениям в колонке key_col
merge(x = dt1, y = dt2, by = 'key_col')
## key_col col_num col_char
## 1: r1 1 a
## 2: r2 2 b
## 3: r3 3 c
Здесь первая таблица задается аргументом x
, вторая таблица - аргументом y
, а колонка (или колонки), по значениям которой происходит объединение таблиц, задается аргументом by
. Если аргумент by
не указан, то слияние происходит по тем колонкам, которые имеют одинаковое название в объединяемых таблицах. Притом, таблицы можно объединять по значениям колонок разными именами, тогда надо отдельно указать, по значениям каких колонок в первой и второй таблице происходит объединение, и для этого вместо общего аргумента by
используют аргументы by.x
и by.y
для первой и второй таблицы соответственно.
В первом приближении операция merge()
похожа на результат работы функции cbind()
. Однако, из-за того, что при слиянии происходит сопоставление по значениям ключевых колонок, в результате решается проблема слияния таблиц, в которых разный порядок строк. Сравните:
cbind(dt1, dt2)
## key_col col_num key_col col_char
## 1: r1 1 r3 c
## 2: r2 2 r1 a
## 3: r3 3 r2 b
merge(x = dt1, y = dt2, by = 'key_col')
## key_col col_num col_char
## 1: r1 1 a
## 2: r2 2 b
## 3: r3 3 c
Второе существенное отличие от cbind()
— обработка ситуаций, когда в таблицах разное количество наблюдений. Например, в первой таблице данные по первой волне опросов, а во второй — данные по тем, кто из принявших участие в первой волне, принял участие и во второй волне, а так же какие-то новые опрошенные респонденты. Разное количество наблюдений в объединяемых таблицах порождает четыре варианта слияния, все они задаются аргументом all
с постфиксами.
Создаем данные:
# создаём данные первой волны
wave1 <- data.table(id = paste0('id_', seq_len(5)),
col1 = sample(10, 5))
wave1
## id col1
## 1: id_1 1
## 2: id_2 10
## 3: id_3 3
## 4: id_4 6
## 5: id_5 5
# создаём данные второй волны
wave2 <- data.table(id = paste0('id_', c(1, 3, 5, 6, 7, 8)),
col2 = sample(letters, 6))
wave2
## id col2
## 1: id_1 d
## 2: id_3 f
## 3: id_5 i
## 4: id_6 n
## 5: id_7 y
## 6: id_8 v
Варианты направлений объединения (мерджа) таблиц:
-
all
= FALSE. Значение аргумента по умолчанию, в результате объединения будет таблица с наблюдениями, которые есть и в первой, и во второй таблице. То есть, наблюдения из первой таблицы, которым нет сопоставления из второй таблицы, отбрасываются. В примере с волнами это будет таблица только по тем, кто принял участи и в первой, и во второй волнах опросов:
# сливаем так, чтобы оставить только тех, кто был в обеих волнах, это зачение по умолчанию
merge(x = wave1, y = wave2, by = 'id', all = FALSE)
## id col1 col2
## 1: id_1 1 d
## 2: id_3 3 f
## 3: id_5 5 i
-
all.x
= TRUE. Всем наблюдениям из первой таблицы сопоставляются значения из второй. Если во второй таблице нет соответствующих наблюдений, то пропуски заполняютсяNA
-значениями (в нашем примере в колонкеcol2
):
# сливаем так, чтобы оставить тех, кто был в первой волне
merge(x = wave1, y = wave2, by = 'id', all.x = TRUE)
## id col1 col2
## 1: id_1 1 d
## 2: id_2 10 <NA>
## 3: id_3 3 f
## 4: id_4 6 <NA>
## 5: id_5 5 i
-
all.y
= TRUE. Обратная ситуация, когда всем наблюдениям из второй таблицы сопоставляются значения из первой, и пропущенные значения заполняютсяNA
-значениями (в нашем примере в колонкеco12
):
# сливаем так, чтобы оставить тех, кто был во второй волне
merge(x = wave1, y = wave2, by = 'id', all.y = TRUE)
## id col1 col2
## 1: id_1 1 d
## 2: id_3 3 f
## 3: id_5 5 i
## 4: id_6 NA n
## 5: id_7 NA y
## 6: id_8 NA v
-
all
= TRUE. Объединение предыдущих двух вариантов — создается таблица по всему набору уникальных значений из ключевых таблиц, по которым происходит слияние. И если в какой-то из таблиц нет соответствующих наблюдений, то пропуски также заполняютсяNA
-значениями:
# сливаем так, чтобы оставить тех, кто был в какой-то из обеих волн
merge(x = wave1, y = wave2, by = 'id', all = TRUE)
## id col1 col2
## 1: id_1 1 d
## 2: id_2 10 <NA>
## 3: id_3 3 f
## 4: id_4 6 <NA>
## 5: id_5 5 i
## 6: id_6 NA n
## 7: id_7 NA y
## 8: id_8 NA v
При работе с несколькими таблицами можно столкнуться с ограничением, что базовая функции merge()
работает только с парами таблиц. То есть, если вдруг необходимо объединить по одному ключу сразу несколько таблиц (например, не две волны опросов, а пять), то придется строить последовательные цепочки попарных объединений.
Alarm! Необходимо помнить, что в ситуациях, когда одному значению ключа в первой таблице соответствует одна строка, а во второй таблице — несколько строк, то в результате объединения таблиц значения из первой таблицы размножатся по количеству строк во второй таблице:
# таблица, где на одно значение колонки ключа есть одна строка
dt1 <- data.table(
key_col = c('a', 'b', 'c'),
col1 = 1:3
)
dt1
## key_col col1
## 1: a 1
## 2: b 2
## 3: c 3
# на одно значение ключа (key_col = b)есть три строки
dt2 <- data.table(
key_col = c('a', 'b', 'b', 'b', 'c'),
col2 = rnorm(5)
)
dt2
## key_col col2
## 1: a 0.44019780
## 2: b 1.12687272
## 3: b 2.21855221
## 4: b 1.38706310
## 5: c -0.09708646
Объединяем и получаем размножение значений из первой таблицы для ключа key_col = b
, значение 2
из колонки col1
теперь встречается три раза:
merge(dt1, dt2, by = 'key_col', all.x = TRUE)
## key_col col1 col2
## 1: a 1 0.44019780
## 2: b 2 1.12687272
## 3: b 2 2.21855221
## 4: b 2 1.38706310
## 5: c 3 -0.09708646
Домашнее задание
Level 1: I’m too young to die
внимательно прочитайте материалы занятия и разберитесь с примерами.
обновите в памяти операции над векторами (создание, простейшие манипуляции, фильтрация по номеру элемента и по значению), а так же операции с таблицами: создание, фильтрация по строкам, операции над колонками, агрегаты.
Level 2: Hey, not too rough
- выполните следующие выражения. Посмотрите класс объектов, при необходимости преобразуйте в data.table:
library(data.table)
# таблица привлеченных пользователей и их стоимости
users <- fread('https://gitlab.com/upravitelev/mar201s/raw/master/data/users.csv')
str(users)
## Classes 'data.table' and 'data.frame': 100 obs. of 3 variables:
## $ user_id : chr "user_1" "user_2" "user_3" "user_4" ...
## $ media_source: chr "FbAds" "FbAds" "AdWords" "Ironsource" ...
## $ cost : num 2.61 1.51 2.95 0.97 1.44 1.07 1.88 2.22 1.7 2.94 ...
## - attr(*, ".internal.selfref")=<externalptr>
# таблица платежей привлеченных пользователей
payments <- fread('https://gitlab.com/upravitelev/mar201s/raw/master/data/payments.csv')
str(payments)
## Classes 'data.table' and 'data.frame': 221 obs. of 2 variables:
## $ user_id: chr "user_28" "user_28" "user_28" "user_28" ...
## $ gross : num 0.755 0.37 0.38 0.33 0.286 ...
## - attr(*, ".internal.selfref")=<externalptr>
- Посчитайте среднюю стоимость (
cost
) привлечения пользователей для каждого рекламного канала (media_source
). В результате у вас должна получиться такая таблица:
## media_source cost_mn
## 1: FbAds 1.66
## 2: AdWords 1.60
## 3: Ironsource 1.56
- Посчитайте сколько в среднем платят пользователи (
gross
) каждого канала привлечения пользователей. В результате у вас должна получиться такая таблица:
## media_source gross_mn
## 1: FbAds 97.79
## 2: Ironsource 151.98
## 3: AdWords 154.42
Level 3: Hurt me plenty
Повторите основные задания и добавьте к средней стоимости / платежам еще и общее количество пользователей, и количество пользователей, сделавших платеж. Для подсчета количества пользователей используйте связку функций
length()
+unique()
илиuniqueN()
Добавьте строчку
Total
— все статистики, но без разбивки по источником привлечения пользователей. В результате у вас должна получиться таблица:
## media_source total_users cost_mn payers gross_mn
## 1: AdWords 29 1.60 11 154.42
## 2: FbAds 40 1.66 9 97.79
## 3: Ironsource 31 1.56 10 151.98
## 4: Total 100 1.61 30 404.19
Level 4: Ultra-violence
NB! задания не связаны друг с другом, просто отработка разных алгоритмов.
- Отсортируйте датасет по пользователям. Создайте колонку кумулятивных платежей
gross_cum
по каждому пользователю (кумулята в рамках пользователя). Создайте колонку, в которой будет доля этого значения кумуляты от общей суммы. Вам поможет идея, что делать операции над колонками в группах можно не только при создании новой таблицы, но и при создании новых колонок.
## user_id gross gross_cum share
## 1: user_14 2.7557520 2.7557520 1.00
## 2: user_16 5.5066916 5.5066916 1.00
## 3: user_21 0.6769709 0.6769709 0.03
## 4: user_21 0.4285491 1.1055199 0.04
## 5: user_21 0.7705587 1.8760787 0.07
- Создайте переменную-маркер
gross_quant
, в котором будет маркировка пользователей, в каком квартиле находятся суммарные платежи пользователя. Подумайте, погуглите (напрямую задача вряд ли решается), как сделать это максимально лаконично (достаточно читаемо это можно сделать в два выражения, например).
## user_id gross gross_cum share gross_quant
## 1: user_14 2.7557520 2.7557520 1.00 Q1
## 2: user_16 5.5066916 5.5066916 1.00 Q1
## 3: user_21 0.6769709 0.6769709 0.03 Q4
## 4: user_21 0.4285491 1.1055199 0.04 Q4
## 5: user_21 0.7705587 1.8760787 0.07 Q4
Level 5: Nightmare
Перезагрузите payments.
Для каждого платящего пользователя сгенерируйте метрику, на какой день от инсталла пользователь сделал платеж (округляйте до целых). Для генерации используйте
rlnorm()
c meanlog = 1, sdlog = 0.5.Посчитайте сколько в среднем дней проходит между первым и третьим платежами, с разбивкой по каналам привлечения.
Сделайте все это только с одним мерджем - присоединением канала пользователя. В принципе, четырех выражений вполне хватит. Если воспользоваться некоторыми трюками и пренебречь эстетикой - то и двух.
## media_source delta_mn
## 1: FbAds 1.428571
## 2: Ironsource 0.500000
## 3: AdWords 1.125000