<?xml version="1.0" encoding="utf-8"?> 
<rss version="2.0">

<channel>

<title>Блог об аналитике, визуализации данных, data science и BI, заметки с тегом: dash</title>
<link>http://test.leftjoin.ru/tags/dash/</link>
<description></description>
<generator>E2 (v3365; Aegea)</generator>

<item>
<title>Обзор дашборда в Dash</title>
<guid isPermaLink="false">114</guid>
<link>http://test.leftjoin.ru/all/dash-bi-guide/</link>
<comments>http://test.leftjoin.ru/all/dash-bi-guide/</comments>
<description>
&lt;p class="note"&gt;&lt;a href="http://test.leftjoin.ru/tags/plotly/"&gt;Посмотрите&lt;/a&gt; и другие наши материалы про plotly&lt;/p&gt;
&lt;p&gt;Сегодня публикуем не совсем классический выпуск обзора BI-инструментов — потому что речь пойдёт о &lt;a href="https://dash.plotly.com/"&gt;Dash&lt;/a&gt;, фреймворке для Python от &lt;a href="https://plotly.com/python/"&gt;plotly&lt;/a&gt;. Dash — гибкий инструмент, который предоставляет набор компонентов для работы с HTML и Bootstrap для создания дашбордов с графиками plotly. Дашборд, созданный при помощи Dash — это веб-страница, написанная на Python. Любую диаграмму можно настроить, изменив передаваемые параметры прямо в коде. А работать с самими данными можно любым удобным в Python способом — например, при помощи датафреймов pandas.&lt;/p&gt;
&lt;p&gt;В новом обзоре посмотрим на работу коллбэков и фильтров в Dash, а также на реализацию таблиц и диаграмм дашборда Superstore в plotly и Dash.&lt;/p&gt;
&lt;div class="e2-text-video"&gt;
&lt;iframe src="https://www.youtube.com/embed/HYDHljU1h7w" frameborder="0" allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;p&gt;Внутри команды мы оценили дашборд и получили следующие средние оценки (1 — худшая оценка, 10 — лучшая):&lt;br /&gt;
Отвечает ли заданным вопросам — 8,83&lt;br /&gt;
Порог входа в инструмент — 4,83&lt;br /&gt;
Функциональность инструмента — 8,66&lt;br /&gt;
Удобство пользования — 7,83&lt;br /&gt;
Соответствие результата макету — 9,00&lt;br /&gt;
Визуальная составляющая — 8,16&lt;/p&gt;
&lt;p&gt;Итог: дашборд получает 8,05 баллов из 10. &lt;a href="https://superstore-dash.herokuapp.com/"&gt;Посмотрите на полученный результат&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;i&gt;Автор дашборда, член команды Valiotti Analytics — &lt;a href="https://t.me/emazurova"&gt;Елизавета Мазурова&lt;/a&gt;&lt;/i&gt;&lt;/p&gt;
</description>
<pubDate>Tue, 03 Aug 2021 13:18:50 +0300</pubDate>
</item>

<item>
<title>Деплой дашборда на виртуальной машине Amazon EC2</title>
<guid isPermaLink="false">107</guid>
<link>http://test.leftjoin.ru/all/deploy-dashboard-ec2/</link>
<comments>http://test.leftjoin.ru/all/deploy-dashboard-ec2/</comments>
<description>
&lt;p&gt;Мы уже рассказывали о том, как развернуть дашборд с помощью сервиса Elastic Beanstalk от Amazon Web Services. В этом материале расскажем как развертывать дашборды на виртуальной машине Amazon EC2.&lt;/p&gt;
&lt;h2&gt;Подготовка&lt;/h2&gt;
&lt;p&gt;Начало работы с платформой AWS и создание сервера мы описали в материале &lt;a href="http://test.leftjoin.ru/all/stavim-clickhouse-na-aws/"&gt;Устанавливаем Clickhouse на AWS&lt;/a&gt;. Проект дашборда был подготовлен в предыдущей заметке &lt;a href="http://test.leftjoin.ru/all/dashboard-python-2/"&gt;Деплой дашборда на AWS Elastic Beanstalk&lt;/a&gt;. Все файлы можно скачать из нашего репозитория на &lt;a href="https://github.com/valiotti/leftjoin/tree/master/dashboard_deployment"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Работа с терминалом&lt;/h2&gt;
&lt;p&gt;Подключитесь к вашему серверу на EC2 через терминал, используя SSH-ключ.&lt;br /&gt;
Из домашней директории копируем архив с необходимыми файлами на сервер командой scp:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;scp -i /home/user/.ssh/ssh_key.pem /home/user/brewery_dashboard.zip ubuntu@api.sample.ru:/home/ubuntu/&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Распаковываем архив с помощью команды &lt;span class="inline-code"&gt;unzip&lt;/span&gt;, указав директорию:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;unzip -d /home/ubuntu/brewery_dashboard brewery_dashboard.zip&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;После этого в каталоге появится папка &lt;span class="inline-code"&gt;/brewery_dashboard/&lt;/span&gt;, в которой среди прочих будет текстовый файл &lt;span class="inline-code"&gt;requirements.txt&lt;/span&gt;. В нем находятся все библиотеки Python, которые нужны для корректной работы дашборда. Устанавливаем их следующей командой:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;pip install -r requirements.txt&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Запускаем дашборд&lt;/h2&gt;
&lt;p&gt;Создаем сервисный файл &lt;span class="inline-code"&gt;brewery.service&lt;/span&gt; в системной папке &lt;span class="inline-code"&gt;/etc/systemd/system&lt;/span&gt;:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;sudo touch brewery.service&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;В нем прописываем всю необходимую информацию для деплоя нашего дашборда. Текстовый редактор вызывается следующей командой:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;sudo nano brewery.service&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;В WorkingDirectory указываем папку, в которой находятся файлы проекта, а в ExecStart команду для запуска:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;[Unit]
Description=Brewery Dashboard
After=network.target

[Service]
User=ubuntu
Group=www-data
WorkingDirectory=/home/ubuntu/brewery_dashboard/
ExecStart=/usr/bin/gunicorn3 --workers 3 --bind 0.0.0.0:8083 application:application&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/--2021-04-29--14.21.41.png" width="669" height="130" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Запускаем &lt;span class="inline-code"&gt;brewery.service&lt;/span&gt; следующей командой:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;sudo systemctl start brewery.service&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;И проверяем успешность запуска:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;sudo systemctl status brewery.service&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Система должна ответить, что все хорошо:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/--2021-04-29--14.24.57.png" width="991" height="257" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Теперь дашборд доступен по публичному адресу сервера с указанием порта . Можно открыть его в браузере или вставить на любой сайт с помощью тега &amp;lt;iframe&gt;:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;&amp;lt;ifrаme id='igraph' scrolling='no' style='border:none;'seamless='seamless' src='http://54.227.137.142:8083/' height='1100' width='800'&amp;gt;&amp;lt;/ifrаme&amp;gt;&lt;/code&gt;&lt;/pre&gt;</description>
<pubDate>Fri, 30 Apr 2021 14:33:38 +0300</pubDate>
</item>

<item>
<title>Создаём дашборд на Bootstrap (Часть 2)</title>
<guid isPermaLink="false">64</guid>
<link>http://test.leftjoin.ru/all/untappd-bootstrap-dashboard-part-2/</link>
<comments>http://test.leftjoin.ru/all/untappd-bootstrap-dashboard-part-2/</comments>
<description>
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/Mockup-analysts-1.png" width="2000" height="1154" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;В &lt;a href="http://test.leftjoin.ru/all/untappd-bootstrap-dashboard-part-1/"&gt;последнем материале&lt;/a&gt; мы подготовили базовый макет дашборда при помощи библиотеки dash-bootstrap-components с двумя графиками: scatter plot и российской картой, которые подробно разбирали ранее. Сегодня продолжим наполнять дашборд информацией: встроим в него таблицы и фильтр данных по пивоварням.&lt;/p&gt;
&lt;h2&gt;Получение таблиц&lt;/h2&gt;
&lt;p&gt;Сами таблицы будем описывать в макете в файле application.py, но информацию, которую они отображают лаконичнее будет получить в отдельном модуле. Создадим файл &lt;span class="inline-code"&gt;get_tables.py&lt;/span&gt;: в нём будет функция, передающая готовую таблицу класса Table библиотеки &lt;span class="inline-code"&gt;dbc&lt;/span&gt; в &lt;span class="inline-code"&gt;application.py&lt;/span&gt;. В этом материале мы опишем только таблицу лучших пивоварен России, но на GithHub будут представлены все три.&lt;/p&gt;
&lt;p&gt;В таблицах по заведениям и пивоварням мы реализуем фильтр по городам, но изначально города в собранных с Untappd данных записаны на латинице. Для запросов мы будем переводить русскоязычные наименования городов на английский при помощи библиотеки Google Translate. Кроме того, одни и те же города могут называться по-разному — например, «Москва» на латинице где-то записана как «Moskva», а где-то как «Moscow». Для этого дополнительно настроим маппинг наименований города и заранее создадим словарь с корректными наименованиями основных городов. Он пригодится в самом конце.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;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': 'Нижний Новгород',
}&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Таблица лучших пивоварен&lt;/h2&gt;
&lt;p&gt;Таблица, о которой идёт речь в материале, будет показывать топ-10 лучших российских пивоварен с изменением рейтинга. То есть мы сравниваем данные за два периода: [30 дней назад; сегодня] и [60 дней назад; 30 дней назад] и смотрим, как менялось место пивоварни в рейтинге. Соответственно, мы опишем следующие колонки: место в рейтинге, название пивоварни, ассортимент сортов пива, рейтинг пивоварни на untappd, изменение места и количество чекинов у этой пивоварни.&lt;br /&gt;
Опишем функцию &lt;span class="inline-code"&gt;get_top_russian_breweries&lt;/span&gt;, которая отправляет запрос к Clickhouse, получает общий топ пивоварен России, формирует данные и возвращает готовый для вывода DataFrame. Отправим два запроса — топ пивоварен за последние 30 дней и топ пивоварен за предыдущие 30 дней. Следующий запрос будет отбирать лучшие пивоварни, основываясь на количестве отзывов о пиве данной пивоварни.&lt;/p&gt;
&lt;p&gt;&lt;details&gt;&lt;br /&gt;
&lt;summary&gt;&lt;span style="color:#7ea9b8"&gt;Забираем данные из базы&lt;/span&gt;&lt;/summary&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;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 &amp;gt;= (today() - 30)) AND (venue_country = 'Россия') 
          GROUP BY           
              brewery_id,
              brewery_name) rt
      WHERE (checkins&amp;gt;={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 &amp;gt;= (today() - 60) AND created_at &amp;lt;= (today() - 30)) AND (venue_country = 'Россия')
      GROUP BY           
          brewery_id,
          brewery_name) rt
  WHERE (checkins&amp;gt;={checkins_n})
  ORDER BY avg_rating DESC
  LIMIT 10
  '''
)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;/details&gt;&lt;/p&gt;
&lt;p&gt;Формируем из полученных строк два DataFrame:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;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))&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;А затем в итераторе считаем, как изменилось место за последнее время у пивоварни. Обработаем исключение на случай, если 60 дней назад этой пивоварни в нашей базе ещё не было.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;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&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Теперь пройдёмся по полученным колонкам с текущими местами и изменениями. Если они не пустые, то при положительном изменении добавим к записи стрелочку вверх. При отрицательном — стрелочку вниз.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;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 &amp;gt; 0:
           diff_rank_list.append(f'↑ +{difference}')
       elif difference &amp;lt; 0:
           diff_rank_list.append(f'↓ {difference}')
       else:
           diff_rank_list.append('–')
   else:
       diff_rank_list.append(rank_was)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Наконец, разметим итоговый DataFrame и вставим в него колонку с текущим местом. При этом у топ-3 будет отображаться эмодзи с золотым кубком.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;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&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;#Выбор пивоварен с фильтром по городам&lt;br /&gt;
Одна из функций нашего дашборда — просмотр топа пивоварен по конкретному городу. Для корректной работы напишем скрипт, который для каждого из списка российских городов получает топ пивоварен по числу чекинов и записывает данные по каждому городу в отдельные csv-файлы. В сущности, он мало чем отличается от предыдущего — рассмотрим главные отличия.&lt;/p&gt;
&lt;p&gt;Прежде всего, функция принимает конкретный город. Мы уже отметили, что города в базе данных записаны на латинице — поэтому сначала переводим наименование города. В случае с Санкт-Петербургом, Нижним Новгородом и Пермью придётся перевести вручную: например, Санкт-Петербург переводится в Google Translate как St. Petersburg вместо ожидаемого Saint Petersburg.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;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&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Следующее отличие — запрос к базе. Нам нужно добавить в него условие совпадения по городу, чтобы получать чекины только в запрошенном городе:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;WHERE (rt.venue_city='{ru_city}' OR rt.venue_city='{en_city}')&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Наконец, сформированный DataFrame мы не возвращаем, а сохраняем в директорию &lt;span class="inline-code"&gt;data/cities&lt;/span&gt;.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;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!')&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Обновление таблиц по расписанию&lt;/h2&gt;
&lt;p&gt;Наш дашборд будет использовать библиотеку &lt;span class="inline-code"&gt;apscheduler&lt;/span&gt; для вызова последней функции по расписанию и обновления таблиц по городам. Следующие строки добавим в файл &lt;span class="inline-code"&gt;application.py&lt;/span&gt;: scheduler будет обновлять данные для каждого города из списка &lt;span class="inline-code"&gt;all_cities&lt;/span&gt; ежедневно в 13:30 по МСК.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;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()&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Формирование таблицы&lt;/h2&gt;
&lt;p&gt;Наконец, опишем заключительную функцию &lt;span class="inline-code"&gt;get_top_russian_breweries_table(venue_city, checkins_n=250)&lt;/span&gt; — она будет принимать город, количество чекинов и будет возвращать сформированную таблицу &lt;span class="inline-code"&gt;dbc&lt;/span&gt;. Второй параметр — &lt;span class="inline-code"&gt;checkins_n&lt;/span&gt; будет отсеивать пивоварни, у которых чекинов меньше значения этой переменной. Если город не указан, сразу вызываем ранее описанную &lt;span class="inline-code"&gt;get_top_russian_breweries(checkins_n)&lt;/span&gt; — она вернёт общую статистику за последнее время. В противном случае снова переводим города на латиницу.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;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&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Читаем все строки из таблицы с нужным городом и проверяем количество чекинов каждой пивоварни. В самом начале материала мы завели словарь &lt;span class="inline-code"&gt;city_names&lt;/span&gt;. При помощи функции &lt;span class="inline-code"&gt;map()&lt;/span&gt; мы пишем лямбда-выражение, которое возвращает значение ключа словаря &lt;span class="inline-code"&gt;city_names&lt;/span&gt; только если входной аргумент из колонки &lt;span class="inline-code"&gt;df[‘ГОРОД’]&lt;/span&gt; совпадает с каким-либо из ключей в &lt;span class="inline-code"&gt;city_names&lt;/span&gt;. В случае, если совпадения не будет возвращает просто &lt;span class="inline-code"&gt;x&lt;/span&gt; во избежание &lt;span class="inline-code"&gt;np.Nan&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;Например, для наименования «СПБ» в колонке &lt;span class="inline-code"&gt;df[‘ГОРОД’]&lt;/span&gt; вернётся значение «Санкт-Петербург», так как такой ключ есть в &lt;span class="inline-code"&gt;city_names&lt;/span&gt;. Для «Воронеж» название таким и останется, так как совпадающий ключ не найден. В конце удаляем возможные дубликаты из DataFrame, добавляем колонку с номером места пивоварни и забираем себе первые 10 строк — это и будет топ-10 пивоварен по нужному городу.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;df = pd.read_csv(f'data/cities/{en_city}.csv')
df = df.loc[df['ЧЕКИНОВ'] &amp;gt;= 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)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Вне зависимости от того, получали мы DataFrame общей функцией &lt;span class="inline-code"&gt;get_top_russian_breweries()&lt;/span&gt; или по конкретному городу, собираем таблицу, задаём стили и возвращаем готовый dbc-объект.&lt;/p&gt;
&lt;p&gt;&lt;details&gt;&lt;br /&gt;
&lt;summary&gt;&lt;span style="color:#7ea9b8"&gt;Вёрстка в Dash Bootstrap Components&lt;/span&gt;&lt;/summary&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;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&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Структура вёрстки&lt;/h2&gt;
&lt;p&gt;Опишем в &lt;span class="inline-code"&gt;application.py&lt;/span&gt; слайдер, таблицу и Dropdown-фильтр с выбором города.&lt;/p&gt;
&lt;p class="note"&gt;О вёрстке дашборда при помощи Dash Bootstrap Components мы говорили в &lt;a href="http://test.leftjoin.ru/all/untappd-bootstrap-dashboard-part-1/"&gt;предыдущем материале цикла&lt;/a&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;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=&amp;quot;tab-1-content&amp;quot;, className=&amp;quot;card-text&amp;quot;),
               ],
           ),
   ],
)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;/details&gt;&lt;/p&gt;
&lt;p&gt;И для обновления таблицы по фильтру и слайдеру с минимальным количеством чекинов опишем callback с вызовом &lt;span class="inline-code"&gt;get_top_russian_breweries_table(city, checkin_n)&lt;/span&gt;:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;@app.callback(
   Output(&amp;quot;tab-1-content&amp;quot;, &amp;quot;children&amp;quot;), [Input(&amp;quot;city_menu&amp;quot;, &amp;quot;value&amp;quot;),
                                         Input(&amp;quot;checkin_n_tab_1&amp;quot;, &amp;quot;value&amp;quot;)]
)
def table_content(city, checkin_n):
   return get_top_russian_breweries_table(city, checkin_n)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Готово! Напомню, в материале описан пример создания только одной таблицы. На данный момент дашборд помимо лучших пивоварен выдаёт лучшие и худшие сорта пива, а также средний рейтинг пива по регионам и отношение количества чекинов каждой пивоварни к её средней оценке.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;a href="http://rutappd.ru" class="e2-text-picture-link"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/1-15.png" width="1424" height="819" alt="" /&gt;
&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;&lt;i&gt;Полный код проекта доступен на &lt;a href="https://github.com/valiotti/leftjoin/tree/master/rutappd"&gt;GitHub&lt;/a&gt;&lt;/i&gt;&lt;/p&gt;
</description>
<pubDate>Wed, 23 Sep 2020 17:03:30 +0300</pubDate>
</item>

<item>
<title>Дашборды умерли</title>
<guid isPermaLink="false">58</guid>
<link>http://test.leftjoin.ru/all/dashbords-are-dead/</link>
<comments>http://test.leftjoin.ru/all/dashbords-are-dead/</comments>
<description>
&lt;p&gt;&lt;i&gt;Перевод статьи &lt;a href="https://towardsdatascience.com/dashboards-are-dead-b9f12eeb2ad2" class="nu"&gt;«&lt;u&gt;Dashboards are Dead&lt;/u&gt;»&lt;/a&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p&gt;Дашборды были главным оружием распространения данных в течение последних нескольких десятилетий, но мир не стоит на месте. Чтобы восприятие данных стало доступнее, нам следует переосмыслить текущие инструменты, а ответ может оказаться ближе, чем нам кажется.&lt;/p&gt;
&lt;h2&gt;Hello Dashboard, my old friend&lt;/h2&gt;
&lt;p&gt;На старте карьеры я работала в крупной технологической компании. Компания только-только приобрела свой первый инструмент для создания дашбордов, и наша команда отвечала за захватывающий переход от устаревших spreadsheets и отчетов SSRS к новым ярким дашбордам.&lt;/p&gt;
&lt;p&gt;Переход от spreadsheets к дашбордам стал значительным шагом в нашем росте как аналитиков. Продуманный дизайн и интерактивность дашбордов резко снизили «стоимость доступа» к данным. Представьте, вы прогуливаетесь по офису и видите сотрудников любой должности и любого опыта, которые возятся с дашбордами. Это рай для любителей данных, правда?&lt;/p&gt;
&lt;p&gt;Не совсем. Вскоре мы обнаружили, что дашборды приносят с собой ряд новых проблем:&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;Как? У вас ещё нет дашборда?! Неожиданно повсюду появились дашборды. Инженеру нужны данные для специального анализа? Вот дашборд. У вице-президента будет презентация на следующей неделе и ему нужны диаграммы? Она получает дашборд. А что происходит дальше? О нём просто забывают. Такой шаблонный подход истощал время, ресурсы и мотивацию нашей команды. Это уникальное деморализующее чувство — наблюдать, как ещё один из ваших дашбордов забросили быстрее, чем профиль MySpace в 2008 году.&lt;/li&gt;
&lt;li&gt;Смерть от 1000 фильтров. После того, как новый дашборд заработал, нас сразу же заваливали запросами на новые представления, фильтры, поля, страницы (напомните мне рассказать вам о том, как я увидела 67-страничный дашборд). Было ясно: дашборды не отвечали на все вопросы, что было либо неудачей на этапе разработки, либо неспособностью инструментов дать ответы, в которых нуждались люди. Что ещё хуже, мы выяснили, что люди использовали все эти фильтры, чтобы экспортировать данные в Excel и уже там работать с ними 🤦‍♀️&lt;/li&gt;
&lt;li&gt;Не мой дашборд. Постепенно шумиха вокруг дашбордов начала сходить на нет, люди начали пренебрегать ими и откровенно игнорировать их. Многие видели в них угрозу для своей работы, и если они встречали неожиданные цифры, то списывали всё на «плохие данные». У нас на работе были серьёзные проблемы с доверием между людьми, и дашборды только усугубляли положение. В конце концов, мы ведь не могли отправлять другим наши SQL-запросы для получения данных: люди бы просто не смогли не только прочитать их, но даже понять ту сложную схему, по которой они работают. И тем более мы не могли отправлять другим командам необработанные данные. Итак, у нас была просто огромная, наболевшая, серьезная проблема с доверием.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Реальный пример: что это за странная красная точка на карте?&lt;/h2&gt;
&lt;p&gt;Для примера давайте рассмотрим дашборд, который стал широко популярен во время пандемии — &lt;a href="https://www.google.com/url?q=https://coronavirus.jhu.edu/map.html&amp;sa=D&amp;ust=1598616752822000&amp;usg=AFQjCNFRUzw5bve2HdMAY72otVE6txTUTw"&gt;панель мониторинга коронавируса университета Джона Хопкинса&lt;/a&gt;.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/1-11.png" width="700" height="319" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Дашборд привлекателен визуально. Красный и чёрный вызывают чувство строгости и важности с первого взгляда. По мере того, как взгляд останавливается на странице, мы сталкиваемся с числами, точками разного размера и графиками, которые почти всегда направлены вправо-вверх. У нас осталось ощущение, что всё плохо, и, кажется, становится ещё хуже. Этот дашборд был создан с целью получения данных доступным и интересным способом. Возможно, он даже был разработан, чтобы ответить на несколько ключевых вопросов: «Сколько новых случаев было сегодня в моей стране? А в моём регионе?». Безусловно, это намного лучше, чем если бы они просто разместили таблицу или ссылку для скачивания.&lt;/p&gt;
&lt;p&gt;Но кроме этих поверхностных выводов мы не можем сделать с данными ничего. Если бы мы хотели использовать данные для определенной цели, у нас не было бы необходимого контекста вокруг этих цифр, чтобы сделать их полезными и доверять как своим собственным. Например, «Когда в моей стране или в моём регионе начали действовать меры социального дистанцирования? Насколько доступны тесты в моей стране?». И даже если бы нам каким-то образом удалось получить этот контекст, чтобы доверять этим числам самому дашборду не хватает гибкости для проведения самостоятельного анализа.&lt;/p&gt;
&lt;p&gt;Как и в моём опыте работы в компании, имя которой я не называю, этот дашборд позволяет людям делать что-то с данными, но вовсе не что-то значимое. В указанной неназванной компании мы пытались решать эту проблему, добавляя всё больше и больше дашбордов, а затем добавляя всё больше и больше фильтров к этим дашбордам, а затем убивая эти дашборды, когда они становились бесполезными. Эта отрицательная обратная связь способствовала серьёзному недоверию к данным и межгрупповым расколам, многие из которых, как я полагаю, всё ещё существуют, если верить пассивно-агрессивным обновлениям статусов на LinkedIn.&lt;/p&gt;
&lt;p&gt;Дашборды расширили возможности обработки данных, но они определенно не являются оптимальным интерфейсом для совместной работы с данными и создания отчётов. К счастью, есть претендент, который вы, возможно, уже используете...&lt;/p&gt;
&lt;h2&gt;Данные в портретном режиме&lt;/h2&gt;
&lt;p&gt;Блокноты с данными, такие как Jupyter, стали очень популярными за последние несколько лет в области Data Science. Их технологическая направленность оказалась лучше традиционных скриптовых инструментов для Data Analysis и Data Science. Это не только полезно для аналитика, выполняющего работу, но также помогает начальнику, коллеге, другу, который вынужден этим пользоваться.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/2-11.png" width="700" height="310" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;По сути, блокноты обеспечивают:&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;Доверие процессу, потому что пользователи буквально видят код и комментарии автора&lt;/li&gt;
&lt;li&gt;Возможность ответить на любой вопрос, при условии, что пользователь знает язык, на котором написан код&lt;/li&gt;
&lt;li&gt;Сотрудничество между группами и представление решений с более широкой аудиторией&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Я, конечно, не первая, кто хочет применить мощь и гибкость блокнотов в области анализа данных или бизнес-аналитики, и мы поговорили с рядом компаний, которые используют их вместо дашбордов. Некоторые используют только Jupyter для своих отчётов, другие вырезают и вставляют диаграммы оттуда в текстовый редактор для аналогичного эффекта. Это не совершенные решения, но это признак того, что компании готовы отказаться от тщательно продуманных дашбордов, чтобы попробовать преимущества блокнотов.&lt;/p&gt;
&lt;p&gt;Нам просто нужен способ вынести эту идею за пределы Data Science и сделать блокнот таким же доступным, как и дашборды.&lt;/p&gt;
&lt;h2&gt;Блокноты в массы&lt;/h2&gt;
&lt;p&gt;В Count мы настолько верим в преимущества блокнотов, что создали платформу для анализа данных на их основе. Народ, больше никаких дашбордов!&lt;/p&gt;
&lt;p&gt;Чтобы использовать их за пределами Data Science, нам пришлось создать собственную версию, но фундаментальные принципы всё ещё применимы с некоторыми дополнительными преимуществами...&lt;/p&gt;
&lt;h2&gt;Создан для любого уровня опыта&lt;/h2&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/3-10.png" width="700" height="378" alt="" /&gt;
&lt;/div&gt;
&lt;ol start="1"&gt;
&lt;li&gt;Нет необходимости учить всех в вашей команде Python или SQL, поскольку запросы можно создавать по принципу drag-and-drop, используя «составной запрос» SQL или написания запроса с нуля.&lt;/li&gt;
&lt;li&gt;Стройте графики и диаграммы одним щелчком мыши, без сложных пакетов визуализации или программного обеспечения&lt;/li&gt;
&lt;li&gt;Автоматическое объединение таблиц и результатов запроса, нет необходимости писать сложные объединения или пытаться объяснить схему&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Collaboration-enabled&lt;/h2&gt;
&lt;ol start="1"&gt;
&lt;li&gt;Делитесь блокнотами с товарищем по команде, всей командой или тем, у кого есть ссылка&lt;/li&gt;
&lt;li&gt;Добавляйте комментарии и выноски, чтобы сделать документ действительно общим&lt;/li&gt;
&lt;/ol&gt;
&lt;iframe src="https://vk.com/video_ext.php?oid=178098251&amp;id=456239625&amp;hash=d4c30b43d6b4e39f" width="640" height="360" frameborder="0" allowfullscreen&gt;&lt;/iframe&gt;
&lt;p&gt;Взяв лучшее от блокнотов, Count обеспечивает мощность, прозрачность и взаимодействие, необходимое командам, чтобы не просто сообщать людям цифры, а давать возможность получать нужную информацию и делиться ею с остальной частью компании. В процессе создания Count мы работали с рядом организаций, чтобы посмотреть, как блокноты меняют способ взаимодействия с данными в команде. Вот, что мы обнаружили:&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;Аналитики используют блокноты вместо SQL-скриптов для создания нескольких базовых таблиц, которые используют другие команды. Эти блокноты доступны для просмотра всем, что решает проблему доверия в команде&lt;/li&gt;
&lt;li&gt;Команда по работе с данными создаёт несколько базовых отчётов. Эти отчёты полны комментариев, которые помогут читателю лучше понять, как интерпретировать числа и какие соображения следует принять&lt;/li&gt;
&lt;li&gt;Затем пользователи делают fork этих дата-блокнотов или создают свои собственные. Они делятся этими блокнотами с Data Team, чтобы они могли помочь им, а затем и с другими подразделениями компании&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Поскольку всё используется всеми и находится в одном месте, проблемы с доверием начинают решаться. В результате вы не строите дашборды для людей, которые их не используют, не создаются тысячи фильтров для удовлетворения любых потребностей, поскольку у людей больше возможностей для создания полноценных отчётов, которые им действительно нужны. Небольшой переход от дашборда к блокноту может существенно повлиять на то, как ваша команда использует данные.&lt;/p&gt;
</description>
<pubDate>Fri, 28 Aug 2020 15:06:46 +0300</pubDate>
</item>

<item>
<title>Создаём дашборд на Bootstrap с нуля (Часть 1)</title>
<guid isPermaLink="false">55</guid>
<link>http://test.leftjoin.ru/all/untappd-bootstrap-dashboard-part-1/</link>
<comments>http://test.leftjoin.ru/all/untappd-bootstrap-dashboard-part-1/</comments>
<description>
&lt;p&gt;В прошлых материалах мы познакомились с фреймворком Plotly Dash, &lt;a href="http://test.leftjoin.ru/all/scatter-plot-untappd"&gt;научились строить scatter plots&lt;/a&gt; и &lt;a href="http://test.leftjoin.ru/all/plotly-russian-map/"&gt;визуализировать данные на карте&lt;/a&gt;. Сегодня мы попробуем объединить имеющиеся части в одном веб-приложении и расскажем как можно создать полноценный дашборд используя сетчатую структуру Bootstrap.&lt;br /&gt;
В этом нам поможет &lt;a href="https://dash-bootstrap-components.opensource.faculty.ai/"&gt;dash-bootstrap-components&lt;/a&gt;, эта библиотека позволяет строить дашборды по принципу  “plug-and-play”,  добавлять любые компоненты Bootstrap и стилизовать их используя грид-дизайн.&lt;/p&gt;
&lt;h2&gt;Подготовка макета&lt;/h2&gt;
&lt;p&gt;Перед созданием любого приложения, не только дашборда нам просто необходим план, черновой макет, следуя которому, мы бы видели общую картину и могли быстро внести корректировки в структуру. Для создания макета дашборда мы использовали приложение &lt;a href="https://app.diagrams.net/"&gt;draw.io&lt;/a&gt;, этот инструмент позволяет быстро создавать диаграммы, графики, блок-схемы, формы и т. д. Наш дашборд будет построен по следующему макету:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/1-10.png" width="927" height="1045" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Как и сам дашборд, шапка будет оформлена в главных цветах &lt;a href="https://untappd.com/"&gt;Untappd&lt;/a&gt; — белом и золотом. Ниже расположится раздел с пивоварнями, состоящий из scatter plot и панели с настройками. А в самом низу будет карта, показывающая средний рейтинг напитка по регионам России.&lt;/p&gt;
&lt;p&gt;Итак, приступим, для начала создадим файл application.py, в нем будут храниться все фронтенд элементы дашборда, и папку assets в той же директории, структура должна быть такой:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;- application.py
- assets/
    |-- typography.css
    |-- header.css
    |-- custom-script.js
    |-- image.png&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Импортируем нужные библиотеки и инициализируем приложение:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;import dash
import dash_bootstrap_components as dbc
import dash_html_components as html
import dash_core_components as dcc
import pandas as pd
from get_ratio_scatter_plot import get_plot
from get_russian_map import get_map
from clickhouse_driver import Client
from dash.dependencies import Input, Output

standard_BS = dbc.themes.BOOTSTRAP
app = dash.Dash(__name__, external_stylesheets=[standard_BS])&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Главные аргументы для app:&lt;br /&gt;
&lt;span class="inline-code"&gt;__name__&lt;/span&gt; — для активации cтатических элементов из папки assets (картинки, CSS и JS файлы)&lt;br /&gt;
&lt;span class="inline-code"&gt;external_stylesheets&lt;/span&gt; — внешний CSS для стилизации дашборда, здесь мы используем стандартный BootstrapCDN, однако вы можете создать свой или &lt;a href="https://www.bootstrapcdn.com/bootswatch/"&gt;воспользоваться одним из уже готовых стилей&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Включаем ещё  несколько параметров для работы с локальными файлами и подключаемся к ClickHouse:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;app.scripts.config.serve_locally = True
app.css.config.serve_locally = True

client = Client(host='',
                user='default',
                password='',
                port='9000',
                database='default')&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Добавим палитру цветов:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;colors = ['#ffcc00', 
          '#f5f2e8', 
          '#f8f3e3',
          '#ffffff', 
          ]&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Верстка макета&lt;/h2&gt;
&lt;p&gt;Все элементы дашборда будут помещены в Bootstrap Контейнер, который находится в блоке &amp;lt;div&amp;gt;:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;- app 
    |-- div
     |-- container
      |-- logo&amp;amp;header
     |-- container
      |-- div
       |-- controls&amp;amp;scatter
       |-- map&lt;/code&gt;&lt;/pre&gt;&lt;pre class="e2-text-code"&gt;&lt;code&gt;app.layout = html.Div(
                    [
                        dbc.Container(

                                         &amp;lt; шапка &amp;gt;
                         
                        dbc.Container(       
                            html.Div(
                                [
                        
                                    &amp;lt; основной код &amp;gt;
                        
                                ],
                            ),
                            fluid=False, style={'max-width': '1300px'},
                        ),
                    ],
                    style={'background-color': colors[1], 'font-family': 'Proxima Nova Bold'},
                )&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Здесь мы сразу задаем фиксированную ширину контейнера, цвет фона и стиль шрифта на странице, который берется из typography.css в папке assets. Стоит подробней остановится на первом элементе блока div, это и есть заголовок страницы, он включает в себя логотип Untappd:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;logo = html.Img(src=app.get_asset_url('logo.png'),
                        style={'width': &amp;quot;128px&amp;quot;, 'height': &amp;quot;128px&amp;quot;,
                        }, className='inline-image')&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;и главный заголовок:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;header = html.H3(&amp;quot;Статистика российских пивоварен в Untappd&amp;quot;, style={'text-transform': &amp;quot;uppercase&amp;quot;})&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Чтобы расположить эти два элемента на одной строке мы воспользовались Bootstrap Формами:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;logo_and_header = dbc.FormGroup(
        [
            logo,
            html.Div(
                [
                    header
                ],
                className=&amp;quot;p-5&amp;quot;
            )
        ],
        className='form-row',
)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;В блоке html.Div параметр ’p-5’ позволяет добавить отступы и вертикально выровнять заголовок,  а ’form-row’ поставить logo и header в один ряд. На данном этапе шапка дашборда вылгядит следующим образом:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/2-10.png" width="1347" height="128" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Нам осталось выровнять их по центру и добавить красок. Для этого создаем отдельный контейнер, который будет состоять из одного ряда. В параметре &lt;span class="inline-code"&gt;className&lt;/span&gt; указываем &lt;span class="inline-code"&gt;’d-flex justify-content-center’&lt;/span&gt;, чтобы выровнять элементы контейнера по центру.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;dbc.Container(
                    dbc.Row(
                        [
                            dbc.Col(
                                html.Div(
                                    logo_and_header,
                                ),
                            ),
                        ],
                        style={'max-height': '128px',
                               'color': 'white',
                       }

                    ),
                    className='d-flex justify-content-center',
                    style={'max-width': '100%',
                           'background-color': colors[0]},
                ),&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;На данном этапе шапка дашборда готова:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/3-9.png" width="1343" height="118" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Далее в следующий Bootstrap Контейнер добавим первый подзаголовок:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;dbc.Container(
                    html.Div(
                        [
                            html.Br(),
                            html.H5(&amp;quot;Пивоварни&amp;quot;, style={'text-align':'center', 'text-transform': 'uppercase'}),
                            html.Hr(), # разделительная линия&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Сам дашборд будет состоять из Bootstrap Карт, это обеспечит структурированное расположение всех элементов, придаст каждому элементу четкие границы и сохранит white space. Давайте создадим один из элементов дашборда, панель с настройками:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;slider_day_values = [1, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
slider_top_breweries_values = [5, 25, 50, 75, 100, 125, 150, 175, 200]

controls = dbc.Card(
    [
       dbc.CardBody(
           [
               dbc.FormGroup(
                    [
                        dbc.Label(&amp;quot;Временной период&amp;quot;, style={'text-align': 'center', 'font-size': '100%', 'text-transform': 'uppercase'}),
                        dcc.Slider(
                            id='slider-day',
                            min=1,
                            max=100,
                            step=10,
                            value=100,
                            marks={i: i for i in slider_day_values}
                        ),
                    ], style={'text-align': 'center'}
               ),
               dbc.FormGroup(
                    [
                        dbc.Label(&amp;quot;Количество пивоварен&amp;quot;, style={'text-align': 'center', 'font-size': '100%', 'text-transform': 'uppercase'}),
                        dcc.Slider(
                            id='slider-top-breweries',
                            min=5,
                            max=200,
                            step=5,
                            value=200,
                            marks={i: i for i in slider_top_breweries_values}
                        ),
                    ], style={'text-align': 'center'}
               ),
           ],
       )
    ],
    style={'height': '32.7rem', 'background-color': colors[3]}
)&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/4-5.png" width="357" height="179" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Панель включает два слайдера для управления scatter plot, они располагаются друг под другом в Bootstrap формах. Мы добавили слайдеры в один блок —  dbc.CardBody. остальные эдементы будут добавлены по такому же принципу, это позволяет поставить одинаковые  отступы со всех сторон . По умолчанию слайдеры оформлены в голубом цвете, для того чтобы изменить их стиль, воспользуйтесь файлом sliders.css, находящемся в папке assets.&lt;br /&gt;
Добавляем панель управления и scatter plot следующим блоком:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;dbc.Row(
                [
                    dbc.Col(controls, width={&amp;quot;size&amp;quot;: 4,
                                     &amp;quot;order&amp;quot;: 'first',
                                             &amp;quot;offset&amp;quot;: 0},
                     ),
                     dbc.Col(dbc.Card(
                                [
                                    dbc.CardBody(
                                        [
                                            html.H6(&amp;quot;Отношение количества отзывов к средней оценке пивоварни&amp;quot;,
                                                    className=&amp;quot;card-title&amp;quot;,
                                                    style={'text-transform': 'uppercase'}), 
                                            dcc.Graph(id='ratio-scatter-plot'),
                                        ],
                                    ),
                                ],
                                style={'background-color': colors[2], 'text-align':'center'}
                             ),
                     md=8),
                ],
                align=&amp;quot;start&amp;quot;,
                justify='center',
            ),
html.Br(),&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;И в конце страницы расположим карту:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;html.H5(&amp;quot;Заведения и регионы&amp;quot;, style={'text-align':'center', 'text-transform': 'uppercase',}),
                            html.Hr(), #разделительная линия
                            dbc.Row(
                                [
                                    dbc.Col(
                                        dbc.Card(
                                            [
                                                dbc.CardBody(
                                                    [
                                                        html.H6(&amp;quot;Средний рейтинг пива по регионам&amp;quot;,
                                                                className=&amp;quot;card-title&amp;quot;,
                                                                style={'text-transform': 'uppercase'},
                                                        ),  
                                                        dcc.Graph(figure=get_map())
                                                    ],
                                                ),
                                            ],
                                        style={'background-color': colors[2], 'text-align': 'center'}
                                        ),
                                md=12),
                                ]
                            ),
                            html.Br(),&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Callback-функции в Dash&lt;/h2&gt;
&lt;p&gt;Callback-функции позволяют сделать элементы дашборда интерактивными, если меняется входной элемент (Input), то и выход (Output) тоже изменится.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;@app.callback(
    Output('ratio-scatter-plot', 'figure'),
    [Input('slider-day', 'value'),
     Input('slider-top-breweries', 'value'),
     ]
)
def get_scatter_plots(n_days=100, top_n=200):
    if n_days == 100 and top_n == 200:
        df = pd.read_csv('data/ratio_scatter_plot.csv')
        return get_plot(n_days, top_n, df)
    else:
        return get_plot(n_days, top_n)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Входные/ выходные  (Input/Output) значения это, проще говоря, параметр value элемента с определенным id. Например, входное значение верхнего слайдера с id=’slider-day’, показывающего временной период по умолчанию 100.  При изменении этого значения функция, обернутая в декоратор будет вызвана автоматически и output на графике обновится. Больше примеров представлено &lt;a href="https://dash.plotly.com/basic-callbacks/на"&gt;сайте plotly&lt;/a&gt;.&lt;br /&gt;
Важный момент, чтобы scatter plot при загрузке страницы отображал данные нам нужно сперва считать их из сохраненного датафрейма в папке data (указать начальное состояние), иначе scatter plot будет пустым. В дальнейшем при изменении параметров слайдера данные будут загружаться из ClickHouse таблиц.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;div class="fotorama" data-width="831" data-ratio="1.6011560693642"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/5-7.png" width="831" height="519" alt="" /&gt;
&lt;img src="http://test.leftjoin.ru/pictures/6-6.png" width="831" height="519" alt="" /&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Далее добавим пару строк отвечающих за поднятие локального сервера и приложение готово к запуску:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;application = app.server

if __name__ == '__main__':
    application.run(debug=True, port=8000)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Теперь осталось только &lt;a href="http://test.leftjoin.ru/all/dashboard-python-2/"&gt;загрузить его на AWS с помощью BeansTalk&lt;/a&gt; и наш &lt;a href="http://unappd-part-1-ru.us-east-2.elasticbeanstalk.com/"&gt;дашборд на Bootstrap готов&lt;/a&gt;:&lt;/p&gt;
&lt;div class="embed-responsive embed-responsive-4by3" style="min-width:500"&gt;&lt;p&gt;&lt;iframe id="igraph" scrolling="yes" style="border:none;"seamless="seamless" src='http://unappd-part-1-ru.us-east-2.elasticbeanstalk.com/' height="1360px" width="1100px"&lt;/p&gt;
&lt;/iframe&gt;&lt;/div&gt;&lt;p&gt;Это была первая часть нашего выпуска, в следующей мы добавим больше новых bootstrap компонентов, callback-ов и расскажем про стилизацию таблиц.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/valiotti/leftjoin/tree/master/untappd_dashboard%20(part%201)/"&gt;Полный код проекта на GitHub&lt;/a&gt;&lt;/p&gt;
</description>
<pubDate>Thu, 13 Aug 2020 10:20:03 +0300</pubDate>
</item>

<item>
<title>Визуализация данных на российской карте библиотекой Plotly</title>
<guid isPermaLink="false">54</guid>
<link>http://test.leftjoin.ru/all/plotly-russian-map/</link>
<comments>http://test.leftjoin.ru/all/plotly-russian-map/</comments>
<description>
&lt;p&gt;Часто для визуализации данных подходит карта: например, когда нужно показать, как статистика ведёт себя в определённых городах, областях, регионах. В таких случаях каждый регион или другая административная единица кодируется: ее границы преобразуют в полигоны и мультиполигоны с координатами широты и долготы относительно карты мира. Для Америки и Европы не составит труда найти встроенное в библиотеку Plotly решение, но в случае с картой России такого реализованного решения сходу найти не удалось. Сегодня мы разметим готовый geojson файл с административными границами регионов России, напишем парсер последних данных по коронавирусу и визуализируем статистику на карте при помощи библиотеки Plotly.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;from urllib.request import urlopen
import json
import requests
import pandas as pd
from selenium import webdriver
from bs4 import BeautifulSoup as bs
import plotly.graph_objects as go&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Правим geojson&lt;/h2&gt;
&lt;p&gt;Скачаем открытый geojson с границами российских регионов, найденный по одной из первых ссылок в Google по запросу «russia geojson». В нём уже есть кое-какие данные: например, наименования регионов. Но этот geojson-файл пока ещё не подходит под требуемый формат Plotly: в нём не размечены идентификаторы регионов.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;with urlopen('https://raw.githubusercontent.com/codeforamerica/click_that_hood/master/public/data/russia.geojson') as response:
    counties = json.load(response)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Кроме разметки идентификаторов есть различия в наименовании регионов. Например, на сайте стопкоронавирус.рф, откуда мы будем брать свежие данные о заболевших, республика Башкортостан занесена как «Республика Башкортостан», а в нашем geojson-файле — просто «Башкортостан». Все эти различия необходимо устранить во избежание конфликтов. Кроме того, все первые буквы в названиях регионов должны начинаться с верхнего регистра.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;regions_republic_1 = ['Бурятия', 'Тыва', 'Адыгея', 'Татарстан', 'Марий Эл',
                      'Чувашия', 'Северная Осетия – Алания', 'Алтай',
                      'Дагестан', 'Ингушетия', 'Башкортостан']
regions_republic_2 = ['Удмуртская республика', 'Кабардино-Балкарская республика',
                      'Карачаево-Черкесская республика', 'Чеченская республика']
for k in range(len(counties['features'])):
    counties['features'][k]['id'] = k
    if counties['features'][k]['properties']['name'] in regions_republic_1:
        counties['features'][k]['properties']['name'] = 'Республика ' + counties['features'][k]['properties']['name']
    elif counties['features'][k]['properties']['name'] == 'Ханты-Мансийский автономный округ - Югра':
        counties['features'][k]['properties']['name'] = 'Ханты-Мансийский АО'
    elif counties['features'][k]['properties']['name'] in regions_republic_2:
        counties['features'][k]['properties']['name'] = counties['features'][k]['properties']['name'].title()&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Из получившегося geojson-файла сформируем DataFrame с регионами России: возьмём идентификаторы и наименования.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;region_id_list = []
regions_list = []
for k in range(len(counties['features'])):
    region_id_list.append(counties['features'][k]['id'])
    regions_list.append(counties['features'][k]['properties']['name'])
df_regions = pd.DataFrame()
df_regions['region_id'] = region_id_list
df_regions['region_name'] = regions_list&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Если сделаем всё правильно, получим такой DataFrame:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/0-3.png" width="313" height="179" alt="" /&gt;
&lt;/div&gt;
&lt;h2&gt;Собираем данные&lt;/h2&gt;
&lt;p&gt;Будем парсить эту таблицу:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/0_1.png" width="1160" height="280" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Воспользуемся библиотекой Selenium. Перейдём на сайт и получим всю страницу, а затем преобразуем её в Soup для парсинга.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;driver = webdriver.Chrome()
driver.get('https://стопкоронавирус.рф/information/')
source_data = driver.page_source
soup = bs(source_data, 'lxml')&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;На сайте наименования регионов находятся под тегом &amp;lt;th&amp;gt;, а свежие данные по регионам под тегом &amp;lt;td&amp;gt;. Для начала получим данные.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/1-9.png" width="458" height="120" alt="" /&gt;
&lt;/div&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;divs_data = soup.find_all('td')&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Данные в divs_data выглядят следующим образом:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/2-9.png" width="816" height="312" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Вся информация идёт в одну строчку: и новые случаи, и активные, и все прочие. Тем не менее, заметно, что каждому региону соответствует пять значений: для Москвы это первые пять, для Московской области — вторые пять и так далее. Воспользуемся этим: в каждый из пяти списков будем класть значения согласно индексу. Если это первое значение, то оно войдёт в список выявленных случаев, если второе — в список новых случаев и так далее. После пяти индекс будет обнуляться.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;count = 1
for td in divs_data:
    if count == 1:
        sick_list.append(int(td.text))
    elif count == 2:
        new_list.append(int(td.text))
    elif count == 3:
        cases_list.append(int(td.text))
    elif count == 4:
        healed_list.append(int(td.text))
    elif count == 5:
        died_list.append(int(td.text))
        count = 0
    count += 1&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Следующим шагом соберём названия регионов из таблицы — они лежат под классом col-region. Из названий нужно убрать лишние двойные пробелы и символы переноса строки.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;divs_region_names = soup.find_all('th', {'class':'col-region'})
region_names_list = []
for i in range(1, len(divs_region_names)):
    region_name = divs_region_names[i].text
    region_name = region_name.replace('\n', '').replace('  ', '')
    region_names_list.append(region_name)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Соберём DataFrame:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;df = pd.DataFrame()
df['region_name'] = region_names_list
df['sick'] = sick_list
df['new'] = new_list
df['cases'] = cases_list
df['healed'] = healed_list
df['died'] = died_list&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;И посмотрим на Челябинскую область под десятым индексом — в конце наименования остался пробел! Этот пробел в конце строки может причинить много бед, ведь тогда название не будет соответствовать названию региона в geojson-файле. Уберём его — благо, все остальные наименования на сайте в порядке.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/3-8.png" width="310" height="128" alt="" /&gt;
&lt;/div&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;df.loc[10, 'region_name'] = df[df.region_name == 'Челябинская область '].region_name.item().strip(' ')&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Наконец, сделаем merge двух DataFrame по колонке названия региона, чтобы в получившейся таблице с данными по коронавирусу появилась колонка с идентификатором региона, необходимая для генерации карты.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;df = df.merge(df_regions, on='region_name')&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Визуализация данных на карте Plotly&lt;/h2&gt;
&lt;p&gt;Создадим новую фигуру — она будет являться объектом Choroplethmapbox. В параметр geojson передаём переменную counties с geojson-файлом, в параметр locations вставляем идентификаторы регионов. Параметр z — значения, которые мы хотим визуализировать. Для примера возьмём количество новых случаев в каждом регионе — они лежат в колонке new таблицы. В text передаём названия регионов. Другой параметр — colorscale — нужен для цветового сопровождения данных. Он принимает списки со значениями от 0 до 1, которые являются позициями цветов в градиенте. Чем меньше заболевших, тем зеленее будет регион. С увеличением числа заболевших цвет переходит от желтого к красному. Параметр hovertemplate — шаблон панели, появляющейся при наведении на регион. С тултипом связан ещё один аргумент — customdata. Он принимает объединенные вдоль оси объекты, которые затем можно использовать в hovertemplate для отображения новых данных.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;fig = go.Figure(go.Choroplethmapbox(geojson=counties,
                           locations=df['region_id'],
                           z=df['new'],
                           text=df['region_name'],
                           colorscale=[[0, 'rgb(34, 150, 79)'],
                                       [0.2, 'rgb(249, 247, 174)'],
                                       [0.8, 'rgb(253, 172, 99)'],
                                       [1, 'rgb(212, 50, 44)']],
                           colorbar_thickness=20,
                           customdata=np.stack([df['cases'], df['died'], df['sick'], df['healed']], axis=-1),
                           hovertemplate='&amp;lt;b&amp;gt;%{text}&amp;lt;/b&amp;gt;'+ '&amp;lt;br&amp;gt;' +
                                         'Новых случаев: %{z}' + '&amp;lt;br&amp;gt;' +
                                         'Активных: %{customdata[0]}' + '&amp;lt;br&amp;gt;' +
                                         'Умерло: %{customdata[1]}' + '&amp;lt;br&amp;gt;' +
                                         'Всего случаев: %{customdata[2]}' + '&amp;lt;br&amp;gt;' +
                                         'Выздоровело: %{customdata[3]}' +
                                         '&amp;lt;extra&amp;gt;&amp;lt;/extra&amp;gt;',
                           hoverinfo='text, z'))&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Теперь зададим стиль карты — возьмём готовую carto-positron, нейтральный и минималистичный шаблон, который не отвлекает от основных данных. Аргумент mapbox_zoom отвечает за приближение карты, а mapbox_center принимает координаты начального центра карты. Зададим marker_line_width равный нулю, чтобы убрать границы между регионами. После зададим всем отступам в margin значение 0, чтобы карта была визуально шире. Сразу после выведем фигуру методом show().&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;fig.update_layout(mapbox_style=&amp;quot;carto-positron&amp;quot;,
                  mapbox_zoom=1, mapbox_center = {&amp;quot;lat&amp;quot;: 66, &amp;quot;lon&amp;quot;: 94})
fig.update_traces(marker_line_width=0)
fig.update_layout(margin={&amp;quot;r&amp;quot;:0,&amp;quot;t&amp;quot;:0,&amp;quot;l&amp;quot;:0,&amp;quot;b&amp;quot;:0})
fig.show()&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Получилась такая карта. Из диаграммы следует, что больше всего заболевших за прошедшие сутки появилось в Москве — 608 случаев, что существенно относительно остальных регионов. Особенно в сравнении с Ненецким автономным округом, где число случаев новых заражений, на удивление, равняется нулю.&lt;/p&gt;
&lt;iframe id="igraph" scrolling="no" style="border:none;"seamless="seamless" src="http://leftjoincoronavirusmap-env.eba-knpcbpcy.us-east-1.elasticbeanstalk.com"  height="500" width="800"&gt;&lt;/iframe&gt;
&lt;p&gt;&lt;a href="https://github.com/valiotti/leftjoin/tree/master/plotly_russian_map"&gt;Полный код проекта на GitHub&lt;/a&gt;&lt;/p&gt;
</description>
<pubDate>Fri, 24 Jul 2020 15:05:41 +0300</pubDate>
</item>

<item>
<title>Деплой дашборда на AWS Elastic Beanstalk</title>
<guid isPermaLink="false">53</guid>
<link>http://test.leftjoin.ru/all/dashboard-python-2/</link>
<comments>http://test.leftjoin.ru/all/dashboard-python-2/</comments>
<description>
&lt;p&gt;Если под рукой имеется машина на Amazon Web Services и стоит задача развернуть веб-приложение, можно воспользоваться сервисом Elastic Beanstalk от AWS: он позволяет развертывать приложения под другими сервисами от Amazon, включая EC2.&lt;/p&gt;
&lt;h2&gt;Готовим приложение&lt;/h2&gt;
&lt;p&gt;В материале &lt;a href="http://test.leftjoin.ru/all/dashboard-python-1/"&gt;«Делаем дашборд с параметром на Python» &lt;/a&gt; мы создали проект с двумя файлами: application.py — скрипт с генерацией локального дашборда и get_plots.py — скрипт, возвращающий scatter plot с пивоварнями Untappd из материала &lt;a href="http://test.leftjoin.ru/all/scatter-plot-untappd/" class="nu"&gt;«&lt;u&gt;Строим scatter plot по пивоварням Untappd&lt;/u&gt;»&lt;/a&gt;. Немного подкорректируем файл application.py: чтобы приложение запускалось на Elastic Beanstalk, app.server в конце файла присвоим переменной application. Должно получиться вот так:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;application = app.server

if __name__ == '__main__':
   application.run(debug=True, port=8080)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Перед тем, как развернуть приложение, нужно собрать его в архив. В архиве должны присутствовать все необходимые файлы, включая requirements.txt — перечень зависимостей приложения. В нём перечислены пакеты и версии, необходимые для запуска приложения. Чтобы его создать, достаточно в директории с проектом и окружением ввести команду pip freeze и отправить вывод в файл:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;pip freeze &amp;gt; requirements.txt&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Теперь соберём архив. В unix для архивации и сжатия предусмотрена встроенная утилита zip.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;zip deploy_v0 application.py get_plots.py requirements.txt&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Создаём приложение и окружение&lt;/h2&gt;
&lt;p&gt;Переходим на &lt;a href="https://us-east-2.console.aws.amazon.com/elasticbeanstalk/home?region=us-east-2"&gt;Elastic Beanstalk&lt;/a&gt; в раздел «Applications». Жмём на «Create a new application».&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/01.png" width="915" height="170" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;В открывшейся странице заполняем наименование приложения и описание. Ниже предлагается присвоить приложению теги для упрощенной категоризации ресурсов. Формат вводимого тега похож на словарь Python: это пара ключ — значение, ключ должен быть уникален. После заполнения данных жмём на оранжевую кнопку «Create».&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/02-15.05.20.png" width="907" height="358" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Сразу после нам покажут список окружений для приложения: изначально он пустой, поэтому нажимаем на «Create a new environment».&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/03-2.png" width="912" height="326" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Так как мы работаем с веб-приложением, выбираем окружение веб-сервера:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/04-2.png" width="906" height="413" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;После предлагают ввести информацию о приложении, включая домен. Можно ввести свой домен, если таковой будет свободен:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/05-2.png" width="897" height="546" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Следом выбираем платформу веб-приложения. Наше написано на Python.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/06-1.png" width="898" height="413" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Теперь загружаем само приложение: так как код мы уже написали, выбираем «Upload your code» и прикрепляем файл с архивом. После жмём «Create environment».&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/07-1.png" width="893" height="834" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Следом откроется окно с логами создания окружения. Пару минут придётся подождать.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/08-1.png" width="912" height="326" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Если все сделали правильно, увидим экран с галочкой и подписью «OK»: это означает, что наше приложение успешно загружено и доступно. Если захотим загрузить новую версию, достаточно пересобрать архив с файлами и загрузить его по кнопке «Upload and deploy».&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="http://test.leftjoin.ru/pictures/09-1.png" width="914" height="388" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;По ссылке, представленной выше можем пройти на сайт, где лежит дашборд. При помощи тега &amp;lt;iframe&amp;gt; этот дашборд можно также встроить на другой сайт:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;&amp;lt;iframe id=&amp;quot;igraph&amp;quot; scrolling=&amp;quot;no&amp;quot; style=&amp;quot;border:none;&amp;quot;seamless=&amp;quot;seamless&amp;quot; src=&amp;quot;http://dashboardleftjoin-env.eba-qxzgfj64.us-east-2.elasticbeanstalk.com&amp;quot; height=&amp;quot;1100&amp;quot; width=&amp;quot;800&amp;quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;В итоге получим такой дашборд на сайте:&lt;/p&gt;
&lt;div class="embed-responsive embed-responsive-4by3" style="min-width:500"&gt;&lt;p&gt;&lt;iframe id="igraph" scrolling="no" style="border:none;"seamless="seamless" src="http://leftjoinbreweriesscatterplotrus-env.eba-bjmemw3u.us-east-1.elasticbeanstalk.com/" height="1100" width="800"&lt;/p&gt;
&lt;/iframe&gt;&lt;/div&gt;&lt;p&gt;&lt;a href="https://github.com/valiotti/leftjoin/tree/master/dashboard_deployment"&gt;Полный код проекта на GitHub&lt;/a&gt;&lt;/p&gt;
</description>
<pubDate>Wed, 22 Jul 2020 16:24:47 +0300</pubDate>
</item>

<item>
<title>Делаем дашборд с параметром на Python</title>
<guid isPermaLink="false">52</guid>
<link>http://test.leftjoin.ru/all/dashboard-python-1/</link>
<comments>http://test.leftjoin.ru/all/dashboard-python-1/</comments>
<description>
&lt;p&gt;В прошлом материале мы подготовили scatter plot, используя библиотеку plotly: он отображал отношение количества отзывов пивоварни к её рейтингу в социальной сети Untappd. Ещё мы добавили каждому маркеру характеристики: дату регистрации пивоварни и количество сортов пива в её ассортименте. Сегодня воспользуемся другим инструментом plotly — Dash, и построим дашборд с двумя параметрами для этого графика. Создадим новый файл — application.py, который будет импортировать функцию get_scatter_plot(n_days, top_n) из последнего материала.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;import dash
import dash_core_components as dcc
import dash_html_components as html
from get_plots import get_scatter_plot&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;После импорта библиотек загружаем css-стили и инициируем веб-приложение:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Опишем структуру дашборда:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;app.layout = html.Div(children=[
       html.Div([
           dcc.Graph(id='fig1'),
       ]) ,
       html.Div([
           html.H6('Временной период, дней'),
           dcc.Slider(
               id='slider-day1',
               min=0,
               max=100,
               step=1,
               value=30,
               marks={i: str(i) for i in range(0, 100, 10)}
           ),
           html.H6('Количество пивоварен в топе'),
           dcc.Slider(
               id='slider-top1',
               min=0,
               max=500,
               step=50,
               value=500,
               marks={i: str(i) for i in range(0, 500, 50)})
       ])
])&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Мы обозначили на панели график и два слайдера. У каждого слайдера есть свой идентификатор и параметры: минимальное значение, максимальное, шаг изменения, начальное значение. Так как данные из слайдеров будут передаваться в график, опишем callback для них: первый аргумент — Output — график, который будет изменяться, это наш вывод. Следующие два — Input — параметры, от которых график зависит.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;@app.callback(
   dash.dependencies.Output('fig1', 'figure'),
   [dash.dependencies.Input('slider-day1', 'value'),
    dash.dependencies.Input('slider-top1', 'value')])
def output_fig(n_days, top_n):
    fig = get_scatter_plot(n_days, top_n)
    return fig&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;В конце файла добавим вызов локального сервера:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;if __name__ == '__main__':
   app.run_server(debug=True)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Теперь при запуске файла в терминале появится адрес локального сервера. Пройдя по нему, в браузере откроем наш интерактивный дашборд, который самостоятельно обновляется при изменении значений слайдеров.&lt;/p&gt;
&lt;div class="embed-responsive embed-responsive-4by3" style="min-width:500"&gt;&lt;iframe id="igraph" scrolling="no" style="border:none;"seamless="seamless" src="http://leftjoinbreweriesscatterplotrus-env.eba-bjmemw3u.us-east-1.elasticbeanstalk.com/" height="1100" width="800" &gt;&lt;/iframe&gt;
&lt;/div&gt;</description>
<pubDate>Wed, 15 Jul 2020 13:26:06 +0300</pubDate>
</item>

<item>
<title>Строим scatter plot по пивоварням Untappd</title>
<guid isPermaLink="false">51</guid>
<link>http://test.leftjoin.ru/all/scatter-plot-untappd/</link>
<comments>http://test.leftjoin.ru/all/scatter-plot-untappd/</comments>
<description>
&lt;p&gt;Сегодня построим scatter plot, который отобразит отношение количества отзывов российских пивоварен к их средней оценке за последние 30 дней. В качестве данных будем использовать чекины социальной сети Untappd, которые пользователи оставляют для оценки пива. Маркеры на графике будут иметь две характеристики: цвет и размер. Цвет будет зависеть от даты регистрации пивоварни на сервисе (то есть показывать сколько лет пивоварне в Untappd), а размер — от количества сортов пива в её ассортименте. Этот материал — первая часть цикла материалов, посвященных построению дашборда с библиотекой dash от plotly.&lt;/p&gt;
&lt;h2&gt;Пишем запрос к Clickhouse&lt;/h2&gt;
&lt;p&gt;Данные, по которым мы хотим построить дашборд для начала нужно обработать. Мы использовали открытые данные, собранные с сайта Untappd в материалах &lt;a href="http://test.leftjoin.ru/all/selenium-button/" class="nu"&gt;«&lt;u&gt;Обрабатываем нажатие кнопки в Selenium&lt;/u&gt;»&lt;/a&gt; и &lt;a href="http://test.leftjoin.ru/all/untappd-clickhouse-dictionaries/" class="nu"&gt;«&lt;u&gt;Использование словарей в Clickhouse на примере данных Untappd&lt;/u&gt;»&lt;/a&gt;.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;from datetime import datetime, timedelta
from clickhouse_driver import Client
import plotly.graph_objects as go
import pandas as pd
import numpy as np
client = Client(host='ec1-2-34-567-89.us-east-2.compute.amazonaws.com', user='default', password='', port='9000', database='default')&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;График будет строиться в функции &lt;span class="inline-code"&gt;get_scatter_plot(n_days, top_n)&lt;/span&gt;. Первый аргумент будет отвечать за временной период, который нужно обработать. Второй — какое количество пивоварен из топа отобразить на графике. Для начала напишем SQL-запрос, который возьмёт данные из таблицы Clickhouse и посчитает Brewery Pure Average. Для каждой пивоварни на &lt;a href="https://help.untappd.com/hc/en-us/articles/360034136372-How-are-ratings-determined-on-Untappd-"&gt;сервисе он считается так&lt;/a&gt;: умножаем рейтинг сорта пива на количество оценок этого сорта и делим на общее число оценок пивоварни. Ещё запрос возьмёт наименование пивоварни и количество сортов пива у пивоварни из словаря, с которым мы работали ранее: при помощи функции &lt;span class="inline-code"&gt;dictGet&lt;/span&gt; обратимся к нему прямо в запросе и возьмём нужные колонки. Зададим ограничение: нас интересуют только те пивоварни, у которых Brewery Pure Average отличен от нуля, а количество отзывов более 100.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;brewery_pure_average = client.execute(f&amp;quot;&amp;quot;&amp;quot;
SELECT
       t1.brewery_id,
       sum(t1.beer_pure_average_mult_count / t2.count_for_that_brewery) AS brewery_pure_average,
       t2.count_for_that_brewery,
       dictGet('breweries', 'brewery_name', toUInt64(t1.brewery_id)),
       dictGet('breweries', 'beer_count', toUInt64(t1.brewery_id)),
       t3.stats_age_on_service / 365
   FROM
   (
       SELECT
           beer_id,
           brewery_id,
           sum(rating_score) AS beer_pure_average_mult_count
       FROM beer_reviews
       WHERE created_at &amp;gt;= today()-{n_days}
       GROUP BY
           beer_id,
           brewery_id
   ) AS t1
   ANY LEFT JOIN
   (
       SELECT
           brewery_id,
           count(rating_score) AS count_for_that_brewery
       FROM beer_reviews
       WHERE created_at &amp;gt;= today()-{n_days}
       GROUP BY brewery_id
   ) AS t2 ON t1.brewery_id = t2.brewery_id
   ANY LEFT JOIN
   (
       SELECT
           brewery_id,
           stats_age_on_service
       FROM brewery_info
   ) AS t3 ON t1.brewery_id = t3.brewery_id
   GROUP BY
       t1.brewery_id,
       t2.count_for_that_brewery,
       t3.stats_age_on_service
   HAVING t2.count_for_that_brewery &amp;gt;= 150
   ORDER BY brewery_pure_average
   LIMIT {top_n}
    &amp;quot;&amp;quot;&amp;quot;)

scatter_plot_df_with_age = pd.DataFrame(brewery_pure_average, columns=['brewery_id', 'brewery_pure_average', 'rating_count', 'brewery_name', 'beer_count'])&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Обрабатываем значения из DataFrame&lt;/h2&gt;
&lt;p&gt;Добавим на график две пунктирные линии: они будут проходить в медианных значениях каждой оси. Так мы будем знать, какие пивоварни находятся выше медианного значения. Лучшими будут те, что находятся в верхнем правом квадрате.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;dict_list = []
dict_list.append(dict(type=&amp;quot;line&amp;quot;,
                     line=dict(
                         color=&amp;quot;#666666&amp;quot;,
                         dash=&amp;quot;dot&amp;quot;),
                     x0=0,
                     y0=np.median(scatter_plot_df_with_age.brewery_pure_average),
                     x1=7000,
                     y1=np.median(scatter_plot_df_with_age.brewery_pure_average),
                     line_width=1,
                     layer=&amp;quot;below&amp;quot;))
dict_list.append(dict(type=&amp;quot;line&amp;quot;,
                     line=dict(
                         color=&amp;quot;#666666&amp;quot;,
                         dash=&amp;quot;dot&amp;quot;),
                     x0=np.median(scatter_plot_df_with_age.rating_count),
                     y0=0,
                     x1=np.median(scatter_plot_df_with_age.rating_count),
                     y1=5,
                     line_width=1,
                     layer=&amp;quot;below&amp;quot;))&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Добавим аннотации: они будут сообщать пользователю медианные значения.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;annotations_list = []
annotations_list.append(
    dict(
        x=8000,
        y=np.median(scatter_plot_df_with_age.brewery_pure_average) - 0.1,
        xref=&amp;quot;x&amp;quot;,
        yref=&amp;quot;y&amp;quot;,
        text=f&amp;quot;Медианное значение: {round(np.median(scatter_plot_df_with_age.brewery_pure_average), 2)}&amp;quot;,
        showarrow=False,
        font={
            'family':'Roboto, light',
            'color':'#666666',
            'size':12
        }
    )
)
annotations_list.append(
    dict(
        x=np.median(scatter_plot_df_with_age.rating_count) + 180,
        y=0.8,
        xref=&amp;quot;x&amp;quot;,
        yref=&amp;quot;y&amp;quot;,
        text=f&amp;quot;Медианное значение: {round(np.median(scatter_plot_df_with_age.rating_count), 2)}&amp;quot;,
        showarrow=False,
        font={
            'family':'Roboto, light',
            'color':'#666666',
            'size':12
        },
        textangle=-90
    )
)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Прибавим графику информативности: поделим пивоварни на 4 группы по сортам пива. В первой группе будут пивоварни, у которых менее 10 сортов пива, во второй 10 — 30 сортов, в третьей 30 — 50 и в четвертой те, у кого сортов более 50. Значения списка &lt;span class="inline-code"&gt;bucket_beer_count&lt;/span&gt; — размеры маркеров.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;bucket_beer_count = []
for beer_count in scatter_plot_df_with_age.beer_count:
   if beer_count &amp;lt; 10:
       bucket_beer_count.append(7)
   elif 10 &amp;lt;= beer_count &amp;lt;= 30:
       bucket_beer_count.append(9)
   elif 31 &amp;lt;= beer_count &amp;lt;= 50:
       bucket_beer_count.append(11)
   else:
       bucket_beer_count.append(13)
scatter_plot_df_with_age['bucket_beer_count'] = bucket_beer_count&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Следом поделим график ещё на четыре группы: теперь уже по возрасту.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;bucket_age = []
for age in scatter_plot_df_with_age.age_on_service:
   if age &amp;lt; 4:
       bucket_age.append(0)
   elif 4 &amp;lt;= age &amp;lt;= 6:
       bucket_age.append(1)
   elif 6 &amp;lt; age &amp;lt; 8:
       bucket_age.append(2)
   else:
       bucket_age.append(3)
scatter_plot_df_with_age['bucket_age'] = bucket_age&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Разделим один DataFrame на четыре, чтобы по каждому построить отдельный scatter plot со своим цветом и размером.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;scatter_plot_df_0 = scatter_plot_df[scatter_plot_df.bucket == 0]
scatter_plot_df_1 = scatter_plot_df[scatter_plot_df.bucket == 1]
scatter_plot_df_2 = scatter_plot_df[scatter_plot_df.bucket == 2]
scatter_plot_df_3 = scatter_plot_df[scatter_plot_df.bucket == 3]&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Описываем график&lt;/h2&gt;
&lt;p&gt;Построим график: поочередно добавим четыре группы пивоварен. Для каждой зададим своё имя и цвет маркера, название, прозрачность и текст.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;fig = go.Figure()
fig.add_trace(go.Scatter(
    x=scatter_plot_df_0.rating_count,
    y=scatter_plot_df_0.brewery_pure_average,
    name='&amp;lt; 4',
    mode='markers',
    opacity=0.85,
    text=scatter_plot_df_0.name_count,
    marker_color='rgb(114, 183, 178)',
    marker_size=scatter_plot_df_0.bucket_beer_count,
    textfont={&amp;quot;family&amp;quot;:&amp;quot;Roboto, light&amp;quot;,
              &amp;quot;color&amp;quot;:&amp;quot;black&amp;quot;
             }
))

fig.add_trace(go.Scatter(
    x=scatter_plot_df_1.rating_count,
    y=scatter_plot_df_1.brewery_pure_average,
    name='4 – 6',
    mode='markers',
    opacity=0.85,
    marker_color='rgb(76, 120, 168)',
    text=scatter_plot_df_1.name_count,
    marker_size=scatter_plot_df_1.bucket_beer_count,
    textfont={&amp;quot;family&amp;quot;:&amp;quot;Roboto, light&amp;quot;,
              &amp;quot;color&amp;quot;:&amp;quot;black&amp;quot;
             }
))

fig.add_trace(go.Scatter(
    x=scatter_plot_df_2.rating_count,
    y=scatter_plot_df_2.brewery_pure_average,
    name='6 – 8',
    mode='markers',
    opacity=0.85,
    marker_color='rgb(245, 133, 23)',
    text=scatter_plot_df_2.name_count,
    marker_size=scatter_plot_df_2.bucket_beer_count,
    textfont={&amp;quot;family&amp;quot;:&amp;quot;Roboto, light&amp;quot;,
              &amp;quot;color&amp;quot;:&amp;quot;black&amp;quot;
             }
))

fig.add_trace(go.Scatter(
    x=scatter_plot_df_3.rating_count,
    y=scatter_plot_df_3.brewery_pure_average,
    name='8+',
    mode='markers',
    opacity=0.85,
    marker_color='rgb(228, 87, 86)',
    text=scatter_plot_df_3.name_count,
    marker_size=scatter_plot_df_3.bucket_beer_count,
    textfont={&amp;quot;family&amp;quot;:&amp;quot;Roboto, light&amp;quot;,
              &amp;quot;color&amp;quot;:&amp;quot;black&amp;quot;
             }
))

fig.update_layout(
    title=f&amp;quot;Отношение количества отзывов к средней оценке пивоварни&amp;lt;br&amp;gt;за последние {n_days} дней, топ-{top_n} пивоварен&amp;quot;,
    font={
            'family':'Roboto, light',
            'color':'black',
            'size':14
        },
    plot_bgcolor='rgba(0,0,0,0)',
    yaxis_title=&amp;quot;Средняя оценка&amp;quot;,
    xaxis_title=&amp;quot;Количество отзывов&amp;quot;,
    legend_title_text='Возраст пивоварни&amp;lt;br&amp;gt;на Untappd, лет:',
    height=750,
    shapes=dict_list,
    annotations=annotations_list
)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Получили такой график. Каждый маркер — отдельная пивоварня. Цвет характеризует количество сортов пива этой пивоварни, а при наведении увидим среднюю оценку по данным за последние 30 дней, количество отзывов, название и количество сортов пива у пивоварни. Две пунктирные линии проходят в соответствующих медианных значениях, рассчитанных модулем numpy: пивоварни, оказавшиеся в верхнем правом квадрате — самые успешные. В следующем материале построим дашборд, и сделаем количество последних дней и количество пивоварен в топе динамически изменяемым параметром.&lt;/p&gt;
&lt;p&gt;&lt;details&gt;&lt;br /&gt;
&lt;summary&gt;&lt;span style="color:#7ea9b8"&gt;Код функции get_scatter_plot&lt;/span&gt;&lt;/summary&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code&gt;def get_scatter_plot(n_days, top_n):
    brewery_pure_average = client.execute(f'''
    SELECT 
        t1.brewery_id, 
        sum(t1.beer_pure_average_mult_count / t2.count_for_that_brewery) AS brewery_pure_average, 
        t2.count_for_that_brewery, 
        dictGet('breweries', 'brewery_name', toUInt64(t1.brewery_id)), 
        dictGet('breweries', 'beer_count', toUInt64(t1.brewery_id)),
        t3.stats_age_on_service / 365

    FROM 
    (
        SELECT 
            beer_id, 
            brewery_id, 
            sum(rating_score) AS beer_pure_average_mult_count
        FROM beer_reviews
        WHERE created_at &amp;gt;= today()-{n_days}
        GROUP BY 
            beer_id, 
            brewery_id
    ) AS t1
    ANY LEFT JOIN 
    (
        SELECT 
            brewery_id, 
            count(rating_score) AS count_for_that_brewery
        FROM beer_reviews
        WHERE created_at &amp;gt;= today()-{n_days}
        GROUP BY brewery_id
    ) AS t2 ON t1.brewery_id = t2.brewery_id
    ANY LEFT JOIN
    (
        SELECT
            brewery_id,
            stats_age_on_service
        FROM brewery_info_new
    ) AS t3 ON t1.brewery_id = t3.brewery_id
    GROUP BY 
        t1.brewery_id, 
        t2.count_for_that_brewery,
        t3.stats_age_on_service
    HAVING t2.count_for_that_brewery &amp;gt;= 150
    ORDER BY brewery_pure_average
    LIMIT {top_n}
    ''')

    scatter_plot_df_with_age = pd.DataFrame(brewery_pure_average, columns=['brewery_id', 'brewery_pure_average', 'rating_count', 'brewery_name', 'beer_count', 'age_on_service'])
    brewery_name_and_beer_count = []
    for name, beer_count in zip(scatter_plot_df_with_age.brewery_name, scatter_plot_df_with_age.beer_count):
        brewery_name_and_beer_count.append(f'{name},&amp;lt;br&amp;gt;количество сортов пива: {beer_count}')
    scatter_plot_df_with_age['name_count'] = brewery_name_and_beer_count
    dict_list = []
    dict_list.append(dict(type=&amp;quot;line&amp;quot;,
        line=dict(
             color=&amp;quot;#666666&amp;quot;,
             dash=&amp;quot;dot&amp;quot;),
        x0=0,
        y0=np.median(scatter_plot_df_with_age.brewery_pure_average),
        x1=9000,
        y1=np.median(scatter_plot_df_with_age.brewery_pure_average),
        line_width=1,
        layer=&amp;quot;below&amp;quot;))
    dict_list.append(dict(type=&amp;quot;line&amp;quot;,
        line=dict(
             color=&amp;quot;#666666&amp;quot;,
             dash=&amp;quot;dot&amp;quot;),
        x0=np.median(scatter_plot_df_with_age.rating_count),
        y0=0,
        x1=np.median(scatter_plot_df_with_age.rating_count),
        y1=5,
        line_width=1,
        layer=&amp;quot;below&amp;quot;))
    annotations_list = []
    annotations_list.append(
        dict(
            x=8000,
            y=np.median(scatter_plot_df_with_age.brewery_pure_average) - 0.1,
            xref=&amp;quot;x&amp;quot;,
            yref=&amp;quot;y&amp;quot;,
            text=f&amp;quot;Медианное значение: {round(np.median(scatter_plot_df_with_age.brewery_pure_average), 2)}&amp;quot;,
            showarrow=False,
            font={
                'family':'Roboto, light',
                'color':'#666666',
                'size':12
            }
        )
    )
    annotations_list.append(
        dict(
            x=np.median(scatter_plot_df_with_age.rating_count) + 180,
            y=0.8,
            xref=&amp;quot;x&amp;quot;,
            yref=&amp;quot;y&amp;quot;,
            text=f&amp;quot;Медианное значение: {round(np.median(scatter_plot_df_with_age.rating_count), 2)}&amp;quot;,
            showarrow=False,
            font={
                'family':'Roboto, light',
                'color':'#666666',
                'size':12
            },
            textangle=-90
        )
    )
    bucket = []
    for beer_count in scatter_plot_df_with_age.beer_count:
        if beer_count &amp;lt; 10:
            bucket.append(7)
        elif 10 &amp;lt;= beer_count &amp;lt;= 30:
            bucket.append(9)
        elif 31 &amp;lt;= beer_count &amp;lt;= 50:
            bucket.append(11)
        else:
            bucket.append(13)
    scatter_plot_df_with_age['bucket_beer_count'] = bucket
    bucket_age = []
    for age in scatter_plot_df_with_age.age_on_service:
        if age &amp;lt; 4:
            bucket_age.append(0)
        elif 4 &amp;lt;= age &amp;lt;= 6:
            bucket_age.append(1)
        elif 6 &amp;lt; age &amp;lt; 8:
            bucket_age.append(2)
        else:
            bucket_age.append(3)
    scatter_plot_df_with_age['bucket_age'] = bucket_age
    scatter_plot_df_0 = scatter_plot_df_with_age[(scatter_plot_df_with_age.bucket_age == 0)]
    scatter_plot_df_1 = scatter_plot_df_with_age[scatter_plot_df_with_age.bucket_age == 1]
    scatter_plot_df_2 = scatter_plot_df_with_age[scatter_plot_df_with_age.bucket_age == 2]
    scatter_plot_df_3 = scatter_plot_df_with_age[scatter_plot_df_with_age.bucket_age == 3]
    
    fig = go.Figure()
    fig.add_trace(go.Scatter(
        x=scatter_plot_df_0.rating_count,
        y=scatter_plot_df_0.brewery_pure_average,
        name='&amp;lt; 4',
        mode='markers',
        opacity=0.85,
        text=scatter_plot_df_0.name_count,
        marker_color='rgb(114, 183, 178)',
        marker_size=scatter_plot_df_0.bucket_beer_count,
        textfont={&amp;quot;family&amp;quot;:&amp;quot;Roboto, light&amp;quot;,
                  &amp;quot;color&amp;quot;:&amp;quot;black&amp;quot;
                 }
    ))

    fig.add_trace(go.Scatter(
        x=scatter_plot_df_1.rating_count,
        y=scatter_plot_df_1.brewery_pure_average,
        name='4 – 6',
        mode='markers',
        opacity=0.85,
        marker_color='rgb(76, 120, 168)',
        text=scatter_plot_df_1.name_count,
        marker_size=scatter_plot_df_1.bucket_beer_count,
        textfont={&amp;quot;family&amp;quot;:&amp;quot;Roboto, light&amp;quot;,
                  &amp;quot;color&amp;quot;:&amp;quot;black&amp;quot;
                 }
    ))

    fig.add_trace(go.Scatter(
        x=scatter_plot_df_2.rating_count,
        y=scatter_plot_df_2.brewery_pure_average,
        name='6 – 8',
        mode='markers',
        opacity=0.85,
        marker_color='rgb(245, 133, 23)',
        text=scatter_plot_df_2.name_count,
        marker_size=scatter_plot_df_2.bucket_beer_count,
        textfont={&amp;quot;family&amp;quot;:&amp;quot;Roboto, light&amp;quot;,
                  &amp;quot;color&amp;quot;:&amp;quot;black&amp;quot;
                 }
    ))

    fig.add_trace(go.Scatter(
        x=scatter_plot_df_3.rating_count,
        y=scatter_plot_df_3.brewery_pure_average,
        name='8+',
        mode='markers',
        opacity=0.85,
        marker_color='rgb(228, 87, 86)',
        text=scatter_plot_df_3.name_count,
        marker_size=scatter_plot_df_3.bucket_beer_count,
        textfont={&amp;quot;family&amp;quot;:&amp;quot;Roboto, light&amp;quot;,
                  &amp;quot;color&amp;quot;:&amp;quot;black&amp;quot;
                 }
    ))

    fig.update_layout(
        title=f&amp;quot;Отношение количества отзывов к средней оценке пивоварни&amp;lt;br&amp;gt;за последние {n_days} дней, топ-{top_n} пивоварен&amp;quot;,
        font={
                'family':'Roboto, light',
                'color':'black',
                'size':14
            },
        plot_bgcolor='rgba(0,0,0,0)',
        yaxis_title=&amp;quot;Средняя оценка&amp;quot;,
        xaxis_title=&amp;quot;Количество отзывов&amp;quot;,
        legend_title_text='Возраст пивоварни&amp;lt;br&amp;gt;на Untappd, лет:',
        height=750,
        shapes=dict_list,
        annotations=annotations_list
    )
    fig.show()
    return fig&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;/details&gt;&lt;/p&gt;
&lt;div class="embed-responsive embed-responsive-4by3" style="min-width:500"&gt;&lt;iframe id="igraph" scrolling="no" style="border:none;"seamless="seamless" src="https://plotly.com/~Elisejj/41.embed?showlink=false" height="800" width="800" &gt;&lt;/iframe&gt;
&lt;/div&gt;</description>
<pubDate>Fri, 10 Jul 2020 15:14:22 +0300</pubDate>
</item>


</channel>
</rss>