{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "## Открытый курс по машинному обучению\n", "
Автор материала: Тышов Никита (@nikt)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##
Библиотека визуализации Bokeh
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 1. Введение" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Визуализация данных неотъемлемая часть работы DS. Визуальный анализ помогает выявить интересные зависимости и лучше понять состояние данных. Также очень часто необходимо представлять результаты своей работы руководству, и лучшим способом оказываются графики: наглядно, красиво, понятно. И чем выше руководство тем графики должны быть более впечатляющие :). \n", " Удобная и функциональная библиотека Bokeh позволяет быстро и в удобной форме решать эти задачи. А обилие инструментов позволяет достигать невероятных результатов." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Библиотека Bokeh в первую очередь предназначена для построения графиков в веб интерфейсе. Основным выводом является html файл, который можно встраивать в любую часть уже готовой страницы или использовать отдельно. Такой формат данных позволяет очень удобно и быстро делиться графиками, достаточно отправить html файл, и любой у кого есть браузер сможет его открыть." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 2. Установка" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Установить Bokeh быстро и просто можно в Anaconda\n", "conda install bokeh\n", "\n", "или с помощью pip\n", "pip install bokeh\n", "\n", "Для начала проверим версию" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import bokeh\n", "\n", "bokeh.__version__" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\"Hello world\" для проверки работоспособности Bokeh. Заодно сразу рассмотрим вариации вывода. Как было сказано ранее Bokeh предназначен для построения графиков для веба, поэтому основной формой вывода является html файл который можно использовать как отдельно, так и вставляя в готовую веб страницу. В начале сеанса работы необходимо определить куда будет выводиться график:\n", "\n", " Для вывода есть две основные функции show и save делают они одно и тоже, только show после сохранения сразу открывает html, а save только сохраняет. \n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from bokeh.plotting import figure, output_file, output_notebook, save, show\n", "\n", "# подготовим немного данных\n", "x = [1, 2, 3, 4, 10]\n", "y = [6, 7, 2, 4, 10]\n", "\n", "# определим конфигурацию вывода\n", "output_notebook() # вывод в тетрадку\n", "#output_file(\"lines.html\") # вывод в файл \n", "\n", "# создадим объект графика и ограничим размеры для удобного рассмотрения\n", "p = figure(title=\"simple line example\", x_axis_label='x', y_axis_label='y', plot_width = 400, plot_height = 400)\n", "\n", "# добавим на него объект line\n", "p.line(x, y, legend=\"Temp.\", line_width=2)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#сохранить по указанному имени и открыть в браузере если вывод в файл\n", "#вывести в тетрадку если вывод в тетрадку\n", "show(p)\n", "#сохранить по указанному имени если вывод в файл\n", "#сохранить во временный файл '/tmp/tmpkctayz0p.html' если вывод в тетрадку\n", "save(p)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Это самый простой график в Bokeh. Все что нужно обычному графику: оси, легенда, заголовок - здесь присутствуют. Плюс справа панель с инструментами: \n", "\n", " Для большей нагдядности, и чтобы не придумывать данные, я загрузил знакомый датасет из второй лекции и подготовил все необходимые данные." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import pandas as pd\n", "\n", "df = pd.read_csv('../../data/video_games_sales.csv')\n", "df = df.dropna()\n", "df['User_Score'] = df.User_Score.astype('float64')\n", "df['Year_of_Release'] = df.Year_of_Release.astype('int64')\n", "df['User_Count'] = df.User_Count.astype('int64')\n", "df['Critic_Count'] = df.Critic_Count.astype('int64')\n", "useful_cols = ['Name', 'Platform', 'Year_of_Release', 'Genre', \n", " 'Global_Sales', 'Critic_Score', 'Critic_Count',\n", " 'User_Score', 'User_Count', 'Rating'\n", " ]\n", "df[useful_cols].head()\n", "sales_df = df[[x for x in df.columns if 'Sales' in x] + ['Year_of_Release']]\n", "sales_df = sales_df.groupby('Year_of_Release',as_index=True).sum()\n", "data = sales_df.to_dict(orient='list')\n", "platform_year_sales = df.pivot_table(\n", " index='Year_of_Release', \n", " columns='Platform', \n", " values='Global_Sales', \n", " aggfunc=sum).fillna(0).applymap(float)\n", "platform_year_sales_dict = platform_year_sales.to_dict(orient='list')\n", "\n", "platform_year_critic = df[['Critic_Score','Year_of_Release']].groupby('Year_of_Release',as_index=True).mean()\n", "platform_year_critic_dict = platform_year_critic.to_dict(orient='list')\n", "\n", "genre_year_sales = df.pivot_table(\n", " index='Year_of_Release', \n", " columns='Genre', \n", " values='Global_Sales', \n", " aggfunc=sum).fillna(0).applymap(float)\n", "genre_year_sales_dict = genre_year_sales.to_dict(orient='list')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 3. Примеры" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Для примера построим график продаж видео игр в различных странах в зависимости от года. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# импортируем палитру, простой доступ к нужному количеству цветов()\n", "from bokeh.palettes import viridis\n", "\n", "p = figure(plot_width=800, plot_height=400,title=\"Sales\")\n", "\n", "for key,color in zip(data,viridis(len(data))):\n", " p.line(y=data[key],x=sales_df.index.values,legend=key,color=color,)\n", "\n", "p.xaxis.axis_label = 'Year'\n", "p.yaxis.axis_label = 'Sales' \n", " \n", "show(p)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "На данном этапе график ничем не примечателен, но уже имеет базовый набор инструментов и интерактивен. Добавим в него красоты и функциональности. Сначала сделаем линии толще, перенесем легенду влево и наделим ее функцией скрывания выбранного графика. При нажатии на запись в легенде, соответствующий график можно скрывать - hide, или уменьшать его насыщеность - mute. При использовании mute необходимо в объекте указать muted_color и muted_alpha. Также добавим размерность оси Y" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from bokeh.models import PrintfTickFormatter\n", "\n", "p = figure(plot_width=800, plot_height=400,title=\"Sales\")\n", "\n", "for key,color in zip(data,viridis(len(data))):\n", " p.line(y=data[key],x=sales_df.index.values,legend=key,color=color,line_width = 2,\n", " muted_color = color, # параметр для mute\n", " muted_alpha = 0.3) # параметр для mute\n", " \n", "p.xaxis.axis_label = 'Year'\n", "p.yaxis.axis_label = 'Sales' \n", "p.yaxis[0].formatter = PrintfTickFormatter(format=\"%0u mln\")\n", "#расположение легенды\n", "p.legend.location = \"top_left\"\n", "#поведение легенды hide-скрыть, mute-понизить насыщеность\n", "p.legend.click_policy= \"mute\"\n", "\n", "show(p)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь добавим новый инструмент. HoverTool - инструмент позволяющий привязывать к точкам дополнительную информацию. Например выводить значения конкретного положения, или любую другую информацию." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from bokeh.models import HoverTool\n", "\n", "#добавим новый инструмент, также придется перечислить все остальные\n", "_tools_to_show = 'box_zoom,pan,save,hover,reset,wheel_zoom'\n", "\n", "p = figure(plot_width=800, plot_height=400,title=\"Sales\",tools=_tools_to_show)\n", "\n", "for key,color in zip(data,viridis(len(data))):\n", " p.line(y=data[key],x=sales_df.index.values,legend=key,color=color,line_width = 2,\n", " muted_color = color,\n", " muted_alpha = 0.3,)\n", "\n", "#конфигурация инструмента\n", "hover = p.select(dict(type=HoverTool))\n", "# Поле - значение, поддержка форматирования вывода.\n", "hover.tooltips = [(\"Sale\", \"@y{0.0f} mln\"), (\"Year\", \"@x\")] \n", "\n", "\n", "p.xaxis.axis_label = 'Year'\n", "p.yaxis.axis_label = 'Sales' \n", "\n", "p.legend.location = \"top_left\"\n", "p.legend.click_policy= \"mute\"\n", "\n", "show(p)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "У HoverTool есть интересные режимы работы vline, hline,они очень хорошо смотрятся когда не накладываются. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from bokeh.models import HoverTool\n", "\n", "_tools_to_show = 'box_zoom,pan,save,hover,reset,wheel_zoom'\n", "\n", "p = figure(plot_width=800, plot_height=400,title=\"Sales\",tools=_tools_to_show)\n", "\n", "for key,color in zip(data,viridis(len(data))):\n", " p.line(y=data[key],x=sales_df.index.values,legend=key,color=color,line_width = 2,\n", " muted_color = color,\n", " muted_alpha = 0.3,)\n", "\n", "hover = p.select(dict(type=HoverTool))\n", "hover.tooltips = [(\"Sale\", \"@y{0.0f} mln\"), (\"Year\", \"@x\")] \n", "#Добавляем режимы работы\n", "hover.mode = 'vline'\n", "\n", "p.xaxis.axis_label = 'Year'\n", "p.yaxis.axis_label = 'Sales' \n", "\n", "p.legend.location = \"top_left\"\n", "p.legend.click_policy= \"mute\"\n", "\n", "show(p)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь добавим еще пару графиков динамики, в жанрах и платформах. Организуем это в столбик. Кстати, вы заметили, что при скроле возле конкретной оси, скейл происходит только данной оси." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from bokeh.layouts import column\n", "from bokeh.models import HoverTool\n", "\n", "_tools_to_show = 'box_zoom,pan,save,hover,reset,wheel_zoom'\n", "\n", "########################## Первый график #####################################################################\n", "\n", "p = figure(plot_width=950, plot_height=400,title=\"Sales\",tools=_tools_to_show)\n", "\n", "for key,color in zip(data,viridis(len(data))):\n", " p.line(y=data[key],x=sales_df.index.values,legend=key,color=color,line_width = 2,\n", " muted_color = color,\n", " muted_alpha = 0.3,)\n", "\n", "hover = p.select(dict(type=HoverTool))\n", "hover.tooltips = [(\"Sale\", \"@y{0.0f} mln\"), (\"Year\", \"@x\")] \n", "\n", "p.xaxis.axis_label = 'Year'\n", "p.yaxis.axis_label = 'Sales' \n", "\n", "p.legend.location = \"top_left\"\n", "p.legend.click_policy= \"mute\"\n", "\n", "########################## Второй график #####################################################################\n", "p1 = figure(plot_width=950, plot_height=400,title=\"Sales platforms\",tools=_tools_to_show)\n", "\n", "for key,color in zip(platform_year_sales_dict,viridis(len(platform_year_sales_dict))):\n", " p1.line(y=platform_year_sales_dict[key],x=platform_year_sales.index.values,legend=key,color=color,line_width = 2,\n", " muted_color = color,\n", " muted_alpha = 0.3,)\n", "\n", "hover = p1.select(dict(type=HoverTool))\n", "hover.tooltips = [(\"Sale\", \"@y{0.0f} mln\"), (\"Year\", \"@x\")] \n", "\n", "p1.xaxis.axis_label = 'Year'\n", "p1.yaxis.axis_label = 'Sales' \n", "# Подкорректируем большую легенду, чтобы влезала.\n", "p1.legend.location = \"top_left\"\n", "p1.legend.click_policy= \"mute\"\n", "p1.legend.orientation = \"horizontal\"\n", "p1.legend.label_text_font_size = '8pt'\n", "\n", "########################## Третий график #####################################################################\n", "p2 = figure(plot_width=950, plot_height=400,title=\"Sales genres\",tools=_tools_to_show)\n", "\n", "for key,color in zip(genre_year_sales_dict,viridis(len(genre_year_sales_dict))):\n", " p2.line(y=genre_year_sales_dict[key],x=genre_year_sales.index.values,legend=key,color=color,line_width = 2,\n", " muted_color = color,\n", " muted_alpha = 0.3,)\n", "\n", "hover = p2.select(dict(type=HoverTool))\n", "hover.tooltips = [(\"Sale\", \"@y{0.0f} mln\"), (\"Year\", \"@x\")] \n", "\n", "p2.xaxis.axis_label = 'Year'\n", "p2.yaxis.axis_label = 'Sales' \n", "\n", "p2.legend.location = \"top_left\"\n", "p2.legend.click_policy= \"mute\"\n", "p2.legend.orientation = \"horizontal\"\n", "p2.legend.label_text_font_size = '8pt'\n", "\n", "########################## Layout #####################################################################\n", "# конфигурация Layuot\n", "show(column(p,p1,p2))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Добавим связность графиков. При зуме одного, также будут зумиться и другие. Так как второй и третий график имеют примерно один диапазон по Y, то свяжем их по Y. Также свяжем все графики по Х. Инструмент Box_zoom при этом работает исправно и зуммирует все графики." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "_tools_to_show = 'box_zoom,pan,save,hover,reset,wheel_zoom'\n", "\n", "########################## Первый график #####################################################################\n", "\n", "p = figure(plot_width=950, plot_height=400,title=\"Sales\",tools=_tools_to_show)\n", "\n", "for key,color in zip(data,viridis(len(data))):\n", " p.line(y=data[key],x=sales_df.index.values,legend=key,color=color,line_width = 2,\n", " muted_color = color,\n", " muted_alpha = 0.3,)\n", "\n", "hover = p.select(dict(type=HoverTool))\n", "hover.tooltips = [(\"Sale\", \"@y{0.0f} mln\"), (\"Year\", \"@x\")] \n", "\n", "p.xaxis.axis_label = 'Year'\n", "p.yaxis.axis_label = 'Sales' \n", "\n", "p.legend.location = \"top_left\"\n", "p.legend.click_policy= \"mute\"\n", "\n", "########################## Второй график #####################################################################\n", "#Добавим связь с первым графиком по х, x_range=p.x_range\n", "p1 = figure(plot_width=950, plot_height=400,title=\"Sales platforms\",tools=_tools_to_show,x_range=p.x_range)\n", "\n", "for key,color in zip(platform_year_sales_dict,viridis(len(platform_year_sales_dict))):\n", " p1.line(y=platform_year_sales_dict[key],x=platform_year_sales.index.values,legend=key,color=color,line_width = 2,\n", " muted_color = color,\n", " muted_alpha = 0.3,)\n", "\n", "hover = p1.select(dict(type=HoverTool))\n", "hover.tooltips = [(\"Sale\", \"@y{0.0f} mln\"), (\"Year\", \"@x\")] \n", "\n", "p1.xaxis.axis_label = 'Year'\n", "p1.yaxis.axis_label = 'Sales' \n", "\n", "p1.legend.location = \"top_left\"\n", "p1.legend.click_policy= \"mute\"\n", "p1.legend.orientation = \"horizontal\"\n", "p1.legend.label_text_font_size = '8pt'\n", "\n", "########################## Третий график #####################################################################\n", "#Добавим связь с первым графиком по х, x_range=p.x_range, и со вторым графиком по y y_range=p1.y_range\n", "p2 = figure(plot_width=950, plot_height=400,title=\"Sales generes\",tools=_tools_to_show,x_range=p.x_range,y_range=p1.y_range)\n", "\n", "for key,color in zip(genre_year_sales_dict,viridis(len(genre_year_sales_dict))):\n", " p2.line(y=genre_year_sales_dict[key],x=genre_year_sales.index.values,legend=key,color=color,line_width = 2,\n", " muted_color = color,\n", " muted_alpha = 0.3,)\n", "\n", "hover = p2.select(dict(type=HoverTool))\n", "hover.tooltips = [(\"Sale\", \"@y{0.0f} mln\"), (\"Year\", \"@x\")] \n", "\n", "p2.xaxis.axis_label = 'Year'\n", "p2.yaxis.axis_label = 'Sales' \n", "\n", "p2.legend.location = \"top_left\"\n", "p2.legend.click_policy= \"mute\"\n", "p2.legend.orientation = \"horizontal\"\n", "p2.legend.label_text_font_size = '8pt'\n", "\n", "########################## Layout #####################################################################\n", "\n", "show(column(p,p1,p2))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В завершении, для визуализации динамики оценок критиков, добавим на первый график вторую ось Y . Для валидного отображения необходимо переопределить HoverTool." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from bokeh.models import LinearAxis, Range1d\n", "\n", "_tools_to_show = 'box_zoom,pan,save,hover,reset,wheel_zoom'\n", "\n", "########################## Первый график #####################################################################\n", "\n", "p = figure(plot_width=950, plot_height=400,title=\"Sales\",tools=_tools_to_show)\n", "\n", "for key,color in zip(data,viridis(len(data))):\n", " p.line(y=data[key],x=sales_df.index.values,legend=key,color=color,line_width = 2,\n", " muted_color = color,\n", " muted_alpha = 0.3)\n", "\n", "hover = p.select(dict(type=HoverTool))\n", "hover.tooltips = [(\"Sale\", \"@y{0.0f} mln\"), (\"Year\", \"@x\")] \n", "\n", "p.xaxis.axis_label = 'Year'\n", "p.yaxis.axis_label = 'Sales' \n", "\n", "p.legend.location = \"top_left\"\n", "p.legend.click_policy= \"mute\"\n", "\n", "#Добавление дополнительной оси.\n", "p.extra_y_ranges = {\"Critic\": Range1d(start=0, end=100)}\n", "# Рисование нового графика\n", "add_line = p.line(platform_year_critic.index.values, platform_year_critic.values.reshape(-1),\n", " color=\"blue\", y_range_name=\"Critic\", legend=\"Critic score\",\n", " muted_color = \"blue\",\n", " muted_alpha = 0.3,\n", " line_dash=\"4 4\")\n", "p.add_layout(LinearAxis(y_range_name=\"Critic\"), 'left')\n", "#Переопределение HoverTool для нового графика\n", "p.add_tools(HoverTool(tooltips=[(\"Critic score\", \"@y{0.0f}\"), (\"Year\", \"@x\")] , renderers=[add_line]))\n", "########################## Второй график #####################################################################\n", "p1 = figure(plot_width=950, plot_height=400,title=\"Sales platforms\",tools=_tools_to_show,x_range=p.x_range)\n", "\n", "for key,color in zip(platform_year_sales_dict,viridis(len(platform_year_sales_dict))):\n", " p1.line(y=platform_year_sales_dict[key],x=platform_year_sales.index.values,legend=key,color=color,line_width = 2,\n", " muted_color = color,\n", " muted_alpha = 0.3)\n", "\n", "hover = p1.select(dict(type=HoverTool))\n", "hover.tooltips = [(\"Sale\", \"@y{0.0f} mln\"), (\"Year\", \"@x\")] \n", "p1.xaxis.axis_label = 'Year'\n", "p1.yaxis.axis_label = 'Sales' \n", "\n", "p1.legend.location = \"top_left\"\n", "p1.legend.click_policy= \"mute\"\n", "p1.legend.orientation = \"horizontal\"\n", "p1.legend.label_text_font_size = '8pt'\n", "\n", "########################## Третий график #####################################################################\n", "p2 = figure(plot_width=950, plot_height=400,title=\"Sales generes\",tools=_tools_to_show,x_range=p.x_range,y_range=p1.y_range)\n", "\n", "for key,color in zip(genre_year_sales_dict,viridis(len(genre_year_sales_dict))):\n", " p2.line(y=genre_year_sales_dict[key],x=genre_year_sales.index.values,legend=key,color=color,line_width = 2,\n", " muted_color = color,\n", " muted_alpha = 0.3,)\n", "\n", "hover = p2.select(dict(type=HoverTool))\n", "hover.tooltips = [(\"Sale\", \"@y{0.0f} mln\"), (\"Year\", \"@x\")] \n", "\n", "p2.xaxis.axis_label = 'Year'\n", "p2.yaxis.axis_label = 'Sales' \n", "\n", "p2.legend.location = \"top_left\"\n", "p2.legend.click_policy= \"mute\"\n", "p2.legend.orientation = \"horizontal\"\n", "p2.legend.label_text_font_size = '8pt'\n", "\n", "########################## Layout #####################################################################\n", "\n", "show(column(p,p1,p2))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 3. Вывод и полезные ссылки" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Bokeh очень мощный инструмент для построения графиков. В данном ноутбуке я показал только то, что мне очень понравилось при первом знакомстве с этой библиотекой. Возможно, есть еще более крутые фишки до которых я не добрался. Целью ноутбука ставлю - заинтересовать вас, если вам после просмотра захотелось разобраться получше, то вот ссылки:\n", "" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.5.2" } }, "nbformat": 4, "nbformat_minor": 2 }