Изучаем Python. Программирование игр, визуализация данных, веб-приложения

Мэтиз Эрик

Проект 2. Визуализация данных

 

15. Генерирование данных

Под визуализацией данных понимается исследование данных через их визуальное представление. Визуализация тесно связана с анализом данных (data mining), использующим программный код для изучения закономерностей и связей в наборе данных. Набором данных может быть как маленький список чисел, помещающийся в одной строке кода, так и массивом из многих гигабайт.

Качественное представление данных не сводится к красивой картинке. Если для набора данных подобрано простое, визуально привлекательное представление, его смысл становится очевидным для зрителя. Люди замечают в наборе данных закономерности, о которых они и не подозревали.

К счастью, для визуализации сложных данных не нужен суперкомпьютер. Благодаря эффективности Python вы сможете быстро исследовать наборы данных из миллионов отдельных элементов данных (точек данных) на обычном ноутбуке. Элементы данных даже не обязаны быть числовыми. Приемы, о которых вы узнали в первой части книги, позволят вам проанализировать даже нечисловые данные.

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

Также в этом проекте будет использоваться пакет Pygal, ориентированный на ­создание визуализаций, хорошо работающих с цифровыми устройствами. С помощью Pygal можно выделять и изменять размеры элементов в ходе взаимодействия пользователя с визуализацией; кроме того, размер визуализации легко изменяется под крошечные «умные часы» или гигантский монитор. Мы используем Pygal для исследования закономерностей различных бросков кубиков.

 

Установка matplotlib

Сначала необходимо установить библиотеку matplotlib, которая будет использоваться в исходном наборе визуализаций. Если вы еще не использовали про­грамму pip, обращайтесь к разделу «Установка пакетов Python с использованием pip» (с. 227).

 

В Linux

Если вы используете версию Python, входящую в поставку системы, вы сможете использовать менеджер пакетов своей системы для установки matplotlib всего одной командой:

$ sudo apt-get install python3-matplotlib

Если вы используете Python 2.7, команда выглядит так:

$ sudo apt-get install python-matplotlib

Если у вас установлена более новая версия Python, вам придется установить еще несколько библиотек, от которых зависит matplotlib:

$ sudo apt-get install python3.5-dev python3.5-tk tk-dev

$ sudo apt-get install libfreetype6-dev g++

Затем программа pip используется для установки matplotlib:

$ pip install --user matplotlib

 

В OS X

Компания Apple включает matplotlib в свою стандартную установку Python. Чтобы проверить, установлена ли библиотека в вашей системе, откройте терминальный сеанс и попробуйте импортировать matplotlib. Если библиотека matplotlib еще не установлена в системе и вы использовали Homebrew для установки Python, установите ее следующей командой:

$ pip install --user matplotlib

ПРИМЕЧАНИЕ

Возможно, при установке пакетов вам придется использовать команду pip3 вместо pip. Если же эта команда не работает, попробуйте исключить флаг --user.

 

В Windows

В системе Windows сначала необходимо установить Visual Studio. Откройте страницу https://dev.windows.com/, щелкните на ссылке Скачать средства (Downloads) и найдите Visual Studio Community — бесплатный набор средств разработчика для Windows. Загрузите и запустите программу установки.

Затем вам понадобится программа установки для matplotlib. Обратитесь по адресу https://pypi.python.org/pypi/matplotlib/ и найдите файл с расширением .whl, соответствующий используемой версии Python. Например, если вы ­используете 32-разрядную версию Python 3.5, загрузите файл matplotlib-1.4.3-cp35-none-win32.whl.

Примечание

Если вы не нашли файл, соответствующий используемой версии Python, просмотрите возможные варианты по адресу http://www.lfd.uci.edu/~gohlke/pythonlibs/#matplotlib. На этом сайте новые пакеты появляются немного ранее, чем на официальном сайте matplotlib.

Скопируйте файл .whl в каталог проекта, откройте окно командной строки и ­перейдите в каталог проекта. Используйте pip для установки matplotlib:

> cd python_work

python_work> python -m pip install --user matplotlib-1.4.3-cp35-none-win32.whl

 

Тестирование matplotlib

После того как необходимые пакеты будут установлены, протестируйте свою установку. Для этого откройте сеанс командной строки командой python или python3 и импортируйте matplotlib:

$ python3

>>> import matplotlib

>>>

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

Примечание

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

 

Галерея matplotlib

Чтобы получить представления о визуализациях, которые можно строить в matplotlib, посетите галерею на сайте http://matplotlib.org/. Щелкая на визуализации в галерее, вы сможете просмотреть код, использованный для ее построения.

 

Построение простого графика

Начнем с построения простого линейного графика с использованием matplotlib, а затем настроим его для более содержательной визуализации данных. В качестве данных для графика будет использоваться последовательность квадратов 1, 4, 9, 16 и 25.

Передайте matplotlib числа так, как показано ниже, а matplotlib сделает все остальное:

mpl_squares.py

import matplotlib.pyplot as plt

squares = [1, 4, 9, 16, 25]

plt.plot(squares)

plt.show()

Сначала импортируйте модуль pyplot с псевдонимом plt, чтобы вам не приходилось многократно вводить имя pyplot. (Это сокращение часто встречается в примерах на сайте, поэтому мы поступим так же.) Модуль pyplot содержит ряд функций для построения диаграмм и графиков.

Мы создаем список для хранения квадратов и передаем его функции plot(), которая пытается построить осмысленное графическое представление для заданных чисел. Вызов plt.show() открывает окно просмотра matplotlib и выводит график (рис. 15.1). В окне просмотра можно изменять масштаб и перемещаться по построенному графику, а кнопка с диском позволяет сохранить любое изображение по вашему выбору.

Рис. 15.1. Пример простейшего графика в matplotlib

 

Изменение типа надписей и толщины графика

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

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

mpl_squares.py

import matplotlib.pyplot as plt

squares = [1, 4, 9, 16, 25]

(1) plt.plot(squares, linewidth=5)

# Назначение заголовка диаграммы и меток осей.

(2)plt.title("Square Numbers", fontsize=24)

(3)plt.xlabel("Value", fontsize=14)

plt.ylabel("Square of Value", fontsize=14)

# Назначение размера шрифта делений на осях.

(4)plt.tick_params(axis='both', labelsize=14)

plt.show()

Параметр linewidth (1) управляет толщиной линии, которая строится вызовом plot(). Функция title() (2) назначает заголовок диаграммы. Параметры fontsize, неоднократно встречающиеся в коде, управляют размером текста диаграммы.

Функции xlabel() и ylabel() позволяют назначить метки (заголовки) каждой из осей (3), а функция tick_params() определяет оформление делений на осях (4). Аргументы, использованные в данном примере, относятся к делениям на ­обоих осях (axes='both') и устанавливают для меток делений размер шрифта 14 (labelsize=14).

Как видно из рис. 15.2, график выглядит гораздо лучше. Текст надписей стал крупнее, а линия графика — толще.

Рис. 15.2. График выглядит гораздо лучше

 

Корректировка графика

Теперь, когда текст на графике стал нормально читаться, мы видим, что данные помечены неправильно. Обратите внимание: для точки 4,0 в конце графика указан квадрат 25! Давайте исправим эту ошибку.

Если plot() передает числовую последовательность, функция считает, что первый элемент данных соответствует координате x со значением 0, но в нашем примере первая точка соответствует значению 1. Чтобы переопределить значение по умолчанию, передайте plot() как входные значения, так и квадраты:

mpl_squares.py

import matplotlib.pyplot as plt

input_values = [1, 2, 3, 4, 5]

squares = [1, 4, 9, 16, 25]

plt.plot(input_values, squares, linewidth=5)

# Назначение заголовка диаграммы и меток осей.

...

Теперь plot() правильно строит график, потому что мы предоставили оба набора значений, и функции не нужно предполагать, как был сгенерирован выходной набор чисел. На рис. 15.3 изображен правильный график.

Рис. 15.3. График с правильными данными

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

 

Нанесение и оформление отдельных точек функцией scatter()

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

Для нанесения на диаграмму отдельной точки используется функция scatter(). Передайте scatter() координаты (x, y) нужной точки, и функция нанесет эти значения на диаграмму:

scatter_squares.py

import matplotlib.pyplot as plt

plt.scatter(2, 4)

plt.show()

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

import matplotlib.pyplot as plt

(1) plt.scatter(2, 4, s=200)

# Назначение заголовка диаграммы и меток осей.

plt.title("Square Numbers", fontsize=24)

plt.xlabel("Value", fontsize=14)

plt.ylabel("Square of Value", fontsize=14)

# Назначение размера шрифта делений на осях.

plt.tick_params(axis='both', which='major', labelsize=14)

plt.show()

В точке (1) вызывается функция; аргумент s задает размер точек, используемых для рисования диаграммы. Если запустить программу scatter_squares.py в текущем состоянии, вы увидите одну точку в середине диаграммы (рис. 15.4).

Рис. 15.4. Вывод одной точки

 

Вывод серии точек функцией scatter()

Чтобы вывести на диаграмме серию точек, передайте scatter() списки значений координат x и y:

scatter_squares.py

import matplotlib.pyplot as plt

x_values = [1, 2, 3, 4, 5]

y_values = [1, 4, 9, 16, 25]

plt.scatter(x_values, y_values, s=100)

# Назначение заголовка диаграммы и меток осей.

...

Список x_values содержит числа, возводимые в квадрат, а в y_values содержатся квадраты. При передаче этих списков scatter() библиотека matplotlib читает по одному значению из каждого списка и наносит их на диаграмму как точку. ­Таким образом, на диаграмму будут нанесены точки (1, 1), (2, 4), (3, 9), (4, 16) и (5, 25); результат показан на рис. 15.5.

Рис. 15.5. Точечная диаграмма с несколькими точками

 

Автоматическое вычисление данных

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

scatter_squares.py

import matplotlib.pyplot as plt

(1) x_values = list(range(1, 1001))

y_values = [x**2 for x in x_values]

(2)plt.scatter(x_values, y_values, s=40)

# Назначение заголовка диаграммы и меток осей.

...

# Назначение диапазона для каждой оси.

w plt.axis([0, 1100, 0, 1100000])

plt.show()

Все начинается со списка значений координаты x с числами от 1 до 1000 (1) . Затем генератор списка строит значения y, перебирая значения x (for x in x_values), возводя каждое число в квадрат (x**2) и сохраняя результаты в y_values. Затем оба списка (входной и выходной) передаются scatter() (2).

Набор данных достаточно велик, поэтому мы выбираем меньший размер шрифта, а функция axis() используется для задания диапазона каждой оси (3). Функция axis() получает четыре значения: минимум и максимум по осям x и y. В данном случае по оси x откладывается диапазон от 0 до 1100, а по оси y — диапазон от 0 до 1 100 000. На рис. 15.6 показан результат.

Рис. 15.6. Диаграмма с 1000 точками строится так же легко, как и диаграмма с 5 точками

 

Удаление контуров точек

Библиотека matplotlib позволяет раскрашивать точки на точечной диаграмме в разные цвета. Оформление по умолчанию — синие точки с черным контуром — хорошо подходит для диаграмм с несколькими точками. Однако при большом количестве точек черные контуры начинают сливаться. Чтобы удалить контуры вокруг точек, передайте аргумент edgecolor='none' при вызове scatter():

plt.scatter(x_values, y_values, edgecolor='none', s=40)

Запустите scatter_squares.py с этим вызовом, и вы увидите, что на диаграмме выводятся только заполненные синие точки.

 

Определение пользовательских цветов

Чтобы изменить цвет точек, передайте scatter() аргумент c с именем используемого цвета:

plt.scatter(x_values, y_values, c='red', edgecolor='none', s=40)

Также возможно определять пользовательские цвета в цветовой модели RGB. Чтобы определить цвет, передайте аргумент c с кортежем из трех дробных значений (для красной, зеленой и синей составляющих) в диапазоне от 0 до 1. Например, следующая строка создает диаграмму со светло-синими точками:

plt.scatter(x_values, y_values, c=(0, 0, 0.8), edgecolor='none', s=40)

Значения, близкие к 0, дают более темные цвета, а со значениями, близкими к 1, цвета получаются более светлыми.

 

Цветовые карты

Цветовая карта (colormap) представляет собой серию цветов градиента, определяющую плавный переход от начального цвета к конечному. Цветовые карты используются в визуализациях для выделения закономерностей в данных. Например, малые значения можно обозначить светлыми цветами, а большие — темными.

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

scatter_squares.py

import matplotlib.pyplot as plt

x_values = list(range(1001))

y_values = [x**2 for x in x_values]

plt.scatter(x_values, y_values, c=y_values, cmap=plt.cm.Blues,

. .edgecolor='none', s=40)

# Назначение заголовка диаграммы и меток осей.

...

Мы передаем в c список y-values, а затем указываем pyplot, какая цветовая карта должна использоваться, при помощи аргумента cmap. Следующий код окрашивает точки с меньшими значениями y в светло-синий цвет, а точки с бульшими значениями y — в темно-синий цвет. Полученная диаграмма изображена на рис. 15.7.

Примечание

Все цветовые карты, доступные в pyplot, можно просмотреть на сайте http://matplotlib.org/; откройте раздел Examples, прокрутите содержимое до пункта Color Examples и щелкните на ссылке colormaps_reference.

Рис. 15.7. Точечная диаграмма с цветовой картой Blues

 

Автоматическое сохранение диаграмм

Если вы хотите, чтобы программа автоматически сохраняла диаграмму в файле, замените вызов plt.show() вызовом plt.savefig():

plt.savefig('squares_plot.png', bbox_inches='tight')

Первый аргумент содержит имя файла для сохранения диаграммы; файл будет сохранен в одном каталоге с scatter_squares.py. Второй аргумент отсекает от диаграммы лишние пропуски. Если вы хотите оставить пустые места вокруг диаграммы, этот аргумент можно опустить.

Упражнения

15-1. Кубы: число, возведенное в третью степень, называется «кубом». Нанесите на диаграмму первые пять кубов, а затем первые 5000 кубов.

15-2. Цветные кубы: примените цветовую карту к диаграмме с кубами.

 

Случайное блуждание

В этом разделе мы используем Python для генерирования данных для случайного обхода, а затем при помощи matplotlib создадим привлекательное представление сгенерированных данных. Случайным блужданием (random walk) называется путь, который не имеет четкого направления, но определяется серией полностью случайных решений. Представьте, что муравей сошел с ума и делает каждый новый шаг в случайном направлении; его путь напоминает случайное блуждание.

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

 

Создание класса RandomWalk()

Чтобы создать путь случайного блуждания, мы создадим класс RandomWalk, который принимает случайные решения по выбору направления. Классу нужны три ­атрибута: переменная для хранения количества точек в пути и два списка для координат x и y каждой точки.

Класс RandomWalk содержит всего два метода: __init__() и fill_walk() для вычисления точек случайного блуждания. Начнем с метода __init__():

random_walk.py

(1) from random import choice

class RandomWalk():

. ."""Класс для генерирования случайных блужданий."""

. .

(2) . .def __init__(self, num_points=5000):

. . . ."""Инициализирует атрибуты блуждания."""

. . . .self.num_points = num_points

. . . .

. . . .# Все блуждания начинаются с точки (0, 0).

(3) . . . .self.x_values = [0]

. . . .self.y_values = [0]

Чтобы принимать случайные решения, мы сохраним возможные варианты в списке и используем функцию choice() для принятия решения (1) . Затем для списка устанавливается количество точек по умолчанию равным 5000 — достаточно большим, чтобы генерировать интересные закономерности, но достаточно малым, чтобы блуждания генерировались быстро (2). Затем в точке (3) создаются два списка для хранения значений x и y, после чего каждый путь начинается с точки (0, 0).

 

Выбор направления

Метод fill_walk(), как показано ниже, заполняет путь точками и определяет направление каждого шага. Добавьте этот метод в random_walk.py:

random_walk.py

def fill_walk(self):

. ."""Вычисляет все точки блуждания."""

. .

. .# Шаги генерируются до достижения нужной длины.

(1) . .while len(self.x_values) < self.num_points:

. . . . . .# Определение направления и длины перемещения.

(2) . . . . . .x_direction = choice([1, -1])

. . . . . .x_distance = choice([0, 1, 2, 3, 4])

(3) . . . . . .x_step = x_direction * x_distance

. . . . . .y_direction = choice([1, -1])

. . . . . .y_distance = choice([0, 1, 2, 3, 4])

(4) . . . . . .y_step = y_direction * y_distance

. . . .

. . . . . .# Отклонение нулевых перемещений.

(5) . . . . . .if x_step == 0 and y_step == 0:

. . . . . . . .continue

. . . .

. . . . . .# Вычисление следующих значений x и y.

? . . . . . .next_x = self.x_values[-1] + x_step

. . . . . .next_y = self.y_values[-1] + y_step

. . . .

. . . . . .self.x_values.append(next_x)

. . . . . .self.y_values.append(next_y)

В точке (1) запускается цикл, который выполняется вплоть до заполнения пути правильным количеством точек. Главная часть этого метода сообщает Python, как следует моделировать четыре случайных решения: двигаться ли вправо или влево? Как далеко идти в этом направлении? Двигаться ли вверх или вниз? Как далеко идти в этом направлении?

Выражение choice([1, -1]) выбирает значение x_direction; оно возвращает 1 для перемещения вправо или –1 для движения влево (2). Затем выражение choice([0, 1, 2, 3, 4]) определяет дальность перемещения в этом направлении (x_distance) случайным выбором целого числа от 0 до 4. (Включение 0 позволяет выполнять шаги по оси y, а также шаги со смещением по обеим осям.)

В точках (3) и (4) определяется длина каждого шага в направлениях x и y, для чего направление движения умножается на выбранное расстояние. При положительном результате x_step смещает вправо, при отрицательном — влево, при нулевом — вертикально. При положительном результате y_step смещает вверх, при отрицательном — вниз, при нулевом — горизонтально. Если оба значения x_step и y_step равны 0, то блуждание останавливается, но цикл продолжается (5).

Чтобы получить следующее значение x, мы прибавляем значение x_step к последнему значению, хранящемуся в x_values ?, и делаем то же самое для значений y. После того как значения будут получены, они присоединяются к x_values и y_values.

 

Вывод случайного блуждания

Ниже приведен код отображения всех точек блуждания:

rw_visual.py

import matplotlib.pyplot as plt

from random_walk import RandomWalk

# Построение случайного блуждания и нанесение точек на диаграмму.

(1) rw = RandomWalk()

rw.fill_walk()

(2)plt.scatter(rw.x_values, rw.y_values, s=15)

plt.show()

Сначала программа импортирует pyplot и RandomWalk. Затем она создает случайное блуждание и сохраняет его в rw (1) , не забывая вызвать fill_walk(). В точке (2) программа передает scatter() координаты x и y блуждания и выбирает подходящий размер точки.

На рис. 15.8 показана диаграмма с 5000 точками. (В изображениях этого раздела область просмотра matplotlib не показана, но вы увидите ее при запуске rw_visual.py.)

Рис. 15.8. Случайное блуждание с 5000 точек

 

Генерирование нескольких случайных блужданий

Все случайные блуждания отличаются друг от друга; интересно понаблюдать за тем, какие узоры генерирует программа. Один из способов использования предыдущего кода — построить несколько блужданий без многократного запуска программы в цикле while:

rw_visual.py

import matplotlib.pyplot as plt

from random_walk import RandomWalk

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

while True:

# Построение случайного блуждания и нанесение точек на диаграмму.

rw = RandomWalk()

rw.fill_walk()

plt.scatter(rw.x_values, rw.y_values, s=15)

plt.show()

(1) . .keep_running = input("Make another walk? (y/n): ")

. .if keep_running == 'n':

. . . .break

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

Примечание

Если у вас версия Python 2.7, используйте raw_input() вместо input() в точке (1) .

 

Оформление случайного блуждания

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

 

Назначение цветов

Мы используем цветовую карту для отображения точек блуждания, а также удаляем черный контур из каждой точки, чтобы цвет точек был лучше виден. Чтобы точки окрашивались в соответствии с их позицией в блуждании, мы передаем в аргументе c список с позицией каждой точки. Так как точки выводятся по порядку, список просто содержит числа от 1 до 5000:

rw_visual.py

...

while True:

# Построение случайного блуждания и нанесение точек на диаграмму.

rw = RandomWalk()

rw.fill_walk()

(1) . .point_numbers = list(range(rw.num_points))

. .plt.scatter(rw.x_values, rw.y_values, c=point_numbers, cmap=plt.cm.Blues,

. . . .edgecolor='none', s=15)

plt.show()

keep_running = input("Make another walk? (y/n): ")

...

В точке (1) функция range() используется для генерирования списка чисел, размер которого равен количеству точек в блуждании. Полученный результат сохраняется в списке point_numbers, который используется для назначения цвета каждой точки в блуждании. Мы передаем point_numbers в аргументе c, используем цветовую карту Blues и затем передаем edgecolor=none для удаления черного контура ­вокруг каждой точки. В результате создается диаграмма блуждания с градиентным переходом от светло-синего к темно-синему (рис. 15.9).

Рис. 15.9. Случайное блуждание, окрашенное с применением цветовой карты Blues

 

Начальные и конечные точки

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

rw_visual.py

...

while True:

...

plt.scatter(rw.x_values, rw.y_values, c=point_numbers, cmap=plt.cm.Blues,

edgecolor='none', s=15)

. .# Выделение первой и последней точек.

. .plt.scatter(0, 0, c='green', edgecolors='none', s=100)

. .plt.scatter(rw.x_values[-1], rw.y_values[-1], c='red', edgecolors='none',

. . . .s=100)

. . . .

plt.show()

...

Чтобы вывести начальную точку, мы рисуем точку (0, 0) зеленым цветом с большим размером (s=100) по сравнению с остальными точками. Для выделения конечной точки последняя пара координат x и y выводится с размером 100. Обязательно вставьте этот код непосредственно перед вызовом plt.show(), чтобы начальная и конечная точки выводились поверх всех остальных точек.

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

 

Удаление осей

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

rw_visual.py

...

while True:

...

plt.scatter(rw.x_values[-1], rw.y_values[-1], c='red', edgecolors='none',

s=100)

. .# Удаление осей.

(1) . .plt.axes().get_xaxis().set_visible(False)

. .plt.axes().get_yaxis().set_visible(False)

. . . .

plt.show()

...

Функция plt.axes() (1) переводит флаг видимости каждой оси в состояние False. При работе с визуализацией подобные цепочки вызовов встречаются очень часто.

Запустите программу rw_visual.py; теперь выводимые диаграммы не имеют осей.

 

Добавление точек

Увеличим количество точек, чтобы работать с большим объемом данных. Для этого мы увеличим значение num_points при создании экземпляра RandomWalk и отрегулируем размер каждой точки при выводе диаграммы:

rw_visual.py

...

while True:

# Построение случайного блуждания и нанесение точек на диаграмму.

. .rw = RandomWalk(50000)

rw.fill_walk()

. .

# Вывод точек и отображение диаграммы.

point_numbers = list(range(rw.num_points))

. .plt.scatter(rw.x_values, rw.y_values, c=point_numbers, cmap=plt.cm.Blues,

. . . .edgecolor='none', s=1)

...

В этом примере создается случайное блуждание из 50 000 точек (что в большей степени соответствует реальным данным), и каждая точка рисуется с размером s=1. Как видно из рис. 15.10, изображение получается эфемерным и туманным. Простая точечная диаграмма превратилась в произведение искусства!

Поэкспериментируйте с этим кодом и посмотрите, насколько вам удастся увеличить количество точек в случайном блуждании, прежде чем система начнет заметно «тормозить» или диаграмма потеряет свою визуальную привлекательность.

Рис. 15.10. Случайное блуждание из 50 000 точек

 

Изменение размера диаграммы для заполнения экрана

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

rw_visual.py

...

while True:

# Make a random walk, and plot the points.

rw = RandomWalk()

rw.fill_walk()

. .

. .# Назначение размера области просмотра.

. .plt.figure(figsize=(10, 6))

...

Функция figure() управляет шириной, высотой, разрешением и цветом фона диаграммы. Параметр figsize получает кортеж с размерами окна диаграммы в дюймах.

Python предполагает, что разрешение экрана составляет 80 пикселов на дюйм; если этот код не дает точного размера, внесите необходимые изменения в числа. Или, если вы знаете разрешение экрана в вашей системе, передайте его figure() в параметре dpi для выбора размера, эффективно использующего доступное пространство:

plt.figure(dpi=128, figsize=(10, 6))

Упражнения

15-3. Молекулярное движение: измените программу rw_visual.py и замените plt.scatter() вызовом plt.plot(). Чтобы смоделировать путь пыльцевого зерна на поверхности водяной капли, передайте значения rw.x_values и rw.y_values и включите аргумент linewidth. Используйте 5000 точек вместо 50 000.

15-4. Измененные случайные блуждания: в классе RandomWalk значения x_step и y_step генерируются по единому набору условий. Направление выбирается случайно из списка [1, -1], а расстояние — из списка [0, 1, 2, 3, 4]. Измените значения в этих списках и посмотрите, что произойдет с общей формой диаграммы. Попробуйте применить расширенный список вариантов расстояния (например, от 0 до 8) или удалите –1 из списка направлений по оси x или y.

15-5. Рефакторинг: метод fill_walk() получился слишком длинным. Создайте новый метод с именем get_step(), который определяет расстояние и направление для каждого шага, после чего вычисляет этот шаг. В результате метод fill_walk() должен содержать два вызова get_step():

x_step = get_step()

y_step = get_step()

Рефакторинг сокращает размер fill_walk(), а метод становится более простым и понятным.

 

Моделирование бросков кубиков в Pygal

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

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

Броски кубиков часто используются в математике для пояснения различных типов анализа данных. Впрочем, они также находят применение в реальных приложениях — скажем, в казино и в обычных играх («Монополия», ролевые игры и т.д.).

 

Установка Pygal

Установите Pygal при помощи pip. (Если вы еще не использовали pip, обратитесь к разделу «Установка пакетов Python с использованием pip» на с. 227.)

В системах Linux и OS X команда должна выглядеть примерно так:

pip install --user pygal

В системе Windows она выглядит так:

python -m pip install --user pygal

ПРИМЕЧАНИЕ

Возможно, при установке пакетов вам придется использовать команду pip3 вместо pip. Если же эта команда не работает, попробуйте исключить флаг --user.

 

Галерея Pygal

Примеры визуализаций, которые могут быть построены с использованием Pygal, представлены в галерее диаграмм: зайдите на сайт http://www.pygal.org/, щелкните на ссылке Documentation, затем щелкните на ссылке Chart types. Каждый пример сопровождается исходным кодом, так что вы сможете увидеть, как была построена каждая из визуализаций.

 

Создание класса кубика

Для моделирования броска одного кубика мы будем использовать следующий класс:

die.py

from random import randint

class Die():

. ."""Класс, представляющий один кубик."""

. .

(1) . .def __init__(self, num_sides=6):

. . . ."""По умолчанию используется шестигранный кубик."""

. . . .self.num_sides = num_sides

. . . .

. .def roll(self):

. . . .""""Возвращает случайное число от 1 до числа граней."""

(2) . . . .return randint(1, self.num_sides)

Метод __init__() получает один необязательный аргумент. Если при создании экземпляра кубика аргумент с количеством сторон не передается, по умолчанию создается шестигранный кубик. Если же аргумент присутствует, то переданное значение используется для определения количества граней (1) . (Кубики принято обозначать по количеству граней: шестигранный кубик — D6, восьмигранный — D8 и т.д.)

Метод roll() использует функцию randint() для получения случайного числа в диапазоне от 1 до количества граней (2). Функция может вернуть начальное значение (1) , конечное значение (num_sides) или любое целое число в этом диапазоне.

 

Бросок кубика

Прежде чем строить визуализацию на основе этого класса, бросим кубик D6, выведем результаты и убедимся в том, что они выглядят разумно:

die_visual.py

from die import Die

# Создание кубика D6.

(1) die = Die()

# Моделирование серии бросков с сохранением результатов в списке.

results = []

(2)for roll_num in range(100):

. .result = die.roll()

. .results.append(result)

. .

print(results)

В точке (1) создается экземпляр Die с шестью гранями по умолчанию. В точке (2) моделируются 100 бросков кубика, а результат каждого броска сохраняется в списке results. Выборка выглядит примерно так:

[4, 6, 5, 6, 1, 5, 6, 3, 5, 3, 5, 3, 2, 2, 1, 3, 1, 5, 3, 6, 3, 6, 5, 4,

1, 1, 4, 2, 3, 6, 4, 2, 6, 4, 1, 3, 2, 5, 6, 3, 6, 2, 1, 1, 3, 4, 1, 4,

3, 5, 1, 4, 5, 5, 2, 3, 3, 1, 2, 3, 5, 6, 2, 5, 6, 1, 3, 2, 1, 1, 1, 6,

5, 5, 2, 2, 6, 4, 1, 4, 5, 1, 1, 1, 4, 5, 3, 3, 1, 3, 5, 4, 5, 6, 5, 4,

1, 5, 1, 2]

Беглое знакомство с результатами показывает, что класс Die работает. В результатах встречаются граничные значения 1 и 6, то есть модель возвращает наименьшее и наибольшее возможное значение; значения 0 и 7 не встречаются, а значит, все результаты лежат в диапазоне допустимых значений. Также в выборке встречаются все числа от 1 до 6, то есть представлены все возможные результаты.

 

Анализ результатов

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

die_visual.py

...

# Моделирование серии бросков с сохранением результатов в списке.

results = []

(1) for roll_num in range(1000):

result = die.roll()

results.append(result)

# Анализ результатов.

frequencies = []

(2)for value in range(1, die.num_sides+1):

(3) . .frequency = results.count(value)

(4) . .frequencies.append(frequency)

. .

print(frequencies)

Так как Pygal используется для анализа, а не для вывода результатов, количество моделируемых бросков можно увеличить до 1000 (1) . Для анализа создается пустой список frequencies, в котором хранится количество выпадений каждого значения. Программа перебирает возможные значения (от 1 до 6 в данном случае) в цикле (2), подсчитывает количество вхождений каждого числа в результатах (3), после чего присоединяет полученное значение к списку frequencies (4). Содержимое списка выводится перед построением визуализации:

[155, 167, 168, 170, 159, 181]

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

 

Построение гистограммы

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

die_visual.py

import pygal

...

# Анализ результатов.

frequencies = []

for value in range(1, die.num_sides+1):

frequency = results.count(value)

frequencies.append(frequency)

. .

# Визуализация результатов.

(1) hist = pygal.Bar()

hist.title = "Results of rolling one D6 1000 times."

(2)hist.x_labels = ['1', '2', '3', '4', '5', '6']

hist.x_title = "Result"

hist.y_title = "Frequency of Result"

(3)hist.add('D6', frequencies)

hist.render_to_file('die_visual.svg')

Чтобы построить столбцовую диаграмму, мы создаем экземпляр pygal.Bar(), который сохраняется в переменной hist (1) . Затем мы задаем атрибут title объекта hist (обычная строка, используемая как заголовок гистограммы), используем возможные результаты броска D6 как метки оси x (2), а также добавляем надпись для каждой из осей. Метод add() используется для добавления на гистограмму серии значений (3) (при этом ему передается метка для добавляемого набора и список значений, отображаемых на диаграмме). Наконец, диаграмма записывается в файл SVG, что предполагает имя файла с расширением .svg.

Полученную диаграмму проще всего просмотреть в браузере. Откройте новую вкладку в любом браузере, а в ней откройте файл die_visual.svg (из папки, в которой был сохранен файл die_visual.py). Диаграмма должна выглядеть примерно так, как на рис. 15.11. (Мы слегка изменили изображение для печати; по умолчанию Pygal генерирует диаграммы с более темным фоном, чем на рисунке.)

Рис. 15.11. Простая гистограмма, созданная с использованием Pygal

Обратите внимание на интерактивность диаграмм, построенных с использованием Pygal: если навести указатель мыши на столбец диаграммы, вы увидите данные, связанные с этим столбцом. Данная возможность особенно полезна при нанесении нескольких наборов данных на одну диаграмму.

 

Бросок двух кубиков

При броске двух кубиков вы получаете большие значения с другим распределением результатов. Изменим наш код и создадим два кубика D6, моделирующих бросок пары кубиков. При броске каждой пары программа складывает два числа (по одному с каждого кубика) и сохраняет сумму в results. Сохраните копию die_visual.py под именем dice_visual.py и внесите следующие изменения:

dice_visual.py

import pygal

from die import Die

# Создание двух кубиков D6.

die_1 = Die()

die_2 = Die()

# Моделирование серии бросков с сохранением результатов в списке.

results = []

for roll_num in range(1000):

(1) . .result = die_1.roll() + die_2.roll()

results.append(result)

. .

# Анализ результатов.

frequencies = []

(2)max_result = die_1.num_sides + die_2.num_sides

(3)for value in range(2, max_result+1):

frequency = results.count(value)

frequencies.append(frequency)

. .

# Визуализация результатов.

hist = pygal.Bar()

(4)hist.title = "Results of rolling two D6 dice 1000 times."

hist.x_labels = ['2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12']

hist.x_title = "Result"

hist.y_title = "Frequency of Result"

hist.add('D6 + D6', frequencies)

hist.render_to_file('dice_visual.svg')

Создав два экземпляра Die, мы бросаем кубики и вычисляем сумму для каждого броска (1) . Самый большой возможный результат (12) вычисляется суммированием наибольших результатов на обоих кубиках; мы сохраняем его в max_result (2). Наименьший возможный результат (2) равен сумме наименьших результатов на обоих кубиках. В процессе анализа подсчитывается количество результатов для каждого значения от 2 до max_result (3). (Также можно было использовать диапазон range(2, 13), но он работал бы только для двух кубиков D6. При моделировании реальных ситуаций лучше писать код, который легко адаптируется для разных ситуаций. В частности, этот код позволяет смоделировать бросок пары кубиков с любым количеством граней.)

При создании диаграммы мы задаем заголовок, метки оси x и серии данных (4). (Если бы список x_labels был намного длиннее, то его было бы удобнее сгенерировать автоматически в цикле.)

После выполнения кода обновите в браузере вкладку с диаграммой; примерный вид диаграммы показан на рис. 15.12.

На диаграмме показаны примерные результаты, которые могут быть получены для пары кубиков D6. Как видите, реже всего выпадают результаты 2 и 12, а чаще всего 7, потому что эта комбинация может быть выброшена шестью способами: 1+6, 2+5, 3+4, 4+3, 5+2 и 6+1.

Рис. 15.12. Смоделированные результаты 1000 бросков двух шестигранных кубиков

 

Броски кубиков с разным числом граней

Создадим кубики с 6 и 10 гранями и посмотрим, что произойдет, если бросить их 50 000 раз:

different_dice.py

from die import Die

import pygal

# Создание кубиков D6 и D10.

die_1 = Die()

(1) die_2 = Die(10)

# Моделирование серии бросков с сохранением результатов в списке.

results = []

for roll_num in range(50000):

result = die_1.roll() + die_2.roll()

results.append(result)

. .

# Analyze the results.

...

. .

# Визуализация результатов.

hist = pygal.Bar()

(2)hist.title = "Results of rolling a D6 and a D10 50,000 times."

hist.x_labels = ['2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12',

. .'13', '14', '15', '16']

hist.x_title = "Result"

hist.y_title = "Frequency of Result"

hist.add('D6 + D10', frequencies)

hist.render_to_file('dice_visual.svg')

Чтобы создать модель кубика D10, мы передаем аргумент 10 при создании вто­рого экземпляра Die1 и изменяем первый цикл для моделирования 50 000 бросков вместо 1000. Наименьший возможный результат, как и прежде, равен 2, зато наибольший увеличился до 16; мы соответственно изменяем заголовок, метки оси x и метки серии данных (2).

На рис. 15.13 показана полученная диаграмма. Вместо одного наиболее вероятного результата их стало целых пять. Это объясняется тем, что наименьшее (1+1) и наибольшее (6+10) значения по-прежнему могут быть получены только одним способом, но кубик D6 ограничивает количество способов генерирования средних чисел: суммы 7, 8, 9, 10 и 11 можно выбросить шестью способами. Следовательно, именно эти результаты являются наиболее частыми, и все эти числа выпадают с равной вероятностью.

Рис. 15.13. Результаты 50 000 бросков шести- и десятигранного кубиков

Возможность применения Pygal для моделирования бросков кубиков дает существенную свободу при исследовании этого явления. За считаные минуты вы сможете смоделировать огромное количество бросков с разнообразными кубиками.

Упражнения

15-6. Автоматические метки: измените программы die.py и dice_visual.py. Замените список, используемый для задания значений hist.x_labels, циклом, автоматически генерирующим этот список. Если вы хорошо освоили генераторы списков, также попробуйте заменить другие циклы for в die_visual.py и dice_visual.py генераторами списков.

15-7. Два кубика D8s: создайте модель, которая показывает, что происходит при 1000-кратном бросании двух восьмигранных кубиков. Постепенно наращивайте количество бросков, пока не начнете замечать ограничения, связанные с ресурсами вашей системы.

15-8. Три кубика: при броске 3 кубиков D6 наименьший возможный результат равен 3, а наибольший — 18. Создайте визуализацию, которая показывает, что происходит при броске трех кубиков D6.

15-9. Умножение: при броске двух кубиков результат обычно определяется суммированием двух чисел. Создайте визуализацию, которая показывает, что происходит при умножении этих чисел.

15-10. Эксперименты с библиотеками: попробуйте использовать matplotlib для создания визуализации бросков кубиков, а Pygal — для создания визуализации случайного блуждания.

 

Итоги

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

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

В главе 16 мы загрузим данные из сетевого источника и продолжим использовать matplotlib и Pygal для анализа данных.