Анимируем теннисные мячики в Tableau

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

В прошлом видео мы научились визуализировать теннисные мячики из Swing Vision на графике в Tableau с кастомным фоном и кастомными фигурами. Сегодня будем анимировать дашборд, чтобы посмотреть, как менялись удары и получим видео с результатами игры, которое можно экспортировать и кому-нибудь показать.

Краткое резюме:

  1. Создаём элемент Pages. Он позволяет управлять движением анимации: нажимая на кнопку Play, Title страницы будет меняться.
  2. Добавим историю: ставим галочку на Show History ниже и выберем длину в 7 ударов.
  3. Вернёмся на дашборд. Переходим во вкладку Worksheet — Show Cards и выбираем Current Page.
  4. Для захвата видео с экрана добавим новый контейнер и перенесём в него панель с ударами.
  5. Утилитой записи экрана выбираем область с дашбордом и жмём на Play. Для macOS можно воспользоваться встроенной: достаточно нажать комбинацию клавиш ⌘ + Shift + 5.
 Нет комментариев    38   5 дн   bi   BI-инструменты   tableau   визуализация

Теннисные мячики из статистики Swing Vision на графике в Tableau

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

Я увлекаюсь теннисом и недавно обнаружил относительно свежее приложение, помогающее теннисистам понять качество своей игры — Swing Vision. Приложение чудесное: в реальном времени распознает удары ракеткой по мячу и отображает координаты каждого удара. Автор приложения — профессор UC Berkeley, Swupnil Sahai, недавно я написал ему на почту и поблагодарил за такую полезную вещь.

Так выглядит статистика в приложении

Приложение позволяет посмотреть свои «ралли» и конкретные удары, понять среднюю скорость удара и узнать процент ошибок. Помимо этого в приложении есть возможность экспорта статистики о собственных ударах в формате xls-файла.

Так выглядит xls-файл

В этом видео я расскажу, как визуализировать данные из приложения в Tableau и получить красивую статистику собственных ударов.

Краткое резюме:

  1. Подготавливаем изображение теннисного корта
  2. Импортируем данные из xls-файла в Tableau.
  3. Создаём новые Calculated Fields, в которых преобразуем вертикально и горизонтально координаты удара.
  4. Для всех мячей, попавших в сетку, ставим конкретную координату по Y = -11,89.
  5. Фильтруем, добавляем цвета и размечаем данные на графике:
    a. Работаем с фоновым изображением
    b. Считаем ratio корта на изображении к корту в метрах. К примеру, в моём случае ширина 913px, а ширина корта — 10,97 метров. Значит, соотношение — 83,2270
    c. Считаем отступы, которые нужно сделать от 0 коориднат
    d. Аналогичные действия проделываем с Y-координатами
    e. Добавляем фоновое изображение и устанавливаем его свойства
  6. Находим векторную иконку тенисного мяча.
  7. Кладём иконку в папку Shapes репозитория Tableau.
  8. Меняем иконку в меню Shapes.

В результате получаем:

 Нет комментариев    115   11 дн   BI-инструменты   dashboard   tableau

Постановка задачи для дашборда

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

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

Постановка задачи

Наша задача — спроектировать и реализовать дашборд на тестовом датасете SuperStore Sales (он, кстати, весьма приближен к реальности), который ответит на следующие вопросы:

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

О датасете

В данных содержится информация о покупках (лист Orders) и возвратах (лист Returns) клиентов. Данные о покупках содержат всю доступную информацию о заказах: идентификаторы записи, даты заказов, приоритетность заказов, количество позиций, объём продаж и размер прибыли, размеры скидкок, типы и цены доставки, данные о клиентах и прочую полезную информацию. Мы будем работать только с листом данных Orders.

Фрагмент листа Orders

Макет дашборда

В верхней панели расположен заголовок дашборда с его кратким описанием. На том же уровне фильтр временного отрезка (конкретный месяц отчета). На уровне ниже — подзаголовок «KPI».

В первую очередь мы хотим понять общие ключевые изменения, поэтому далее расположим фактоиды:

  • Прибыль (Profit) и прирост YoY
  • Продажи (Sales) и прирост YoY
  • Количество заказов (Orders count) и прирост YoY
  • Средняя скидка (Avg Discount) и прирост YoY
  • Число клиентов (Customers) и прирост YoY
  • Продажи на клиента (Sales per Customer) и прирост YoY

Ниже будет расположен график с топом регионов по продажам, визуализированный в виде древовидной карты (или аналога). Размер прямоугольника на графике будет соответствовать объёму продаж, а цвет — показателю прибыли. Такая визуализация даст понять, какие регионы эффективны, а какие нет. Классно, если у исследуемого BI-инструмента будет возможность получить расширенную информацию при клике на регион и посмотреть, чем отличаются прибыльные регионы от неэффективных.

Правее расположим график с динамикой прибыли: в нём будем рассматривать, как в зависимости от времени меняется показатель. Хочется, чтобы на графике точками были отмечены анализируемый месяц и аналогичный месяц год назад для понимания тренда.

Следующий блок анализа — продукты и клиентские сегменты. На левой горизонтальной столбчатой диаграмме типа «Градусник» расположим объём продаж и прибыль по категориям и подкатегориям. По возможности BI-системы к диаграмме добавим фильтр топа товарных наименований по прибыли.

О том, как строить диаграмму Градусник в Python можно почитать в материале о красивой визуализации

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

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

В итоге получится приблизительно такой макет дашборда:

Предполагаемый макет дашборда в BI-инструменте
 Нет комментариев    275   23 дн   bi   BI-инструменты

Гайд по современным BI-системам

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

В новой серии постов постараемся подробно изучить различные BI-системы на популярной группе датасетов SuperStore Sales. В основе данных — продажи и прибыль сетевого ритейлера в долларах.

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

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

Ниже перечислен перечень BI-систем и инструментов для работы, с данными, которые хотелось бы опробовать и описать опыт построения дашборда. Приглашаю тех, кто желает поучаствовать в решении данной задачи написать мне в Telegram — @valiotti. Разумеется, авторство дашборда будет указано. Проект некоммерческий, но полезный для сравнения современных систем для аналитики независимо от квадрантов Gartner.

Сейчас в планах подготовить материалы о следующих инструментах:

Бесплатные (Open source):

  • Metabase
  • Redash
  • Apache Superset
  • Dash / Plotly

Бесплатные (cloud):

  • Google Studio
  • Yandex Datalens

Платные (cloud):

  • Mode
  • Cluvio
  • Holistic
  • Chartio
  • Periscope
  • DeltaDNA
  • Klipfolio
  • Count.co

Платные:

  • PowerBI
  • Tableau
  • Looker
  • Excel
  • Alteryx
  • Qlik Sense
  • Qlik View

Итоговая цель — оценить системы по нескольким внутренним критериям:

  • порог входа в инструмент (1 — супер сложно, 10 — легко)
  • функциональность инструмента (1 — очень бедный функционал, 10 — сложно что-то добавить)
  • удобство пользования (1 — очень неудобно, 10 — супер удобно)
  • соответствие результата задаче (1 — совсем не попали в желаемый макет, 10 — очень близко к описанию и макету)
  • визуальная составляющая (1 — выглядит непривлекательно, 10 — визуально привлекательный дашборд)

На основе полученных внутренних оценок будет рассчитана интегральная взвешенная оценка для инструмента.

Параллельно, результаты работы будут представлены в Telegram-канале @leftjoin, и подписчики также смогут высказать свое мнение относительно полученного результата.
В итоге каждый инструмент будет описан точкой на плоскости, а сама плоскость будет поделена на 4 части.

По мере написания новых материалов в цикле этот пост будет обновляться: будут добавляться ссылки на посты и оценки.

 Нет комментариев    373   1 мес   bi   BI-инструменты   excel   looker   powerbi   redash   tableau

Создаём дашборд на Bootstrap (Часть 2)

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

В последнем материале мы подготовили базовый макет дашборда при помощи библиотеки dash-bootstrap-components с двумя графиками: scatter plot и российской картой, которые подробно разбирали ранее. Сегодня продолжим наполнять дашборд информацией: встроим в него таблицы и фильтр данных по пивоварням.

Получение таблиц

Сами таблицы будем описывать в макете в файле application.py, но информацию, которую они отображают лаконичнее будет получить в отдельном модуле. Создадим файл get_tables.py: в нём будет функция, передающая готовую таблицу класса Table библиотеки dbc в application.py. В этом материале мы опишем только таблицу лучших пивоварен России, но на GithHub будут представлены все три.

В таблицах по заведениям и пивоварням мы реализуем фильтр по городам, но изначально города в собранных с Untappd данных записаны на латинице. Для запросов мы будем переводить русскоязычные наименования городов на английский при помощи библиотеки Google Translate. Кроме того, одни и те же города могут называться по-разному — например, «Москва» на латинице где-то записана как «Moskva», а где-то как «Moscow». Для этого дополнительно настроим маппинг наименований города и заранее создадим словарь с корректными наименованиями основных городов. Он пригодится в самом конце.

import pandas as pd
import dash_bootstrap_components as dbc
from clickhouse_driver import Client
import numpy as np
from googletrans import Translator

translator = Translator()

client = Client(host='12.34.56.78', user='default', password='', port='9000', database='')

city_names = {
   'Moskva': 'Москва',
   'Moscow': 'Москва',
   'СПБ': 'Санкт-Петербург',
   'Saint Petersburg': 'Санкт-Петербург',
   'St Petersburg': 'Санкт-Петербург',
   'Nizhnij Novgorod': 'Нижний Новгород',
   'Tula': 'Тула',
   'Nizhniy Novgorod': 'Нижний Новгород',
}

Таблица лучших пивоварен

Таблица, о которой идёт речь в материале, будет показывать топ-10 лучших российских пивоварен с изменением рейтинга. То есть мы сравниваем данные за два периода: [30 дней назад; сегодня] и [60 дней назад; 30 дней назад] и смотрим, как менялось место пивоварни в рейтинге. Соответственно, мы опишем следующие колонки: место в рейтинге, название пивоварни, ассортимент сортов пива, рейтинг пивоварни на untappd, изменение места и количество чекинов у этой пивоварни.
Опишем функцию get_top_russian_breweries, которая отправляет запрос к Clickhouse, получает общий топ пивоварен России, формирует данные и возвращает готовый для вывода DataFrame. Отправим два запроса — топ пивоварен за последние 30 дней и топ пивоварен за предыдущие 30 дней. Следующий запрос будет отбирать лучшие пивоварни, основываясь на количестве отзывов о пиве данной пивоварни.


Забираем данные из базы

def get_top_russian_breweries(checkins_n=250):
   top_n_brewery_today = client.execute(f'''
      SELECT  rt.brewery_id,
              rt.brewery_name,
              beer_pure_average_mult_count/count_for_that_brewery as avg_rating,
              count_for_that_brewery as checkins FROM (
      SELECT           
              brewery_id,
              dictGet('breweries', 'brewery_name', toUInt64(brewery_id)) as brewery_name,
              sum(rating_score) AS beer_pure_average_mult_count,
              count(rating_score) AS count_for_that_brewery
          FROM beer_reviews t1
          ANY LEFT JOIN venues AS t2 ON t1.venue_id = t2.venue_id
          WHERE isNotNull(venue_id) AND (created_at >= (today() - 30)) AND (venue_country = 'Россия') 
          GROUP BY           
              brewery_id,
              brewery_name) rt
      WHERE (checkins>={checkins_n})
      ORDER BY avg_rating DESC
      LIMIT 10
      '''
   )

top_n_brewery_n_days = client.execute(f'''
  SELECT  rt.brewery_id,
          rt.brewery_name,
          beer_pure_average_mult_count/count_for_that_brewery as avg_rating,
          count_for_that_brewery as checkins FROM (
  SELECT           
          brewery_id,
          dictGet('breweries', 'brewery_name', toUInt64(brewery_id)) as brewery_name,
          sum(rating_score) AS beer_pure_average_mult_count,
          count(rating_score) AS count_for_that_brewery
      FROM beer_reviews t1
      ANY LEFT JOIN venues AS t2 ON t1.venue_id = t2.venue_id
      WHERE isNotNull(venue_id) AND (created_at >= (today() - 60) AND created_at <= (today() - 30)) AND (venue_country = 'Россия')
      GROUP BY           
          brewery_id,
          brewery_name) rt
  WHERE (checkins>={checkins_n})
  ORDER BY avg_rating DESC
  LIMIT 10
  '''
)

Формируем из полученных строк два DataFrame:

top_n = len(top_n_brewery_today)
column_names = ['brewery_id', 'brewery_name', 'avg_rating', 'checkins']

top_n_brewery_today_df = pd.DataFrame(top_n_brewery_today, columns=column_names).replace(np.nan, 0)
top_n_brewery_today_df['brewery_pure_average'] = round(top_n_brewery_today_df.avg_rating, 2)
top_n_brewery_today_df['brewery_rank'] = list(range(1, top_n + 1))

top_n_brewery_n_days = pd.DataFrame(top_n_brewery_n_days, columns=column_names).replace(np.nan, 0)
top_n_brewery_n_days['brewery_pure_average'] = round(top_n_brewery_n_days.avg_rating, 2)
top_n_brewery_n_days['brewery_rank'] = list(range(1, len(top_n_brewery_n_days) + 1))

А затем в итераторе считаем, как изменилось место за последнее время у пивоварни. Обработаем исключение на случай, если 60 дней назад этой пивоварни в нашей базе ещё не было.

rank_was_list = []
for brewery_id in top_n_brewery_today_df.brewery_id:
   try:
       rank_was_list.append(
           top_n_brewery_n_days[top_n_brewery_n_days.brewery_id == brewery_id].brewery_rank.item())
   except ValueError:
       rank_was_list.append('–')
top_n_brewery_today_df['rank_was'] = rank_was_list

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

diff_rank_list = []
for rank_was, rank_now in zip(top_n_brewery_today_df['rank_was'], top_n_brewery_today_df['brewery_rank']):
   if rank_was != '–':
       difference = rank_was - rank_now
       if difference > 0:
           diff_rank_list.append(f'↑ +{difference}')
       elif difference < 0:
           diff_rank_list.append(f'↓ {difference}')
       else:
           diff_rank_list.append('–')
   else:
       diff_rank_list.append(rank_was)

Наконец, разметим итоговый DataFrame и вставим в него колонку с текущим местом. При этом у топ-3 будет отображаться эмодзи с золотым кубком.

df = top_n_brewery_today_df[['brewery_name', 'avg_rating', 'checkins']].round(2)
df.insert(2, 'Изменение', diff_rank_list)
df.columns = ['НАЗВАНИЕ', 'РЕЙТИНГ', 'ИЗМЕНЕНИЕ', 'ЧЕКИНОВ']
df.insert(0, 'МЕСТО',
         list('🏆 ' + str(i) if i in [1, 2, 3] else str(i) for i in range(1, len(df) + 1)))

return df

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

Прежде всего, функция принимает конкретный город. Мы уже отметили, что города в базе данных записаны на латинице — поэтому сначала переводим наименование города. В случае с Санкт-Петербургом, Нижним Новгородом и Пермью придётся перевести вручную: например, Санкт-Петербург переводится в Google Translate как St. Petersburg вместо ожидаемого Saint Petersburg.

ru_city = venue_city
if ru_city == 'Санкт-Петербург':
   en_city = 'Saint Petersburg'
elif ru_city == 'Нижний Новгород':
   en_city = 'Nizhnij Novgorod'
elif ru_city == 'Пермь':
   en_city = 'Perm'
else:
   en_city = translator.translate(ru_city, dest='en').text

Следующее отличие — запрос к базе. Нам нужно добавить в него условие совпадения по городу, чтобы получать чекины только в запрошенном городе:

WHERE (rt.venue_city='{ru_city}' OR rt.venue_city='{en_city}')

Наконец, сформированный DataFrame мы не возвращаем, а сохраняем в директорию data/cities.

df = top_n_brewery_today_df[['brewery_name', 'venue_city', 'avg_rating', 'checkins']].round(2)
df.insert(3, 'Изменение', diff_rank_list)
df.columns = ['НАЗВАНИЕ', 'ГОРОД', 'РЕЙТИНГ', 'ИЗМЕНЕНИЕ', 'ЧЕКИНОВ']
df.to_csv(f'data/cities/{en_city}.csv', index=False)  # saving DF
print(f'{en_city}.csv updated!')

Обновление таблиц по расписанию

Наш дашборд будет использовать библиотеку apscheduler для вызова последней функции по расписанию и обновления таблиц по городам. Следующие строки добавим в файл application.py: scheduler будет обновлять данные для каждого города из списка all_cities ежедневно в 13:30 по МСК.

from apscheduler.schedulers.background import BackgroundScheduler
from get_tables import update_best_breweries

all_cities = sorted(['Москва', 'Сергиев Посад', 'Санкт-Петербург', 'Владимир',
             'Красная Пахра', 'Воронеж', 'Екатеринбург', 'Ярославль', 'Казань',
             'Ростов-на-Дону', 'Краснодар', 'Тула', 'Курск', 'Пермь', 'Нижний Новгород'])

scheduler = BackgroundScheduler()
@scheduler.scheduled_job('cron', hour=10, misfire_grace_time=30)
def update_data():
   for city in all_cities:
       update_best_breweries(city)
scheduler.start()

Формирование таблицы

Наконец, опишем заключительную функцию get_top_russian_breweries_table(venue_city, checkins_n=250) — она будет принимать город, количество чекинов и будет возвращать сформированную таблицу dbc. Второй параметр — checkins_n будет отсеивать пивоварни, у которых чекинов меньше значения этой переменной. Если город не указан, сразу вызываем ранее описанную get_top_russian_breweries(checkins_n) — она вернёт общую статистику за последнее время. В противном случае снова переводим города на латиницу.

if venue_city == None: 
   selected_df = get_top_russian_breweries(checkins_n)
else: 
   ru_city = venue_city
   if ru_city == 'Санкт-Петербург':
       en_city = 'Saint Petersburg'
   elif ru_city == 'Нижний Новгород':
       en_city = 'Nizhnij Novgorod'
   elif ru_city == 'Пермь':
       en_city = 'Perm'
   else:
       en_city = translator.translate(ru_city, dest='en').text

Читаем все строки из таблицы с нужным городом и проверяем количество чекинов каждой пивоварни. В самом начале материала мы завели словарь city_names. При помощи функции map() мы пишем лямбда-выражение, которое возвращает значение ключа словаря city_names только если входной аргумент из колонки df[‘ГОРОД’] совпадает с каким-либо из ключей в city_names. В случае, если совпадения не будет возвращает просто x во избежание np.Nan.

Например, для наименования «СПБ» в колонке df[‘ГОРОД’] вернётся значение «Санкт-Петербург», так как такой ключ есть в city_names. Для «Воронеж» название таким и останется, так как совпадающий ключ не найден. В конце удаляем возможные дубликаты из DataFrame, добавляем колонку с номером места пивоварни и забираем себе первые 10 строк — это и будет топ-10 пивоварен по нужному городу.

df = pd.read_csv(f'data/cities/{en_city}.csv')
df = df.loc[df['ЧЕКИНОВ'] >= checkins_n]
df['ГОРОД'] = df['ГОРОД'].map(lambda x: city_names[x] if (x in city_names) else x)
df.drop_duplicates(subset=['НАЗВАНИЕ', 'ГОРОД'], keep='first', inplace=True) 
df.insert(0, 'МЕСТО', list('🏆 ' + str(i) if i in [1, 2, 3] else str(i) for i in range(1, len(df) + 1)))
selected_df = df.head(10)

Вне зависимости от того, получали мы DataFrame общей функцией get_top_russian_breweries() или по конкретному городу, собираем таблицу, задаём стили и возвращаем готовый dbc-объект.


Вёрстка в Dash Bootstrap Components

table = dbc.Table.from_dataframe(selected_df, striped=False,
                                bordered=False, hover=True,
                                size='sm',
                                style={'background-color': '#ffffff',
                                       'font-family': 'Proxima Nova Regular',
                                       'text-align':'center',
                                       'fontSize': '12px'},
                                className='table borderless'
                                )

return table

Структура вёрстки

Опишем в application.py слайдер, таблицу и Dropdown-фильтр с выбором города.

О вёрстке дашборда при помощи Dash Bootstrap Components мы говорили в предыдущем материале цикла

checkins_slider_tab_1 = dbc.CardBody(
                           dbc.FormGroup(
                               [
                                   html.H6('Количество чекинов', style={'text-align': 'center'})),
                                   dcc.Slider(
                                       id='checkin_n_tab_1',
                                       min=0,
                                       max=250,
                                       step=25,
                                       value=250,  
                                       loading_state={'is_loading': True},
                                       marks={i: i for i in list(range(0, 251, 25))}
                                   ),
                               ],
                           ),
                           style={'max-height': '80px', 
                                  'padding-top': '25px'
                                  }
                       )

top_breweries = dbc.Card(
       [
           dbc.CardBody(
               [
                   dbc.FormGroup(
                       [
                           html.H6('Фильтр городов', style={'text-align': 'center'}),
                           dcc.Dropdown(
                               id='city_menu',
                               options=[{'label': i, 'value': i} for i in all_cities],
                               multi=False,
                               placeholder='Выберите город',
                               style={'font-family': 'Proxima Nova Regular'}
                           ),
                       ],
                   ),
                   html.P(id="tab-1-content", className="card-text"),
               ],
           ),
   ],
)

И для обновления таблицы по фильтру и слайдеру с минимальным количеством чекинов опишем callback с вызовом get_top_russian_breweries_table(city, checkin_n):

@app.callback(
   Output("tab-1-content", "children"), [Input("city_menu", "value"),
                                         Input("checkin_n_tab_1", "value")]
)
def table_content(city, checkin_n):
   return get_top_russian_breweries_table(city, checkin_n)

Готово! Напомню, в материале описан пример создания только одной таблицы. На данный момент дашборд помимо лучших пивоварен выдаёт лучшие и худшие сорта пива, а также средний рейтинг пива по регионам и отношение количества чекинов каждой пивоварни к её средней оценке.

Полный код проекта доступен на GitHub

 Нет комментариев    233   1 мес   BI-инструменты   dash   plotly   python
Ранее Ctrl + ↓