Граф телеграм-каналов по теме аналитики

Время чтения текста – 4 минуты

Авторы самых разных блогов в телеграме часто публикуют подборки любимых каналов, которыми они хотят поделиться со своей аудиторией. Идея, конечно, не новая, но я решил не просто составить рейтинг интересных аналитических телеграм-блогов, а решить эту задачу аналитически.

В рамках текущего курса моей учебы, я изучаю много современных подходов к анализу и визуализации данных. В самом начале курса было разминочное упражнение: объектно-ориентированное программирование на Python для сбора и итеративного построения графа с TMDB API. В задаче этот метод применяется для построения графа связи актеров, где связь — игра в одном и том же фильме. Но я решил, что можно применить его и к другой задаче: построению графа связей аналитического сообщества.

Поскольку последнее время мой временной ресурс особенно ограничен, а аналогичную задачу для курса я уже выполнил, то я решил передать эти знания кому-то еще, кто интересуется аналитикой. К счастью, в этот момент, ко мне в личку постучался кандидат на вакансию младшего аналитика данных — Андрей. Он сейчас находится в процессе постижения всех тонкостей аналитики, поэтому мы договорились на стажировку, в рамках которой Андрей спарсил данные с telegram-каналов.

Основной задачей Андрея был сбор всех текстов с телеграм-канала Интернет-аналитика, выделение каналов, на которые ссылался Алексей Никушин, сбор текстов из этих телеграм-каналов и ссылок на этих каналах. Под “ссылкой” подразумевается любое упоминание канала: через @, через ссылку или репостом. В результате парсинга, у Андрея получилось два файла: nodes и edges.
Теперь я представлю вам граф, который получился у меня на основе этих данных и прокомментирую результаты.

Пользуясь случаем, хочу выразить мое почтение команде karpov.courses, поскольку у Андрея отличное знание языка Python!

В результате топ-10 каналов по показателю degree (количество связей) выглядит так:

  1. Интернет-аналитика
  2. Reveal The Data
  3. Инжиниринг Данных
  4. Data Events
  5. Datalytics
  6. Чартомойка
  7. LEFT JOIN
  8. Epic Growth
  9. RTD: ссылки и репосты
  10. Дашбордец

По-моему, получилось супер-круто и визуально интересно, а Андрей — большой молодец! Кстати, он тоже начал свой канал ”Это разве аналитика?”, где публикуются новости аналитики.

Забегая вперед: у этой задачи имеется продолжение. С помощью Марковской цепи мы смоделировали в каком канале окажется пользователь, если будет переходить итеративно по всем упоминаниям в каналах. Получилось очень интересно, но об этом мы расскажем в следующий раз!

 Нет комментариев    754   25 дн   Data Analytics   python

Принципы построения bubble-charts: площадь VS радиус

Время чтения текста – 6 минут

Такой навык как визуализация данных применяется в любой отрасли, где присутствуют данные, ведь таблицы хороши лишь для хранения информации. Когда есть необходимость презентовать данные, точнее определенные выводы, полученные на их основе — данные необходимо представить на графиках подходящего типа. И тут перед вами встает две задачи: первая — правильно подобрать тип графика, вторая — правдоподобно отразить результаты на диаграмме. Сегодня мы расскажем вам об одной ошибке, которую иногда допускают дизайнеры при визуализации данных на bubble-charts и о том, как эту ошибку можно избежать.

Суть построения bubble-чарта

Немного скучной теории перед тем, как мы приступим к анализу данных. Bubble-chart — удобный способ показать три параметра наблюдения без построения трехмерной модели. По привычным осям X и Y указываются значения двух параметров, а третий показан размером круга, который соответствует каждому наблюдению. Именно это позволяет избежать необходимости построения сложного 3D графика, то есть любой, кто видит bubble-chart, гораздо быстрее сможет сделать выводы о данных изображенных на одной плоскости.

Ошибка, которую может допустить дизайнер, но не аналитик данных

С метриками, которые отображены на осях графика не возникает никаких вопросов, это привычный способ их визуализации, а вот с размерами возникает некоторая трудность: как грамотно и точно отобразить изменения в значениях переменной, если управление идет не точкой на оси, а размером этой точки?
Дело в том, что при построении такого графика без использования аналитических средств, например, в графическом редакторе, автор может нарисовать круги, принимая радиус круга за его размер. На первый взгляд, все кажется абсолютно корректным — чем больше значение переменной, тем больше радиус круга. Однако, в таком случае, площадь круга будет увеличиваться не как линейная, а как степенная функция, ведь S = π × r2. Например, на рисунке ниже показано, что, если увеличить радиус круга в два раза, то площадь увеличится в 4 раза.


Построение круга в Matplotlib

fig = plt.figure(figsize=(10, 10))
ax = fig.add_subplot(1, 1, 1)
s = 4*10e3


ax.scatter(100, 100, s=s, c='r')
ax.scatter(100, 100, s=s/4 ,c='b')
ax.scatter(100, 100, s=10, c='g')
plt.axvline(99, c='black')
plt.axvline(101, c='black')
plt.axvline(98, c='black')
plt.axvline(102, c='black')


ax.set_xticks(np.arange(95, 106, 1))
ax.grid(alpha=1)

plt.show()

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

Как построить такой график правильно?

К счастью, если строить bubble-charts с помощью библиотек Python (Matplotlib и Seaborn), то размер круга будет определяться именно площадью, что абсолютно корректно и грамотно с точки зрения визуализации.
Сейчас на примере реальных данных, найденных на Kaggle, покажем, как построить bubble-chart правильно. В данных присутствуют следующие переменные: страна, численность населения, процент грамотного населения. Для того чтобы диаграмма была читаемой, возьмем подвыборку из 10 первых стран после сортировки всех данных по возрастанию ВВП.

Для начала, загрузим все нужные библиотеки:

import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

Затем, загрузим данные, очистим и от всех строк с пропущенными значениями и приведем данные по численности населения стран в миллионы:

data = pd.read_csv('countries of the world.csv', sep = ',')
data = data.dropna()
data = data.sort_values(by = 'Population', ascending = False)
data = data.head(10)
data['Population'] = data['Population'].apply(lambda x: x/1000000)

Теперь, когда все подготовка завершена, можно построить bubble-chart:

sns.set(style="darkgrid")    
fig, ax = plt.subplots(figsize=(10, 10))    
g = sns.scatterplot(data=data, x="Literacy (%)", y="GDP ($ per capita)", size = "Population", sizes=(10,1500), alpha=0.5)
plt.xlabel("Literacy (Percentage of literate citizens)")
plt.ylabel("GDP per Capita")
plt.title('Chart with bubbles as area', fontdict= {'fontsize': 'x-large'})

def label_point(x, y, val, ax):
    a = pd.concat({'x': x, 'y': y, 'val': val}, axis=1)
    for i, point in a.iterrows():
        ax.text(point['x'], point['y']+500, str(point['val']))

label_point(data['Literacy (%)'], data['GDP ($ per capita)'], data['Country'], plt.gca()) 

ax.legend(loc='upper left', fontsize = 'medium', title = 'Population (in mln)', title_fontsize = 'large', labelspacing = 1)

plt.show()

На этом графике получилось понятным образом отобразить три метрики: уровень ВВП на душу населения по оси Y, процент грамотного населения по оси X и численность населения — площадью круга.

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

 Нет комментариев    238   1 мес   Data Analytics   python

Различия между медианой и средним арифметическим как целевым показателем анализа данных

Время чтения текста – 11 минут

В сегодняшней статье мы бы хотели осветить простую, но в то же время важную тему выбора простой метрики для оценки того или иного датасета. Со средним арифметическим все давным давно знакомы, чуть ли не каждый школьник отлично знает, что нужно просуммировать все имеющиеся значения, поделить на их количество и получить среднее значение. В школьные знания не входят никакие альтернативные варианты, которых, на самом деле, в статистике много — на любой вкус и случай. Однако, в решении исследовательских и маркетинговых задач люди часто берут именно эту метрику за основу. Правомерно ли это или есть более удачный вариант? Давайте разбираться.

Для начала стоит вспомнить определения двух метрик, о которых мы сегодня поговорим.
Среднее  — самый популярный статистический показатель, который используется для измерения центра данных. А что же такое медиана? Медиана — значение, которое разбивает данные, отсортированные по порядку увеличения значений, на две равные части. Это значит, что медиана показывает центральное значение в выборке, если наблюдений нечетное количество и среднее арифметическое двух значений, если количество наблюдений в выборке четно.

Исследовательские задачи

Итак, оценка среднего значения выборки — зачастую важна во многих исследовательских вопросах. Например, специалисты, изучающие демографию часто задаются вопросом изменения численности регионов России, чтобы проследить за динамикой и отразить это в отчетностях. Давайте попробуем рассчитать среднюю численность региона России, а также медиану, а затем сравним полученные результаты.
Для начала, нужно найти и загрузить данные, подключив для этого библиотеку pandas.

import pandas as pd
population = pd.read_csv('city.csv')

Затем, нужно посчитать среднее и медиану выборки.

mean_pop = round(city.population_2020.mean(), 0)
median_pop = round(city.population_2020.median(), 0)

Значения, естественно, получились разными, так как распределение наблюдений в выборке отлично от нормального. Для того, чтобы понять, сильно ли они отличаются, построим график распределения и отметим среднее и медиану.

import matplotlib.pyplot as plt
import seaborn as sns

sns.set_palette('rainbow')
fig = plt.figure(figsize = (20, 15))
ax = fig.add_subplot(1, 1, 1)
g = sns.histplot(data = city, x= 'population_2020', alpha=0.6, bins = 100, ax=ax)

g.axvline(mean_pop, linewidth=2, color='r', alpha=0.9, linestyle='--', label = 'Среднее = {:,.0f}'.format(mean_pop).replace(',', ' '))
g.axvline(median_pop, linewidth=2, color='darkgreen', alpha=0.9, linestyle='--', label = 'Медиана = {:,.0f}'.format(median_pop).replace(',', ' '))

plt.ticklabel_format(axis='x', style='plain')
plt.xlabel("Численность населения", fontsize=25)
plt.ylabel("Количество городов", fontsize=25)
plt.title("Распределение численности населения российских городов", fontsize=25)
plt.legend(fontsize="xx-large")
plt.show()

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

fig = plt.figure(figsize = (10, 10))
sns.set_theme(style="whitegrid")
sns.set_palette(palette="pastel")

sns.boxplot(y = city['population_2020'], showfliers = False)

plt.scatter(0, 550100, marker='*', s=100, color = 'black', label = 'Выбросы')
plt.scatter(0, 560200, marker='*', s=100, color = 'black')
plt.scatter(0, 570300, marker='*', s=100, color = 'black')
plt.scatter(0, mean_pop, marker='o', s=100, color = 'red', edgecolors = 'black', label = 'Среднее')
plt.legend()

plt.ylabel("Численность населения", fontsize=15)
plt.ticklabel_format(axis='y', style='plain')
plt.title("Боксплот численности населения", fontsize=15)
plt.show()

Из графиков следует, что медиана существенно меньше среднего, а также, ясно, что это следствие наличия больших выбросов — Москвы и Санкт-Петербурга. Поскольку среднее арифметическое — метрика крайне чувствительная к выбросам — при их наличии в выборке опираться на выводы относительно среднего не стоит. Рост или снижение численности населения Москвы может сильно смещать среднюю численность по России, однако это не будет влиять на настоящий общерегиональный тренд.
Используя среднее арифметическое мы скажем, что численность типичного (среднего) города в РФ — 268 тысяч человек. Однако, это вводит нас в заблуждение, так как среднее значительно превышает медиану исключительно из-за численности населения Москвы и Санкт-Петербурга. На самом деле, численность типичного российского города существенно меньше (аж в 2 раза!) и составляет 104 тысячи жителей.

Маркетинговые задачи

В контексте бизнеса разница между средним арифметическим и медианой также важна, так как использование неверной метрики может серьезно сказаться на результатах проведения акции или затруднить достижение цели. Давайте посмотрим на реальном примере, с какими трудностями может столкнуться предприниматель в ритейле, если неверно выберет целевую метрику.
Для начала, как и в предыдущем примере, загрузим датасет о покупках в супермаркете. Выберем необходимые для анализа столбцы датасета и переименуем их, для упрощения кода в дальнейшем. Поскольку эти данные не так хорошо подготовлены, как предыдущие, необходимо сгруппировать все купленные товары по чекам. В этом случае необходима группировка по двум переменным: по id покупателя и по дате покупки (дата и время определяется моментом закрытия чека, поэтому все покупки в рамках одного чека совпадают по дате). Затем, назовем полученный столбец «total_bill», то есть сумма чека и посчитаем среднее и медиану.

df = pd.read_excel('invoice_data.xlsx')
df_nes = df[['Номер КПП', 'Сумма', 'Дата продажи']]
df_nes.columns = ['user','total_price', 'date']
groupped_df = pd.DataFrame(df_nes.groupby(['user', 'date']).total_price.sum())
groupped_df.columns = ['total_bill']
mean_bill = groupped_df.total_bill.mean()
median_bill = groupped_df.total_bill.median()

Теперь, как и в предыдущем примере нужно построить график распределения чеков покупателей и боксплот, а также отметить медиану и среднее арифметическое на каждом из них.

sns.set_palette('rainbow')
fig = plt.figure(figsize = (20, 15))
ax = fig.add_subplot(1, 1, 1)
sns.histplot(groupped_df, x = 'total_bill', binwidth=200, alpha=0.6, ax=ax)
plt.xlabel("Покупки", fontsize=25)
plt.ylabel("Суммы чеков", fontsize=25)
plt.title("Распределение суммы чеков", fontsize=25)
plt.axvline(mean_bill, linewidth=2, color='r', alpha=1, linestyle='--', label = 'Среднее = {:.0f}'.format(mean_bill))
plt.axvline(median_bill, linewidth=2, color='darkgreen', alpha=1, linestyle='--', label = 'Медиана = {:.0f}'.format(median_bill))
plt.legend(fontsize="xx-large")
plt.show()
fig = plt.figure(figsize = (10, 10))
sns.set_theme(style="whitegrid")
sns.set_palette(palette="pastel")

sns.boxplot(y = groupped_df['total_bill'], showfliers = False)

plt.scatter(0, 1800, marker='*', s=100, color = 'black', label = 'Выбросы')
plt.scatter(0, 1850, marker='*', s=100, color = 'black')
plt.scatter(0, 1900, marker='*', s=100, color = 'black')
plt.scatter(0, mean_bill, marker='o', s=100, color = 'red', edgecolors = 'black', label = 'Среднее')
plt.legend()

plt.ticklabel_format(axis='y', style='plain')
plt.ylabel("Сумма чека", fontsize=15)
plt.title("Боксплот суммы чеков", fontsize=15)
plt.show()

Из графиков следует, что распределение смещено к началу координат (отличное от нормального), а значит медиана и среднее не равны. Медианное значение меньше среднего примерно на 220 рублей.
Теперь представим, что у маркетологов есть задача повысить средний чек покупателя. Маркетолог может решить, что поскольку средний чек равен 601 рублю, то можно предложить следующую акцию: «Всем покупателям, кто совершит покупку на 600 рублей, мы предоставляем скидку 20% на товар за 100 рублей». В целом, резонное предложение, однако, в реальности, средний чек ниже — 378 рублей. То есть большая часть покупателей не заинтересуется в предложении, поскольку их покупка обычно не достигает предложенного порога. Это значит. что они не воспользуются предложением и не получат скидку, а компания не сможет достичь поставленной цели и увеличить прибыль супермаркета. Все дело в том, что исходные предпосылки были ошибочны.

Выводы

Как вы уже поняли, среднее арифметическое зачастую показывает более значимый и приятный результат, как для бизнеса, так и для исследовательских задач, ведь руководству всегда выгоднее представить ситуацию со средним чеком или демографической ситуацией в стране лучше, чем она есть на самом деле. Однако, необходимо всегда помнить о недостатках такой метрики, как среднее арифметическое, чтобы уметь грамотно выбрать подходящий аналог для оценки той или иной ситуации.

 Нет комментариев    629   1 мес   analysis   Data Analytics   matplotlib   pandas

Моделирование LTV в SQL

Время чтения текста – 13 минут

У большинства игровых и мобильных компаний имеется кривая Retention, ранее мы писали о том, что такое Retention и как его посчитать. Вкратце — это метрика, которая позволяет понять насколько хорошо продукт вовлекает пользователей в ежедневное использование. А ещё при помощи Retention и ARPDAU можно посчитать LTV (Lifetime Value), пожизненный доход с одного пользователя. Зная средний доход с пользователя за день и кривую Retention мы можем смоделировать ее и спрогнозировать LTV.

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

В сегодняшнем материале мы подробно разберём, как смоделировать LTV для 180 дней при помощи SQL и просто линейной регрессии.

Как посчитать LTV?

В общем случае формула LTV выглядит как ARPDAU умноженное на Lifetime — время жизни пользователя в проекте.

Посмотрим на классический график Retention:

Lifetime — это площадь фигуры под Retention:

Откуда взялись интегралы и площади можно подробнее узнать в этом материале

Значит, чтобы посчитать Lifetime, нужно взять интеграл от функции удержания по времени. Формула приобретает следующий вид:

Для описания кривой Retention лучше всего подходит степенная функция a*x^b. Вот как она выглядит в сравнении с кривой Retention:

При этом x — номер дня, a и b — параметры функции, которую мы построим при помощи линейной регрессии. Регрессия появилась неслучайно — эту степенную функцию можно привести к виду линейной функции, логарифмируя:

ln(a) — intercept, b — slope. Остаётся найти эти параметры — в линейной регрессии для этого используют метод наименьших квадратов. Lifetime — кумулятивная сумма прогноза за 180 дней. Посчитав её, остаётся умножить Lifetime на ARPDAU и получим LTV за 180 дней.

Строим LTV

Перейдём к практике. Для всех расчётов мы использовали данные одной игровой компании и СУБД PostgreSQL — в ней уже реализованы функции поиска параметров для линейной регрессии. Начнём с построения Retention: соберём общее количество пользователей в период с 1 марта по 1 апреля 2021 года — мы изучаем активность за один месяц:

--общее количество юзеров в когорте
with cohort as (
    select count(distinct id) as total_users_of_cohort
    from users
    where date(registration) between date '2021-03-01' and date '2021-03-30'
),

Теперь посмотрим, как ведут себя эти пользователи в последующие 90 дней:

--количество активных юзеров на 1ый день, 2ой, 3ий и тд. из когорты
active_users as (
    select date_part('day', activity.date - users.registration) as activity_day, 
               count(distinct users.id) as active_users_of_day
    from activity
    join users on activity.user_id = users.id
    where date(registration) between date '2021-03-01' and date '2021-03-30' 
    group by 1
    having date_part('day', activity.date - users.registration) between 1 and 90 --берем только первые 90 дней, остальные дни предсказываем.
),

Кривая Retention — отношение количества активных пользователей к размеру когорты текущего дня. В нашем случае она выглядит так:

По данным кривой посчитаем параметры для линейной регрессии. regr_slope(x, y) — функция для вычисления наклона регрессии, regr_intercept(x, y) — функция для вычисления перехвата по оси Y. Эти функции являются стандартными агрегатными функциями в PostgreSQL и для известных X и Y по методу наименьших квадратов.

Вернёмся к нашей формуле — мы получили линейное уравнение, и хотим найти коэффициенты линейной регрессии. Перехват по оси Y и коэффициент наклона можем найти по дефолтным для PostgreSQL функциям. Получается:

Подробнее о том, как работают функции intercept(x, y) и slope(x, y) можно почитать в этом мануале

Из свойства натурального логарифма следует, что:

Наклон считаем аналогичным образом:

Эти же вычисления запишем в подзапрос для расчёта коэффициентов регрессии:

--рассчитываем коэффициенты регрессии
coef as (
    select exp(regr_intercept(ln(activity), ln(activity_day))) as a, 
                regr_slope(ln(activity), ln(activity_day)) as b
    from(
                select activity_day,
                            active_users_of_day::real / total_users_of_cohort as activity
                from active_users 
                cross join cohort order by activity_day 
            )
),

И получим прогноз на 180 дней, подставив параметры в степенную функцию, описанную ранее. Заодно посчитаем Lifetime — кумулятивную сумму спрогнозированных данных. В подзапросе coef мы получим только два числа — параметр наклона и перехвата. Чтобы эти параметры были доступны каждой строке подзапроса lt, делаем cross join к coef:

lt as(
    select generate_series as activity_day,
               active_users_of_day::real/total_users_of_cohort as real_data,
               a*power(generate_series,b) as pred_data, 	 
               sum(a*power(generate_series,b)) over(order by generate_series) as cumulative_lt
    from generate_series(1,180,1)
    cross join coef
    join active_users on generate_series = activity_day::int
),

Сравним прогноз на 180 дней с Retention:

Наконец, считаем сам LTV — Lifetime, умноженный на ARPDAU. В нашем случае ARPDAU равняется $83.7:

select cumulative_lt as LT,
           cumulative_lt * 83.7 as LTV
from lt

Наконец, построим график LTV на 180 дней:

Весь запрос:

--общее количество юзеров в когорте
with cohort as (
    select count(*) as total_users_of_cohort
    from users
    where date(registration) between date '2021-03-01' and date '2021-03-30'
),
--количество активных юзеров на 1ый день, 2ой, 3ий и тд. из когорты
active_users as (
    select date_part('day', activity.date - users.registration) as activity_day, 
               count(distinct users.id) as active_users_of_day
    from activity
    join users on activity.user_id = users.id
    where date(registration) between date '2021-03-01' and date '2021-03-30' 
    group by 1
    having date_part('day', activity.date - users.registration) between 1 and 90 --берем только первые 90 дней, остальные дни предсказываем.
),
--рассчитываем коэффициенты регрессии
coef as (
    select exp(regr_intercept(ln(activity), ln(activity_day))) as a, 
                regr_slope(ln(activity), ln(activity_day)) as b
    from(
                select activity_day,
                            active_users_of_day::real / total_users_of_cohort as activity
                from active_users 
                cross join cohort order by activity_day 
            )
),
lt as(
    select generate_series as activity_day,
               active_users_of_day::real/total_users_of_cohort as real_data,
               a*power(generate_series,b) as pred_data, 	 
               sum(a*power(generate_series,b)) over(order by generate_series) as cumulative_lt
    from generate_series(1,180,1)
    cross join coef
    join active_users on generate_series = activity_day::int
),
select cumulative_lt as LT,
            cumulative_lt * 83.7 as LTV
from lt
 Нет комментариев    636   2 мес   Data Analytics   ltv   postgresql   sql

Анализ альбомов Земфиры: дашборд в Tableau

Время чтения текста – 2 минуты

В марте мы опубликовали исследование «Python и тексты нового альбома Земфиры: анализируем суть песен», в котором при помощи Word2Vec-модели проанализировали близость песен альбома «бордерлайн» и получили самые близкие слова по духу альбома — ими оказались «пламень», «гореть», «тоска», «печаль», «сердце», «солнце» и другие.

Мы продолжили работу над альбомами Земфиры и проанализировали семь из них, а затем результаты собрали в один дашборд и опубликовали его в Tableau Public. Посмотрите, что получилось.

Заглавная страница — общий анализ семи альбомов Земфиры. Переключиться на конкретный альбом можно по нажатию на его иконку внизу страницы. Для каждого альбома представлена матрица семантической близости песен, облако слов и топ схожих слов для альбома.

 Нет комментариев    786   3 мес   Data Analytics   python   tableau   земфира
Ранее Ctrl + ↓