Разработка интерфейсов визуализации данных на Python с помощью Dash

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

Я всегда работаю в PyCharm, поэтому, прежде чем приступить к работе, я создаю проект, в котором создается виртуальная среда. Далее, я добавляю в проект файл, с которым будет происходить работа, а именно: avocado.csv, содержащий набор данных о продажах и ценах на авокадо в США за период с 2015 по 2018 годы. Скачать данный файл можно здесь. После этого, необходимо установить следующие библиотеки: dash и pandas. Теперь мы можем приступать к работе!

Создание первого приложения

Давайте создадим пустой файл с именем app1.py в корневом каталоге нашего проекта и создадим наше первое приложение:

import dash
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd

data = pd.read_csv("avocado.csv")
data = data.query("type == 'conventional' and region == 'Albany'")
data["Date"] = pd.to_datetime(data["Date"], format="%Y-%m-%d")
data.sort_values("Date", inplace=True)

app = dash.Dash(__name__)

app.layout = html.Div(
    children=[
        html.H1(
            children="Avocado Analytics",
            style={"fontSize": "48px", "color": "red"},
        ),
        html.P(
            children="Analyze the behavior of avocado prices"
            " and the number of avocados sold in the US"
            " between 2015 and 2018",
        ),
        dcc.Graph(
            figure={
                "data": [
                    {
                        "x": data["Date"],
                        "y": data["AveragePrice"],
                        "type": "lines",
                    },
                ],
                "layout": {"title": "Average Price of Avocados"},
            },
        ),
        dcc.Graph(
            figure={
                "data": [
                    {
                        "x": data["Date"],
                        "y": data["Total Volume"],
                        "type": "lines",
                    },
                ],
                "layout": {"title": "Avocados Sold"},
            },
        ),
    ]
)

if __name__ == "__main__":
    app.run_server(debug=True)

В строках с 1 по 4 мы импортируем необходимые библиотеки: dash, dash_core_components, dash_html_components и pandas. Каждая библиотека предоставляет стандартный блок для вашего приложения:
• dash поможет нам инициализировать ваше приложение.
• dash_core_components позволяет создавать интерактивные компоненты, такие как графики, раскрывающиеся списки или диапазоны дат.
• dash_html_components позволяет получить доступ к тегам HTML.
• pandas помогает читать и систематизировать данные.

В строках с 6 по 9 мы считываем данные и предварительно обрабатываем их для использования на панели управления. Мы фильтруем некоторые данные, потому что текущая версия нашей информационной панели не является интерактивной и в противном случае значения на графике не имели бы смысла. В строке 11, мы создаем экземпляр класса Dash. Если вы раньше использовали Flask, то инициализация класса Dash может показаться вам знакомой. В Flask вы обычно инициализируете приложение WSGI с помощью Flask(__name__). Точно так же для приложения Dash вы используете Dash(__name__).

Далее, мы определяем свойство макета нашего приложения, которое определяет внешний вид нашего приложения. В нашем случае мы будем использовать заголовок с описанием под ним и двумя графиками В строках с 13 по 23 вы можете увидеть на практике компоненты Dash HTML. Мы начинаем с определения родительского компонента, html.Div. Затем мы добавляем еще два элемента, заголовок (html.H1) и абзац (html.P) в качестве его дочерних элементов. Эти компоненты эквивалентны div, HTML-теги h1 и p. Вы можете использовать аргументы компонентов для изменения атрибутов или содержимого тегов. Например, чтобы указать, что находится внутри тега div, мы используем аргумент children в html.Div. В компонентах также есть другие аргументы, такие как style, className или id, которые относятся к атрибутам тегов HTML. При этом, если вы хотите изменить размер и цвет элемента H1, то достаточно прописать изменения через style, как это сделал я в 17 строке.

В строках 24–50 фрагмента кода макета вы можете увидеть компонент графика из Dash Core Components на практике. В app.layout есть два компонента dcc.Graph. Первый отображает средние цены на авокадо за период исследования, а второй — количество авокадо, проданных в США за тот же период. Под капотом Dash использует Plotly.js для создания графиков. Компоненты dcc.Graph ожидают объект фигуры или словарь Python, содержащий данные графика и макет. В этом случае вы предоставляете последнее.
И последние две строчки кода позволяют запускать приложение Dash локально, используя встроенный сервер Flask. Далее, мы запускаем наше приложение, после чего переходим по высветившейся ссылке и смотрим результат:

Создание второго приложения с добавлением внешних ресурсов

В данной главе вы узнаете, как настроить внешний вид панели инструментов, а именно:
• Добавление значка и заголовка на страницу.
• Изменение семейства шрифтов вашей панели инструментов.
• Использование внешних файлов CSS для стилизации компонентов Dash.

Сначала, в папке с проектом, мы создаем еще одну папку с именем assets, в которую добавляем скаченный значок с именем favicon.ico (скачать его можно здесь) и создаем файл style.css, содержащий следующие команды:

body {
    font-family: "Lato", sans-serif;
    margin: 0;
    background-color: #F7F7F7;
}

.header {
    background-color: #222222;
    height: 256px;
    display: flex;
    flex-direction: column;
    justify-content: center;
}

.header-emoji {
    font-size: 48px;
    margin: 0 auto;
    text-align: center;
}

.header-title {
    color: #FFFFFF;
    font-size: 48px;
    font-weight: bold;
    text-align: center;
    margin: 0 auto;
}

.header-description {
    color: #CFCFCF;
    margin: 4px auto;
    text-align: center;
    max-width: 384px;
}

.wrapper {
    margin-right: auto;
    margin-left: auto;
    max-width: 1024px;
    padding-right: 10px;
    padding-left: 10px;
    margin-top: 32px;
}

.card {
    margin-bottom: 24px;
    box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.18);
}

Файлы в папке assets содержат стили, которые мы применим к компонентам в макете приложения. После запуска сервера, Dash автоматически обслуживает файлы, расположенные в этой папке. И для установки значка по умолчанию нам не нужно предпринимать никаких дополнительных действий. Для использования стилей, определенных в style.css, нам необходимо использовать аргумент className в компонентах Dash. Поэтому, давайте создадим новый файл app2.py, в котором пропишем следующий код:

import dash
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd

data = pd.read_csv("avocado.csv")
data = data.query("type == 'conventional' and region == 'Albany'")
data["Date"] = pd.to_datetime(data["Date"], format="%Y-%m-%d")
data.sort_values("Date", inplace=True)

external_stylesheets = [
    {
        "href": "https://fonts.googleapis.com/css2?"
        "family=Lato:wght@400;700&display=swap",
        "rel": "stylesheet",
    },
]
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
app.title = "Avocado Analytics: Understand Your Avocados!"

app.layout = html.Div(
    children=[
        html.Div(
            children=[
                html.P(children="🥑", className="header-emoji"),
                html.H1(
                    children="Avocado Analytics", className="header-title"
                ),
                html.P(
                    children="Analyze the behavior of avocado prices"
                    " and the number of avocados sold in the US"
                    " between 2015 and 2018",
                    className="header-description",
                ),
            ],
            className="header",
        ),
        html.Div(
            children=[
                html.Div(
                    children=dcc.Graph(
                        id="price-chart",
                        config={"displayModeBar": False},
                        figure={
                            "data": [
                                {
                                    "x": data["Date"],
                                    "y": data["AveragePrice"],
                                    "type": "lines",
                                    "hovertemplate": "$%{y:.2f}"
                                                     "",
                                },
                            ],
                            "layout": {
                                "title": {
                                    "text": "Average Price of Avocados",
                                    "x": 0.05,
                                    "xanchor": "left",
                                },
                                "xaxis": {"fixedrange": True},
                                "yaxis": {
                                    "tickprefix": "$",
                                    "fixedrange": True,
                                },
                                "colorway": ["#17B897"],
                            },
                        },
                    ),
                    className="card",
                ),
                html.Div(
                    children=dcc.Graph(
                        id="volume-chart",
                        config={"displayModeBar": False},
                        figure={
                            "data": [
                                {
                                    "x": data["Date"],
                                    "y": data["Total Volume"],
                                    "type": "lines",
                                },
                            ],
                            "layout": {
                                "title": {
                                    "text": "Avocados Sold",
                                    "x": 0.05,
                                    "xanchor": "left",
                                },
                                "xaxis": {"fixedrange": True},
                                "yaxis": {"fixedrange": True},
                                "colorway": ["#E12D39"],
                            },
                        },
                    ),
                    className="card",
                ),
            ],
            className="wrapper",
        ),
    ]
)

if __name__ == "__main__":
    app.run_server(debug=True)

Код точно такой же, как и в app1.py, но с некоторыми изменениями. В строках с 21 по 37 вы можете увидеть, что в исходную версию панели мониторинга были внесены два изменения:
1. Появился новый элемент абзаца с эмодзи авокадо, который будет служить логотипом.
2. В каждом компоненте есть аргумент className. Эти имена классов должны соответствовать селектору классов в style.css, который будет определять внешний вид каждого компонента.
Например, класс header-description, назначенный компоненту абзаца, начинающемуся с «Analyze the behavior of avocado prices», имеет соответствующий селектор в style.css:

.header-description {
    color: #CFCFCF;
    margin: 4px auto;
    text-align: center;
    max-width: 384px;
}

Строки с 29 по 34 файла style.css определяют формат селектора класса описания заголовка. Они изменят цвет, поля, выравнивание и максимальную ширину любого компонента с className = "header-description". Все компоненты имеют соответствующие селекторы классов в файле CSS. Другое существенное изменение — графики. При построении графика цены, мы выполнили следующие изменения:
• Строка 43: мы удаляем плавающую полосу, которая отображается на графике по умолчанию.
• Строки 50 и 51: мы устанавливаем шаблон наведения таким образом, чтобы при наведении курсора на точку данных отображалась цена в долларах. Вместо 2,5он будет отображаться как 2,5 доллара.
• Строки с 54 по 66: мы настраиваем ось, цвет рисунка и формат заголовка в разделе макета графика.
• Строка 69: мы оборачиваем график в html.Div с классом «card». Это придаст графику белый фон и добавит небольшую тень под ним.
Аналогичные изменения внесены в графики продаж и объемов. После всех этих изменений наша панель управления должна выглядеть так:

Добавление интерактивности в приложения Dash с помощью обратных вызовов

В этом разделе мы будем добавлять интерактивные элементы на панель управления. Интерактивность Dash основана на парадигме реактивного программирования. Это означает, что вы можете связывать компоненты с элементами вашего приложения, которые хотите обновить. Если пользователь взаимодействует с компонентом ввода, например, с раскрывающимся списком или ползунком диапазона, то вывод, например, график, будет автоматически реагировать на изменения во вводе. Теперь давайте сделаем вашу панель управления интерактивной. Эта новая версия вашей панели инструментов позволит пользователю взаимодействовать со следующими фильтрами:
• Область
• Тип авокадо
• Диапазон дат
Поэтому, давайте создадим новый файл в папке assets с имеем style2.css и пропишем следующие команды:

 
body {
    font-family: "Lato", sans-serif;
    margin: 0;
    background-color: #F7F7F7;
}

.header {
    background-color: #222222;
    height: 288px;
    padding: 16px 0 0 0;
}

.header-emoji {
    font-size: 48px;
    margin: 0 auto;
    text-align: center;
}

.header-title {
    color: #FFFFFF;
    font-size: 48px;
    font-weight: bold;
    text-align: center;
    margin: 0 auto;
}

.header-description {
    color: #CFCFCF;
    margin: 4px auto;
    text-align: center;
    max-width: 384px;
}

.wrapper {
    margin-right: auto;
    margin-left: auto;
    max-width: 1024px;
    padding-right: 10px;
    padding-left: 10px;
    margin-top: 32px;
}

.card {
    margin-bottom: 24px;
    box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.18);
}

.menu {
    height: 112px;
    width: 912px;
    display: flex;
    justify-content: space-evenly;
    padding-top: 24px;
    margin: -80px auto 0 auto;
    background-color: #FFFFFF;
    box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.18);
}

.Select-control {
    width: 256px;
    height: 48px;
}

.Select--single > .Select-control .Select-value, .Select-placeholder {
    line-height: 48px;
}

.Select--multi .Select-value-label {
    line-height: 32px;
}

.menu-title {
    margin-bottom: 6px;
    font-weight: bold;
    color: #079A82;
}

После этого, мы создаем еще один файл, но уже просто в папке с проектом, с именем app3.py, в котором прописываем следующий код:

import dash
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd
import numpy as np
from dash.dependencies import Output, Input

data = pd.read_csv("avocado.csv")
data["Date"] = pd.to_datetime(data["Date"], format="%Y-%m-%d")
data.sort_values("Date", inplace=True)

external_stylesheets = [
    {
        "href": "https://fonts.googleapis.com/css2?"
        "family=Lato:wght@400;700&display=swap",
        "rel": "stylesheet",
    },
]
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
app.title = "Avocado Analytics: Understand Your Avocados!"

app.layout = html.Div(
    children=[
        html.Div(
            children=[
                html.P(children="🥑", className="header-emoji"),
                html.H1(
                    children="Avocado Analytics", className="header-title"
                ),
                html.P(
                    children="Analyze the behavior of avocado prices"
                    " and the number of avocados sold in the US"
                    " between 2015 and 2018",
                    className="header-description",
                ),
            ],
            className="header",
        ),
        html.Div(
            children=[
                html.Div(
                    children=[
                        html.Div(children="Region", className="menu-title"),
                        dcc.Dropdown(
                            id="region-filter",
                            options=[
                                {"label": region, "value": region}
                                for region in np.sort(data.region.unique())
                            ],
                            value="Albany",
                            clearable=False,
                            className="dropdown",
                        ),
                    ]
                ),
                html.Div(
                    children=[
                        html.Div(children="Type", className="menu-title"),
                        dcc.Dropdown(
                            id="type-filter",
                            options=[
                                {"label": avocado_type, "value": avocado_type}
                                for avocado_type in data.type.unique()
                            ],
                            value="organic",
                            clearable=False,
                            searchable=False,
                            className="dropdown",
                        ),
                    ],
                ),
                html.Div(
                    children=[
                        html.Div(
                            children="Date Range",
                            className="menu-title"
                            ),
                        dcc.DatePickerRange(
                            id="date-range",
                            min_date_allowed=data.Date.min().date(),
                            max_date_allowed=data.Date.max().date(),
                            start_date=data.Date.min().date(),
                            end_date=data.Date.max().date(),
                        ),
                    ]
                ),
            ],
            className="menu",
        ),
        html.Div(
            children=[
                html.Div(
                    children=dcc.Graph(
                        id="price-chart", config={"displayModeBar": False},
                    ),
                    className="card",
                ),
                html.Div(
                    children=dcc.Graph(
                        id="volume-chart", config={"displayModeBar": False},
                    ),
                    className="card",
                ),
            ],
            className="wrapper",
        ),
    ]
)


@app.callback(
    [Output("price-chart", "figure"), Output("volume-chart", "figure")],
    [
        Input("region-filter", "value"),
        Input("type-filter", "value"),
        Input("date-range", "start_date"),
        Input("date-range", "end_date"),
    ],
)
def update_charts(region, avocado_type, start_date, end_date):
    mask = (
        (data.region == region)
        & (data.type == avocado_type)
        & (data.Date >= start_date)
        & (data.Date <= end_date)
    )
    filtered_data = data.loc[mask, :]
    price_chart_figure = {
        "data": [
            {
                "x": filtered_data["Date"],
                "y": filtered_data["AveragePrice"],
                "type": "lines",
                "hovertemplate": "$%{y:.2f}",
            },
        ],
        "layout": {
            "title": {
                "text": "Average Price of Avocados",
                "x": 0.05,
                "xanchor": "left",
            },
            "xaxis": {"fixedrange": True},
            "yaxis": {"tickprefix": "$", "fixedrange": True},
            "colorway": ["#17B897"],
        },
    }

    volume_chart_figure = {
        "data": [
            {
                "x": filtered_data["Date"],
                "y": filtered_data["Total Volume"],
                "type": "lines",
            },
        ],
        "layout": {
            "title": {"text": "Avocados Sold", "x": 0.05, "xanchor": "left"},
            "xaxis": {"fixedrange": True},
            "yaxis": {"fixedrange": True},
            "colorway": ["#E12D39"],
        },
    }
    return price_chart_figure, volume_chart_figure


if __name__ == "__main__":
    app.run_server(debug=True)

Данный код очень схож с app1 и app2, но он так же претерпел изменения. В строках с 24 по 74 мы определяем html.Div поверх наших графиков, состоящих из двух раскрывающихся списков и селектора диапазона дат. Он будет служить меню, которое пользователь будет использовать для взаимодействия с данными:

В строках с 41 по 55 мы определяем раскрывающийся список, который пользователи будут использовать для фильтрации данных по региону. Помимо заголовка, в нем есть компонент dcc.Dropdown. Вот что означает каждый из параметров:
• id — идентификатор элемента.
• options — это параметры, отображаемые при выборе раскрывающегося списка. Он ожидает словарь с метками и значениями.
• value — значение по умолчанию при загрузке страницы.
• clearable позволяет пользователю оставить это поле пустым, если установлено значение True.
• className — это селектор классов, используемый для применения стилей.
Селекторы «Type» и «Date Range» имеют ту же структуру, что и раскрывающееся меню «Region».

В строках с 90 по 106 мы определяем компоненты dcc.Graph. Возможно, вы заметили, что по сравнению с предыдущей версией панели инструментов в компонентах отсутствует аргумент figure. Это связано с тем, что аргумент figure теперь будет генерироваться функцией обратного вызова с использованием входных данных, которые пользователь устанавливает с помощью селекторов «Region», «Type» и «Date Range».

Но пока что мы только определили, как пользователь будет взаимодействовать с нашим приложением. Теперь нам нужно заставить ваше приложение реагировать на действия пользователя. Для этого вы воспользуетесь функциями обратного вызова. Функции обратного вызова Dash — это обычные функции Python с декоратором app.callback. В Dash при изменении ввода запускается функция обратного вызова. Функция выполняет некоторые заранее определенные операции, такие как фильтрация набора данных, и возвращает результат в приложение. По сути, обратные вызовы связывают входы и выходы в вашем приложении. Сама функция обратного вызова, используемая для обновления графиков, представлена со строки 111 по 164.

В строках 111–119 мы определяем входы и выходы внутри декоратора app.callback. Сначала мы определяем выходы с помощью объектов вывода. Эти объекты принимают два аргумента:
1. Идентификатор элемента, который они изменят при выполнении функции.
2. Свойство изменяемого элемента.
Например, Output("price-chart", "figure") обновит свойство figure элемента «price-chart». Затем мы определяем входы с помощью объектов Input. Они также принимают два аргумента:
1. Идентификатор элемента, за изменениями которого они будут следить.
2. Свойство наблюдаемого элемента, которое они должны принимать, когда происходит изменение.
Итак, Input("region-filter", "value") будет следить за элементом «region-filter» на предмет изменений и примет его свойство value, если элемент изменится.

В строке 120 мы определяем функцию, которая будет применяться при изменении ввода. Здесь следует отметить, что аргументы функции будут соответствовать порядку объектов Input, переданных в обратный вызов. Нет явной связи между именами аргументов в функции и значениями, указанными во входных объектах.

Наконец, в строках 121–164 вы определяете тело функции. В этом случае функция принимает входные данные (region, type of avocado и date range), фильтрует данные и генерирует объекты-фигуры для графиков цен и объемов.
Результат:

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

Интерактивная визуализация данных с использованием Bokeh

Bokeh гордится тем, что является библиотекой для интерактивной визуализации данных. В отличие от популярных аналогов в области визуализации Python, таких как Matplotlib и Seaborn, Bokeh отображает свою графику с помощью HTML и JavaScript. Это делает его отличным кандидатом для создания веб-панелей и приложений. Тем не менее, это не менее мощный инструмент для изучения и понимания ваших данных или создания красивых пользовательских диаграмм для проекта, или отчета.

Создание первой фигуры

Прежде чем приступить к работе, нам необходимо, соответственно, установить определенные библиотеки, а именно: pandas, numpy и bokeh. Всего есть несколько способов сделать инфографику в Bokeh:
1.output_file(‘filename.html’) - запишет инфографику в статический HTML-файл.
2.output_notebook() - отобразит инфографику непосредственно в Jupyter Notebook.
Важно отметить, что ни одна из функций не отображает визуализацию. Этого не произойдет, пока не будет вызвана функция show(). Однако они гарантируют, что при вызове show() визуализация появится там, где вы хотите. И так как я буду работать в привычном для меня PyCharm, то я буду использовать первый способ. Давайте же создадим первую фигуру:

from bokeh.io import output_file
from bokeh.plotting import figure, show

# Рисунок будет отображен в статическом HTML-файле
output_file('Создание_первой_фигуры.html',
            title='Empty Bokeh Figure')

# Настроить общий объект figure()
fig = figure()

# Посмотрите, как это выглядит
show(fig)

Результат:

Как видите, новое окно браузера открылось с вкладкой под названием Empty Bokeh Figure и пустой фигурой, а сам созданный файл с именем «Создание_первой_фигуры.html» появился в папке с проектом.

Подготовка фигуры к данным

Теперь, когда вы знаете, как создавать и просматривать общую фигуру Bokeh в браузере, пора узнать больше о том, как настроить объект figure(). Объект figure() — это не только основа визуализации данных, но и объект, который открывает все доступные инструменты Bokeh для инфографики. Figure Bokeh является подклассом объекта Bokeh Plot, который предоставляет многие параметры, которые позволяют настраивать эстетические элементы инфографики. Чтобы вы могли получить представление о доступных параметрах настройки, давайте создадим самую уродливую инфографика на свете:

from bokeh.plotting import figure, show

output_file('Подготовка_фигуры_к_данным.html',
            title='Bokeh Figure')


fig = figure(background_fill_color='gray',
             background_fill_alpha=0.5,
             border_fill_color='blue',
             border_fill_alpha=0.25,
             plot_height=300,
             plot_width=500,
             x_axis_label='X Label',
             x_axis_type='datetime',
             x_axis_location='above',
             x_range=('2018-01-01', '2018-06-30'),
             y_axis_label='Y Label',
             y_axis_type='linear',
             y_axis_location='left',
             y_range=(0, 100),
             title='Example Figure',
             title_location='right',
             toolbar_location='below',
             tools='save')


show(fig)

Результат:

Рисование данных с помощью глифов

Пустая фигура не так уж и интересна, поэтому давайте посмотрим на глифы. Глиф — это векторизованная графическая форма или маркер, который используется для представления ваших данных в виде круга или квадрата. Начнем с очень простого примера, нарисовав несколько точек на координатной сетке x-y:

#РИСОВАНИЕ ДАННЫХ С ПОМОЩЬЮ ГЛИФОВ
#Создание первых данных
from bokeh.io import output_file
from bokeh.plotting import figure, show

# Мои данные о координатах x-y
x = [1, 2, 1]
y = [1, 1, 2]


output_file('Рисование_данных_с_помощью_глифов1.html', title='First Glyphs')

# Создайте фигуру без панели инструментов и диапазонов осей
fig = figure(title='My Coordinates',
             plot_height=300, plot_width=300,
             x_range=(0, 3), y_range=(0, 3),
             toolbar_location=None)

# Нарисуйте координаты в виде кругов
fig.circle(x=x, y=y,
           color='green', size=10, alpha=0.5)

# Показать сюжет
show(fig)

Результат:

Как только ваша фигура будет создана, вы сможете увидеть, как ее можно использовать для рисования данных координат x-y с использованием настраиваемых круговых глифов. Помимо этого, глифы можно комбинировать по мере необходимости, чтобы соответствовать вашим потребностям в визуализации. Допустим, я хочу создать визуализацию, которая показывает, сколько слов я написал за день, чтобы сделать этот руководство, с наложенной линией тренда совокупного количества слов:

#Создание вторых данных
import numpy as np
from bokeh.plotting import figure, show

# Мои данные о подсчете слов
day_num = np.linspace(1, 10, 10)
daily_words = [450, 628, 488, 210, 287, 791, 508, 639, 397, 943]
cumulative_words = np.cumsum(daily_words)


output_file('Рисование_данных_с_помощью_глифов2.html', title='First Glyphs')

# Создаем фигуру с осью x типа datetime
fig = figure(title='My Tutorial Progress',
             plot_height=400, plot_width=700,
             x_axis_label='Day Number', y_axis_label='Words Written',
             x_minor_ticks=2, y_range=(0, 6000),
             toolbar_location=None)

# Ежедневные слова будут представлены в виде вертикальных полос (столбцов)
fig.vbar(x=day_num, bottom=0, top=daily_words,
         color='blue', width=0.75,
         legend='Daily')

# Накопленная сумма будет линией тренда
fig.line(x=day_num, y=cumulative_words,
         color='gray', line_width=1,
         legend='Cumulative')

# Поместите легенду в левый верхний угол
fig.legend.location = 'top_left'

# Давайте проверим
show(fig)

Результат:

Чтобы объединить столбцы и линии на рисунке, они просто создаются с использованием одного и того же объекта figure (). Кроме того, вы можете увидеть выше, как легко можно создать легенду, задав свойство легенды для каждого глифа. Затем легенда была перемещена в верхний левый угол графика путем присвоения ‘top_left’ значению fig.legend.location.

Небольшая заметка о данных

Каждый раз, когда вы изучаете новую библиотеку визуализации, рекомендуется начинать с некоторых данных из знакомой вам области. Поэтому, в остальных примерах будут использоваться общедоступные данные из Kaggle, который содержит информацию о сезоне 2017-18 Национальной баскетбольной ассоциации (НБА), а именно:
•2017-18_playerBoxScore.csv: снимки статистики игроков по играм;
•2017-18_teamBoxScore.csv: снимки статистики команд по играм;
•2017-18_standings.csv: ежедневное командное положение и рейтинги.
Эти файлы я, соответственно, скачал и разместил в папку с проектом. После чего, они были прочитаны с помощью Pandas DataFrame, используя следующие команды:

import pandas as pd
# Прочитать csv файлы
player_stats = pd.read_csv('2017-18_playerBoxScore.csv', parse_dates=['gmDate'])
team_stats = pd.read_csv('2017-18_teamBoxScore.csv', parse_dates=['gmDate'])
standings = pd.read_csv('2017-18_standings.csv', parse_dates=['stDate'])
print(player_stats)

Этот фрагмент кода считывает данные из трех файлов CSV и автоматически интерпретирует столбцы даты как объекты datetime. Ну и print был применен для того, чтобы проверить, что он выведет, да и считывает ли данные в целом или нет. Результат:

Использование объекта ColumnDataSource

В приведенных выше примерах для представления данных использовались списки Python и массивы Numpy и Bokeh хорошо оснащено для обработки этих типов данных. Однако, когда дело доходит до данных в Python, вы, скорее всего, встретите словари Python и Pandas DataFrames, особенно если вы читаете данные из файла или внешнего источника данных. Bokeh хорошо оснащен для работы с этими более сложными структурами данных и даже имеет встроенные функции для их обработки, а именно ColumnDataSource. Вы можете спросить себя: «Зачем использовать ColumnDataSource, если Bokeh может напрямую взаимодействовать с другими типами данных?» Во-первых, ссылаетесь ли вы напрямую на список, массив, словарь или DataFrame, Bokeh за кулисами все равно превратит его в ColumnDataSource. Что еще более важно, ColumnDataSource значительно упрощает реализацию интерактивных возможностей Bokeh. ColumnDataSource лежит в основе передачи данных глифам, которые вы используете для визуализации.Его основная функция — отображать имена столбцам ваших данных. Это упрощает использование ссылок на элементы данных при построении визуализации. Это также упрощает то же самое для Bokeh при создании визуализации. Давайте начнем с визуализации гонки за первое место в Западной конференции НБА в 2017–2018 годах между действующим чемпионом Golden State Warriors и претендентом Houston Rockets. Ежедневные записи о победах и поражениях этих двух команд хранятся в DataFrame с именем west_top_2:

#ИСПОЛЬЗОВАНИЕ ОБЪЕКТА ColumnDataSource
#Пример 1
west_top_2 = (standings[(standings['teamAbbr'] == 'HOU') | (standings['teamAbbr'] == 'GS')]
              .loc[:, ['stDate', 'teamAbbr', 'gameWon']]
             .sort_values(['teamAbbr','stDate']))
print(west_top_2.head())

Результат:

Отсюда вы можете загрузить этот DataFrame в два объекта ColumnDataSource и визуализировать гонку:

from bokeh.plotting import figure, show
from bokeh.io import output_file
from bokeh.models import ColumnDataSource

# Output to file
output_file('west-top-2-standings-race1.html',
            title='Western Conference Top 2 Teams Wins Race')

# Isolate the data for the Rockets and Warriors
rockets_data = west_top_2[west_top_2['teamAbbr'] == 'HOU']
warriors_data = west_top_2[west_top_2['teamAbbr'] == 'GS']

# Create a ColumnDataSource object for each team
rockets_cds = ColumnDataSource(rockets_data)
warriors_cds = ColumnDataSource(warriors_data)

# Create and configure the figure
fig = figure(x_axis_type='datetime',
             plot_height=300, plot_width=600,
             title='Western Conference Top 2 Teams Wins Race, 2017-18',
             x_axis_label='Date', y_axis_label='Wins',
             toolbar_location=None)

# Render the race as step lines
fig.step('stDate', 'gameWon',
         color='#CE1141', legend='Rockets',
         source=rockets_cds)
fig.step('stDate', 'gameWon',
         color='#006BB6', legend='Warriors',
         source=warriors_cds)

# Move the legend to the upper left corner
fig.legend.location = 'top_left'

# Show the plot
show(fig)

Результат:

Обратите внимание на ссылки на соответствующие объекты ColumnDataSource при создании двух строк. Вы просто передаете исходные имена столбцов в качестве входных параметров и указываете, какой ColumnDataSource использовать через свойство источника. Визуализация показывает напряженную гонку на протяжении всего сезона, Warriors создают довольно большую подушку в середине сезона. Тем не менее, небольшой спад в конце сезона позволил Rockets наверстать упущенное и в конечном итоге превзойти защищающихся чемпионов и завершить сезон посевным номером один в Западной конференции.
В предыдущем примере были созданы два объекта ColumnDataSource, по одному из подмножества фрейма данных west_top_2. В следующем примере будет воссоздан тот же вывод из одного ColumnDataSource на основе всего west_top_2 с использованием GroupFilter, который создает представление для данных:

#Пример 2
from bokeh.plotting import figure, show
from bokeh.io import output_file
from bokeh.models import ColumnDataSource, CDSView, GroupFilter

# Вывод в файл
output_file('west-top-2-standings-race2.html',
            title='Western Conference Top 2 Teams Wins Race')

# Создать ColumnDataSource
west_cds = ColumnDataSource(west_top_2)

# Создайте представления для каждой команды
rockets_view = CDSView(source=west_cds,
                       filters=[GroupFilter(column_name='teamAbbr', group='HOU')])
warriors_view = CDSView(source=west_cds,
                        filters=[GroupFilter(column_name='teamAbbr', group='GS')])

# Создаем и настраиваем фигуру
west_fig = figure(x_axis_type='datetime',
                  plot_height=300, plot_width=600,
                  title='Western Conference Top 2 Teams Wins Race, 2017-18',
                  x_axis_label='Date', y_axis_label='Wins',
                  toolbar_location=None)

# Отрисовываем гонку в виде ступенчатых линий
west_fig.step('stDate', 'gameWon',
              source=west_cds, view=rockets_view,
              color='#CE1141', legend='Rockets')
west_fig.step('stDate', 'gameWon',
              source=west_cds, view=warriors_view,
              color='#006BB6', legend='Warriors')

# Переместите легенду в верхний левый угол
west_fig.legend.location = 'top_left'

# Показать сюжет
show(west_fig)

Результат:

Обратите внимание, как GroupFilter передается в CDSView в виде списка. Это позволяет вам комбинировать несколько фильтров вместе, чтобы при необходимости изолировать нужные данные от ColumnDataSource.

Организация нескольких визуализаций с помощью макетов

В турнирной таблице Восточной конференции в Атлантическом дивизионе остались два соперника: Boston Celtics и Toronto Raptors. Прежде чем повторить шаги, использованные для создания west_top_2, давайте попробуем еще раз протестировать ColumnDataSource, используя то, что вы узнали выше. В этом примере вы увидите, как передать весь DataFrame в ColumnDataSource и создать представления для изоляции соответствующих данных:

#ОРГАНИЗАЦИЯ НЕСКОЛЬКИХ ВИЗУАЛИЗАЦИЙ С ПОМОЩЬЮ МАКЕТОВ
from bokeh.plotting import figure, show
from bokeh.io import output_file
from bokeh.models import ColumnDataSource, CDSView, GroupFilter

# Вывод в файл
output_file('Организация_нескольких_визуализаций_с_помощью_макетов.html',
            title='Eastern Conference Top 2 Teams Wins Race')

# Создать ColumnDataSource
standings_cds = ColumnDataSource(standings)

# Create views for each team
celtics_view = CDSView(source=standings_cds,
                      filters=[GroupFilter(column_name='teamAbbr',
                                           group='BOS')])
raptors_view = CDSView(source=standings_cds,
                      filters=[GroupFilter(column_name='teamAbbr',
                                           group='TOR')])

# Создаем и настраиваем фигуру
east_fig = figure(x_axis_type='datetime',
           plot_height=300, plot_width=600,
           title='Eastern Conference Top 2 Teams Wins Race, 2017-18',
           x_axis_label='Date', y_axis_label='Wins',
           toolbar_location=None)

# Отрисовываем гонку в виде ступенчатых линий
east_fig.step('stDate', 'gameWon',
              color='#007A33', legend='Celtics',
              source=standings_cds, view=celtics_view)
east_fig.step('stDate', 'gameWon',
              color='#CE1141', legend='Raptors',
              source=standings_cds, view=raptors_view)

# Переместите легенду в верхний левый угол
east_fig.legend.location = 'top_left'

# Показать сюжет
show(east_fig)

Результат:

Добавление взаимодействия

Выбор точек данных

Реализовать поведение выбора так же просто, как добавить несколько конкретных ключевых слов при объявлении ваших глифов. В следующем примере создается диаграмма разброса, которая связывает общее количество попыток трехочковых бросков игроком с их процентом (для игроков, сделавших не менее 100 трехочковых бросков). Данные могут быть агрегированы из DataFrame player_stats:

#ДОБАВЛЕНИЕ ВЗАИМОДЕЙСТВИЯ
#ВЫБОР ТОЧЕК ДАННЫХ
# Найдите игроков, которые сделали хотя бы 1 трехочковый бросок за сезон
three_takers = player_stats[player_stats['play3PA'] > 0]

# Убрать имена игроков, поместив их в один столбец
three_takers['name'] = [f'{p["playFNm"]} {p["playLNm"]}'
                        for _, p in three_takers.iterrows()]

# Суммируйте общее количество трехочковых попыток и количество попыток для каждого игрока
three_takers = (three_takers.groupby('name')
                            .sum()
                            .loc[:,['play3PA', 'play3PM']]
                            .sort_values('play3PA', ascending=False))

# Отфильтровать всех, кто не сделал хотя бы 100 трехочковых бросков
three_takers = three_takers[three_takers['play3PA'] >= 100].reset_index()

# Добавить столбец с рассчитанным трехбалльным процентом (выполнено / предпринято)
three_takers['pct3PM'] = three_takers['play3PM'] / three_takers['play3PA']
print(three_takers.sample(5))

Результат:

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

# Выбор группы игроков в раздаче из полученного DataFrame, при этом отключить цвет глифов
from bokeh.plotting import figure, show
from bokeh.io import output_file
from bokeh.models import ColumnDataSource, NumeralTickFormatter

# Файл вывода
output_file('Выбор_точек_данных.html',
            title='Three-Point Attempts vs. Percentage')

# Сохраним данные в ColumnDataSource
three_takers_cds = ColumnDataSource(three_takers)

# Укажите инструменты выбора, которые будут доступны
select_tools = ['box_select', 'lasso_select', 'poly_select', 'tap', 'reset']

# Создание картинки
fig = figure(plot_height=400,
             plot_width=600,
             x_axis_label='Three-Point Shots Attempted',
             y_axis_label='Percentage Made',
             title='3PT Shots Attempted vs. Percentage Made (min. 100 3PA), 2017-18',
             toolbar_location='below',
             tools=select_tools)

# Отформатируйте метки делений оси Y в процентах
fig.yaxis[0].formatter = NumeralTickFormatter(format='00.0%')

# Добавить квадрат, представляющий каждого игрока
fig.square(x='play3PA',
           y='pct3PM',
           source=three_takers_cds,
           color='royalblue',
           selection_color='deepskyblue',
           nonselection_color='lightgray',
           nonselection_alpha=0.3)

# Показать результат
show(fig)

Сначала укажите инструменты выбора, которые вы хотите сделать доступными. В приведенном выше примере «box_select», «lasso_select», «poly_select» и «tap» (плюс кнопка сброса) были указаны в списке под названием select_tools. Когда фигура создается, панель инструментов располагается «below» («ниже») графика, и список передается в инструменты, чтобы сделать инструменты, выбранные выше, доступными. Каждый игрок изначально представлен синим квадратным глифом, но при выборе игрока или группы игроков устанавливаются следующие конфигурации:
• Превратите выбранных игроков в темно-синий
• Измените глифы всех невыделенных игроков на светло-серый цвет с непрозрачностью 0,3
Результат:

Добавление действий при наведении курсора

Таким образом, реализована возможность выбора конкретных точек данных игроков, которые кажутся интересными на моем графике разброса, но что, если вы хотите быстро увидеть, каких отдельных игроков представляет глиф? Один из вариантов — использовать Bokeh HoverTool (), чтобы показывать всплывающую подсказку, когда курсор пересекает пути с глифом. Все, что вам нужно сделать, это добавить в приведенный выше фрагмент кода следующее:

#ДОБАВЛЕНИЕ ДЕЙСТВИЙ ПРИ НАВЕДЕНИИ КУРСОРА
from bokeh.models import HoverTool

output_file('Добавление_действий_при_наведении_курсора.html',
            title='Three-Point Attempts vs. Percentage')

# Отформатируйте всплывающую подсказку
# Отформатируйте всплывающую подсказку
tooltips = [
            ('Player','@name'),
            ('Three-Pointers Made', '@play3PM'),
            ('Three-Pointers Attempted', '@play3PA'),
            ('Three-Point Percentage','@pct3PM{00.0%}'),
           ]

# Настроить рендерер, который будет использоваться при наведении
hover_glyph = fig.circle(x='play3PA', y='pct3PM', source=three_takers_cds,
                         size=15, alpha=0,
                         hover_fill_color='black', hover_alpha=0.5)

# Добавляем HoverTool к фигуре
fig.add_tools(HoverTool(tooltips=tooltips, renderers=[hover_glyph]))

# Показать
show(fig)

HoverTool() немного отличается от инструментов выбора, которые вы видели выше, тем, что у него есть свойства, в частности всплывающие подсказки. Во-первых, вы можете настроить отформатированную всплывающую подсказку, создав список кортежей, содержащих описание и ссылку на ColumnDataSource. Этот список был передан в качестве входных данных в HoverTool(), а затем просто добавлен к рисунку с помощью add_tools(). Для большего подчеркивания игроков при наведении, был создан новый глиф, в данном случае кружков вместо квадратов, и присвоения ему hover_glyph. Обратите внимание, что начальная непрозрачность установлена на ноль, поэтому она невидима, пока курсор не коснется ее. Свойства, которые появляются при наведении курсора, фиксируются путем установки hover_alpha значения 0,5 вместе с hover_fill_color. Теперь вы увидите, как маленький черный кружок появляется над исходным квадратом при наведении курсора на различные маркеры. Вот что произошло:

Выделение данных с помощью легенды

В этом примере вы увидите две идентичные диаграммы разброса, на которых сравниваются игровые очки и подборы Леброна Джеймса и Кевина Дюранта. Единственная разница будет заключаться в том, что один будет использовать hide в качестве своей click_policy, а другой — mute. Первый шаг — настроить вывод и настроить данные, создав представление для каждого игрока из кадра данных player_stats:

#ВЫДЕЛЕНИЕ ДАННЫХ С ПОМОЩЬЮ ЛЕГЕНДЫ
from bokeh.plotting import figure, show
from bokeh.io import output_file
from bokeh.models import ColumnDataSource, CDSView, GroupFilter
from bokeh.layouts import row

# Вывести в записной книжке
output_file('lebron-vs-durant.html',
            title='LeBron James vs. Kevin Durant')

# Хранить данные в ColumnDataSource
player_gm_stats = ColumnDataSource(player_stats)

# Создайте представление для каждого игрока
lebron_filters = [GroupFilter(column_name='playFNm', group='LeBron'),
                  GroupFilter(column_name='playLNm', group='James')]
lebron_view = CDSView(source=player_gm_stats,
                      filters=lebron_filters)

durant_filters = [GroupFilter(column_name='playFNm', group='Kevin'),
                  GroupFilter(column_name='playLNm', group='Durant')]
durant_view = CDSView(source=player_gm_stats,
                      filters=durant_filters)

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

# Объединить общие аргументы ключевых слов в dicts
common_figure_kwargs = {
    'plot_width': 400,
    'x_axis_label': 'Points',
    'toolbar_location': None,
}
common_circle_kwargs = {
    'x': 'playPTS',
    'y': 'playTRB',
    'source': player_gm_stats,
    'size': 12,
    'alpha': 0.7,
}
common_lebron_kwargs = {
    'view': lebron_view,
    'color': '#002859',
    'legend': 'LeBron James'
}
common_durant_kwargs = {
    'view': durant_view,
    'color': '#FFC324',
    'legend': 'Kevin Durant'
}

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

# Создайте две фигуры и нарисуйте данные
hide_fig = figure(**common_figure_kwargs,
                  title='Click Legend to HIDE Data',
                  y_axis_label='Rebounds')
hide_fig.circle(**common_circle_kwargs, **common_lebron_kwargs)
hide_fig.circle(**common_circle_kwargs, **common_durant_kwargs)

mute_fig = figure(**common_figure_kwargs, title='Click Legend to MUTE Data')
mute_fig.circle(**common_circle_kwargs, **common_lebron_kwargs,
                muted_alpha=0.1)
mute_fig.circle(**common_circle_kwargs, **common_durant_kwargs,
                muted_alpha=0.1)

Обратите внимание, что у mute_fig есть дополнительный параметр под названием muted_alpha. Этот параметр управляет непрозрачностью маркеров, когда отключение звука используется как click_policy. Наконец, устанавливается click_policy для каждой фигуры, и они отображаются в горизонтальной конфигурации:

# Добавляем интерактивности в легенду
hide_fig.legend.click_policy = 'hide'
mute_fig.legend.click_policy = 'mute'

# Визуализировать
show(row(hide_fig, mute_fig))

Результат:

После того, как легенда размещена, все, что вам нужно сделать, это назначить либо скрыть, либо отключить для свойства фигуры click_policy. Это автоматически превратит вашу базовую легенду в интерактивную легенду. Также обратите внимание, что специально для отключения звука дополнительное свойство muted_alpha было установлено в соответствующих круговых глифах для Леброна Джеймса и Кевина Дюранта. Это определяет визуальный эффект, обусловленный взаимодействием легенды.

Очистка данных с помощью Pandas и NumPy

Специалисты по обработке данных тратят много времени на очистку наборов данных и приведение их в форму, с которой они могут работать. Фактически, многие специалисты по данным утверждают, что начальные шаги по получению и очистке данных составляют 80% работы. Именно об этом мы и поговорим в данной главе. Но перед этим, нам необходимо установить и импортировать две библиотеки:

import pandas as pd
import numpy as np

Кроме этого, в проекте мы создаем папку Datasets, куда размещаем наши исходные файлы, с которыми будет происходить работа. В моем случае это: BL-Flickr-Images-Book.csv, olympics.csv и university_towns.txt. Проделав все эти действия, можно приступать к работе.

Удаление столбцов в DataFrame

Часто вы обнаруживаете, что не все категории данных в наборе данных вам нужны. Именно поэтому, библиотека Pandas предоставляет удобный способ удаления ненужных столбцов или строк из DataFrame с помощью функции drop(). Давайте посмотрим на простой пример, в котором мы удаляем несколько столбцов из DataFrame. В приведенных ниже примерах мы передаем относительный путь к pd.read_csv, что означает, что все наборы данных находятся в папке с именем Datasets в нашем текущем рабочем каталоге:

df = pd.read_csv('Datasets/BL-Flickr-Images-Book.csv')
print('Вывод загруженного csv файла:')
print(df.head())

Результат:

Сделав вывод нашего csv файла мы видим, что несколько столбцов предствляют собой вспомогательную информацию, которая была бы полезна для библиотеки, но не очень для описания самой книги: Edition Statement, Corporate Author, Corporate Contributors, Former owner, Engraver, Issuance type и Shelfmarks. Эту информацию мы можем удалить следующим образом:

to_drop = ['Edition Statement',
           'Corporate Author',
           'Corporate Contributors',
           'Former owner',
            'Engraver',
           'Contributors',
            'Issuance type',
           'Shelfmarks']

df.drop(to_drop, inplace=True, axis=1)
print('Вывод csv файла с удаленными столбцами:')
print(df.head())

Сначала мы определили список, который содержит имена всех столбцов, которые мы хотим удалить. Затем мы вызываем функцию drop() для нашего объекта, передавая параметр inplace как True и параметр оси как 1, что говорит Pandas об изменениях непосредственно в нашем объекте и что он должен искать значения, которые будут отброшены в столбцах объекта. Результат:

Изменение индекса фрейма данных

Индекс Pandas расширяет функциональность массивов NumPy, чтобы обеспечить более гибкое нарезание и маркировку. Во многих случаях полезно использовать однозначное идентифицирующее поле данных в качестве индекса. Давайте заменим существующий индекс в BL-Flickr-Images-Book.csv столбцом Identifier, используя set_index:

df = df.set_index('Identifier')
print(' Замена существующего индекса столбцом Identifier:')
print(df.head())

Результат:

Кроме этого, мы можем получить доступ к каждой записи простым способом с помощью loc[]. Хотя loc[] может не иметь всего этого интуитивно понятного имени, он позволяет нам выполнять индексацию на основе меток, которая представляет собой маркировку строки или записи независимо от ее положения:

print('Получение доступа к каждой записи:')
print(df.loc[206])

Результат:

Другими словами, 206 — это первая метка индекса. Ранее нашим индексом был RangeIndex: целые числа, начинающиеся с 0, аналог встроенного диапазона Python. Передав имя столбца в set_index, мы изменили индекс на значения в Identifier.

Очистка полей в данных

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

print('Вывод поля даты публикации для того, чтобы мы могли выполнять вычисления в будущем')
print(df.loc[1905:, 'Date of Publication'].head(10))

Результат:

Как известно, у конкретной книги может быть только одна дата публикации. Поэтому нам необходимо удалить лишние даты в квадратных скобках, преобразовать диапазоны дат в их «дату начала», полностью удалить даты, в которых мы не уверены и преобразовать строку nan в значение NaN NumPy. Для этого мы будем использовать следующее регулярное выражение: regex = r'^(\d{4})'. Данное выражение предназначено для поиска любых четырех цифр в начале строки, чего достаточно для нашего случая. Это необработанная строка, что является стандартной практикой с регулярными выражениями. \d представляет любую цифру, а {4} повторяет это правило четыре раза. Символ ^ соответствует началу строки, а круглые скобки обозначают группу захвата, которая сигнализирует Pandas, что мы хотим извлечь эту часть регулярного выражения. Сам код:

extr = df['Date of Publication'].str.extract(r'^(\d{4})', expand=False)
print('Модернизированные поля даты публикации:')
print(extr.head())

Результат:

Объединение методов str с NumPy для очистки столбцов

Для начала, давайте выведем содержимое столбца Place of Publication:

print('Вывод содержимого столбца Place of Publication')
print(df['Place of Publication'].head(10))

Результат:

Мы видим, что для некоторых строк место публикации окружено другой ненужной информацией. Если бы мы посмотрели на большее количество значений, мы бы увидели, что это справедливо только для некоторых строк, место публикации которых — ‘London’ или ‘Oxford’. Давайте взглянем на две конкретные записи:

print('Вывод информации о двух конкретных записях:')
print(df.loc[4157862])
print(df.loc[4159587])

Результат:

Эти две книги были изданы в одном месте, но одна имеет дефис в названии места, а другая — нет. Чтобы очистить этот столбец за один проход, мы можем использовать str.contains() для получения логической маски. Чистим колонку следующим образом:

pub = df['Place of Publication']
london = pub.str.contains('London')
print('Вывод очищенной колонки:')
print(london[:5])
oxford = pub.str.contains('Oxford')

Далее объединяем их с помощью np.where:

df['Place of Publication'] = np.where(london, 'London',
                                      np.where(oxford, 'Oxford',
                                               pub.str.replace('-', ' ')))
print('Объединение с помощью  np.where')
print(df['Place of Publication'].head())

Результат:

Здесь функция np.where вызывается во вложенной структуре с условием, представляющим собой серию логических значений, полученных с помощью str.contains(). Метод contains() работает аналогично встроенному ключевому слову in, используемому для поиска вхождения объекта в итерируемом объекте (или подстроке в строке). Используемая замена — это строка, представляющая желаемое место публикации. Мы также заменяем дефисы пробелом с помощью str.replace() и переназначаем столбец в нашем DataFrame.

Очистка всего набора данных с помощью функции applymap

В определенных ситуациях вы увидите, что «грязь» не локализована в одном столбце, а более разбросана. В некоторых случаях было бы полезно применить настраиваемую функцию к каждой ячейке или элементу DataFrame. Метод Pandas .applymap() похож на метод in-построил функцию map() и просто применяет функцию ко всем элементам в DataFrame. Давайте посмотрим на пример. Мы создадим DataFrame из ранее добавленного в проект файла «university_towns.txt»:

$ head Datasets/univerisity_towns.txt
Alabama[edit]
Auburn (Auburn University)[1]
Florence (University of North Alabama)
Jacksonville (Jacksonville State University)[2]
Livingston (University of West Alabama)[2]
Montevallo (University of Montevallo)[2]
Troy (Troy University)[2]
Tuscaloosa (University of Alabama, Stillman College, Shelton State)[3][4]
Tuskegee (Tuskegee University)[5]
Alaska[edit]

Мы видим, что у нас есть периодические названия штатов, за которыми следуют университетские города в этом штате: StateA TownA1 TownA2 StateB TownB1 TownB2 …. Если мы посмотрим на то, как названия штатов записаны в файле, мы увидим, что все они имеют в них подстрока [edit]. Мы можем воспользоваться этим шаблоном, создав список (state, city) кортежи и обертывание этого списка в DataFrame:

university_towns = []
with open('Datasets/university_towns.txt') as file:
     for line in file:
         if '[edit]' in line:

             state = line
         else:

             university_towns.append((state, line))
print('Вывод созданного списка, преобразованного в DataFrame:')
print(university_towns[:5])

Результат:

Мы можем обернуть этот список в DataFrame и установить столбцы как «State» и «RegionName». Pandas возьмет каждый элемент в списке и установит State на левое значение, а RegionName — на правое значение:

towns_df = pd.DataFrame(university_towns,
                         columns=['State', 'RegionName'])
print('Вывод результирующего DataFrame:')
print(towns_df.head())

Результат:

Хотя мы могли бы очистить эти строки в цикле for выше, Pandas упрощает это. Нам нужно только название штата и название города, а все остальное можно удалить. Хотя здесь мы могли бы снова использовать методы Pandas .str(), мы также могли бы использовать applymap() для сопоставления вызываемого Python с каждым элементом DataFrame.

Переименование столбцов и пропуск строк

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

$ head -n 5 Datasets/olympics.csv
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15
,? Summer,01 !,02 !,03 !,Total,? Winter,01 !,02 !,03 !,Total,? Games,01 !,02 !,03 !,Combined total
Afghanistan (AFG),13,0,0,2,2,0,0,0,0,0,13,0,0,2,2
Algeria (ALG),12,5,2,8,15,3,0,0,0,0,15,5,2,8,15
Argentina (ARG),23,18,24,28,70,18,0,0,0,0,41,18,24,28,70

Теперь мы прочитаем его в DataFrame Pandas:

olympics_df = pd.read_csv('Datasets/olympics.csv')
print('Вывод olympics.csv:')
print(olympics_df.head())

Результат:

Это действительно грязно! Поэтому, мы должны пропустить одну строку и установить заголовок как первую (с нулевым индексом) строку и переименовать столбцы. Для того, чтобы удалить 0-ю строку мы используем:

olympics_df = pd.read_csv('Datasets/olympics.csv', header=1)
print('Вывод olympics.csv без 0 строки:')
print(olympics_df.head())

Результат:

Теперь у нас есть правильная строка, установленная в качестве заголовка, и все ненужные строки удалены. Обратите внимание на то, как Pandas изменил имя столбца, содержащего названия стран, с NaN на Unnamed: 0. Чтобы переименовать столбцы, мы будем использовать метод rename() DataFrame, который позволяет вам изменить метку оси на основе сопоставления (в данном случае dict). Начнем с определения словаря, который сопоставляет текущие имена столбцов (как ключи) с более удобными (значениями словаря):

new_names =  {'Unnamed: 0': 'Country',
              '? Summer': 'Summer Olympics',
              '01 !': 'Gold',
               '02 !': 'Silver',
              '03 !': 'Bronze',
               '? Winter': 'Winter Olympics',
               '01 !.1': 'Gold.1',
              '02 !.1': 'Silver.1',
               '03 !.1': 'Bronze.1',
               '? Games': '# Games',
               '01 !.2': 'Gold.2',
              '02 !.2': 'Silver.2',
              '03 !.2': 'Bronze.2'}

Далее вызываем функцию rename() для нашего объекта:

olympics_df.rename(columns=new_names, inplace=True)

Установка inplace в True указывает, что наши изменения будут внесены непосредственно в объект. Результат: