В этой главе вы научились генерировать наборы данных и строить визуализации этих данных. Вы научились строить простые диаграммы с использованием matplotlib и применять точечные диаграммы для анализа случайных блужданий. Вы узнали, как построить гистограмму с использованием Pygal и как исследовать результаты бросков кубиков с разным количеством граней при помощи гистограммы.
Генерирование собственных наборов данных в программах — интересный и мощный способ моделирования и анализа различных реальных ситуаций. В дальнейших проектах визуализации данных обращайте особое внимание на ситуации, которые могут быть смоделированы на программном уровне. Присмотритесь к визуализациям, встречающимся в выпусках новостей, — возможно, они были сгенерированы методами, сходными с теми, о которых вы узнали в этих проектах?
В главе 16 мы загрузим данные из сетевого источника и продолжим использовать matplotlib и Pygal для анализа данных.
16. Загрузка данных
В этой главе мы загрузим наборы данных из сетевого источника и создадим работоспособные визуализации этих данных. В Интернете можно найти невероятно разнообразную информацию, бульшая часть которой еще не подвергалась основательному анализу. Умение анализировать данные позволит вам выявить связи и закономерности, не найденные никем другим.
В этой главе рассматривается работа с данными в двух популярных форматах, CSV и JSON. Модуль Python csv будет применен для обработки погодных данных в формате CSV (с разделением запятыми) и анализа динамики высоких и низких температур в двух разных местах. Затем библиотека matplotlib будет использована для построения на базе загруженных данных диаграммы изменения температур. Позднее в этой главе модуль json будет использован для обращения к данным численности населения, хранимым в формате JSON, а при помощи модуля Pygal будет построена карта распределения населения по странам.
К концу этой главы вы будете готовы к работе с разными типами и форматами наборов данных и начнете лучше понимать принципы построения сложных визуализаций. Возможность загрузки и визуализации сетевых данных разных типов и форматов крайне важна для работы с разнообразными массивами данных в реальном мире.
Формат CSV
Один из простейших вариантов хранения — запись данных в текстовый файл как серий значений, разделенных запятыми; такой формат хранения получил название CSV (от Comma Separated Values, то есть «значения, разделенные запятыми»). Например, одна строка погодных данных в формате CSV может выглядеть так:
2014-1-5,61,44,26,18,7,-1,56,30,9,30.34,30.27,30.15,,,,10,4,,0.00,0,,195
Это погодные данные за 5 января 2014 г. в Ситке (Аляска). В данных указаны максимальная и минимальная температуры, а также ряд других показателей за этот день. У человека могут возникнуть проблемы с чтением данных CSV, но этот формат хорошо подходит для программной обработки и извлечения значений, а это ускоряет процесс анализа.
Начнем с небольшого набора погодных данных в формате CSV, записанного в Ситке; файл с данными можно загрузить среди ресурсов книги по адресу https://www.nostarch.com/pythoncrashcourse/. Скопируйте файл sitka_weather_07-2014.csv в каталог, в котором сохраняются программы этой главы. (После загрузки ресурсов книги в вашем распоряжении появятся все необходимые файлы для этого проекта.)
Примечание
Погодные данные для этого проекта были загружены с сайта http://www.wunderground.com/history/.
Разбор заголовка файлов CSV
Модуль Python csv из стандартной библиотеки разбирает строки файла CSV и позволяет быстро извлечь нужные значения. Начнем с первой строки файла, которая содержит серию заголовков данных:
highs_lows.py
import csv
filename = 'sitka_weather_07-2014.csv'
(1) with open(filename) as f:
(2) . .reader = csv.reader(f)
(3) . .header_row = next(reader)
. .print(header_row)
После импортирования модуля csv имя обрабатываемого файла сохраняется в переменной filename. Затем файл открывается, а полученный объект сохраняется в переменной f (1) . Далее программа вызывает метод csv.reader() и передает ему объект файла в аргументе, чтобы создать объект чтения данных для этого файла (2). Объект чтения данных сохраняется в переменной reader.
Модуль csv содержит функцию next(), которая возвращает следующую строку файла для полученного объекта чтения данных. В следующем листинге функция next() вызывается только один раз для получения первой строки файла, содержащей заголовки (3). Возвращенные данные сохраняются в header_row. Как видите, header_row содержит осмысленные имена заголовков, которые сообщают, какая информация содержится в каждой строке данных:
['AKDT', 'Max TemperatureF', 'Mean TemperatureF', 'Min TemperatureF',
'Max Dew PointF', 'MeanDew PointF', 'Min DewpointF', 'Max Humidity',
' Mean Humidity', ' Min Humidity', ' Max Sea Level PressureIn',
' Mean Sea Level PressureIn', ' Min Sea Level PressureIn',
' Max VisibilityMiles', ' Mean VisibilityMiles', ' Min VisibilityMiles',
' Max Wind SpeedMPH', ' Mean Wind SpeedMPH', ' Max Gust SpeedMPH',
'PrecipitationIn', ' CloudCover', ' Events', ' WindDirDegrees']
Объект reader обрабатывает первую строку значений, разделенных запятыми, и сохраняет все значения в строке в списке. Заголовок AKDT означает «Alaska Daylight Time» (Аляска, летнее время). Позиция заголовка указывает на то, что первым значением в каждой из следующих строк является дата или время. Заголовок Max TemperatureF сообщает, что второе значение в каждой строке содержит максимальную температуру в этот день по шкале Фаренгейта. По именам заголовков можно определить, какая информация хранится в файле.
Примечание
Форматирование заголовков не всегда последовательно; иногда встречаются лишние пробелы, единицы измерения находятся в неожиданных местах. В необработанных файлах данных это бывает достаточно часто, но не создает проблем.
Печать заголовков и их позиций
Чтобы читателю было проще понять структуру данных в файле, выведем каждый заголовок и его позицию в списке:
highs_lows.py
...
with open(filename) as f:
reader = csv.reader(f)
header_row = next(reader)
. .
(1) . .for index, column_header in enumerate(header_row):
. . . .print(index, column_header)
Мы применяем к списку функцию enumerate() (1) для получения индекса каждого элемента и его значения. (Обратите внимание: строка print(header_row) удалена ради этой более подробной версии.)
Результат с индексами всех заголовков выглядит так:
0 AKDT
1 Max TemperatureF
2 Mean TemperatureF
3 Min TemperatureF
...
20 CloudCover
21 Events
22 WindDirDegrees
Из этих данных видно, что даты и максимальные температуры за эти дни находятся в столбцах 0 и 1. Чтобы проанализировать температурные данные, мы обработаем каждую запись данных в файле sitka_weather_07-2014.csv и извлечем элементы с индексами 0 и 1.
Извлечение и чтение данных
Итак, нужные столбцы данных известны; попробуем прочитать часть этих данных. Начнем с чтения максимальной температуры за каждый день:
highs_lows.py
import csv
# Чтение максимальных температур из файла.
filename = 'sitka_weather_07-2014.csv'
with open(filename) as f:
reader = csv.reader(f)
header_row = next(reader)
(1) . .highs = []
(2) . .for row in reader:
(3) . . . .highs.append(row[1])
. . . .
. .print(highs)
Программа создает пустой список с именем highs (1) и перебирает остальные строки в файле (2). Объект reader продолжает с того места, на котором он остановился в ходе чтения файла CSV, и автоматически возвращает каждую строку после текущей позиции. Так как заголовок уже прочитан, цикл продолжается со второй строки, в которой начинаются фактические данные. При каждом проходе цикла значение с индексом 1 (второй столбец) присоединяется к списку highs (3).
В результате будет получен список highs со следующим содержимым:
['64', '71', '64', '59', '69', '62', '61', '55', '57', '61', '57', '59', '57',
'61', '64', '61', '59', '63', '60', '57', '69', '63', '62', '59', '57', '57',
'61', '59', '61', '61', '66']
Мы извлекли максимальную температуру для каждого дня и аккуратно сохранили полученные данные в строковом формате в списке.
Затем преобразуем строки в числа при помощи функции int(), чтобы данные можно было передать matplotlib:
highs_lows.py
...
highs = []
for row in reader:
(1) . . . .high = int(row[1])
. . . .highs.append(high)
. . . .
print(highs)
Строки преобразуются в целые числа в точке (1) перед добавлением температур в список. Результат представляет собой список максимальных температур в числовом формате:
[64, 71, 64, 59, 69, 62, 61, 55, 57, 61, 57, 59, 57, 61, 64, 61, 59, 63, 60, 57,
69, 63, 62, 59, 57, 57, 61, 59, 61, 61, 66]
Следующим шагом станет построение визуализации этих данных.
Нанесение данных на диаграмму
Для наглядного представления температурных данных мы сначала создадим простую диаграмму дневных максимумов температуры с использованием matplotlib:
highs_lows.py
import csv
from matplotlib import pyplot as plt
# Чтение максимальных температур из файла.
...
# Нанесение данных на диаграмму.
fig = plt.figure(dpi=128, figsize=(10, 6))
(1) plt.plot(highs, c='red')
# Форматирование диаграммы.
(2)plt.title("Daily high temperatures, July 2014", fontsize=24)
(3)plt.xlabel('', fontsize=16)
plt.ylabel("Temperature (F)", fontsize=16)
plt.tick_params(axis='both', which='major', labelsize=16)
plt.show()
Мы передаем при вызове plot() список highs (1) и аргумент c='red' для отображения точек красным цветом. (Максимумы будут выводиться красным цветом, а минимумы синим.) Затем указываются другие аспекты форматирования (например, размер шрифта и метки) (2), уже знакомые вам по главе 15. Так как даты еще не добавлены, метки для оси x не задаются, но вызов plt.xlabel() изменяет размер шрифта, чтобы метки по умолчанию лучше читались (3). На рис. 16.1 показана полученная диаграмма: это простой график температурных максимумов за июль 2014 г. в Ситке (штат Аляска).
Рис. 16.1. График ежедневных температурных максимумов в июле 2014 г. в Ситке (штат Аляска)
Модуль datetime
Теперь нанесем даты на график, чтобы с ним было удобнее работать. Первая дата из файла погодных данных хранится во второй строке файла:
2014-7-1,64,56,50,53,51,48,96,83,58,30,19,...
Данные будут читаться в строковом формате, поэтому нам понадобится способ преобразовать строку '2014-7-1' в объект, представляющий эту дату. Чтобы построить объект, соответствующий 1 июля 2014 года, мы воспользуемся методом strptime() из модуля datetime. Посмотрим, как работает strptime() в терминальном окне:
>>>from datetime import datetime
>>>first_date = datetime.strptime('2014-7-1', '%Y-%m-%d')
>>>print(first_date)
2014-07-01 00:00:00
Сначала необходимо импортировать класс datetime из модуля datetime. Затем вызывается метод strptime(), первый аргумент которого содержит строку с датой. Второй аргумент сообщает Python, как отформатирована дата. В данном примере значение '%Y-' сообщает Python, что часть строки, предшествующая первому дефису, должна интерпретироваться как год из четырех цифр; '%m-' приказывает Python интерпретировать часть строки перед вторым дефисом как число, представляющее месяц; наконец, '%d' приказывает Python интерпретировать последнюю часть строки как день месяца от 1 до 31.
Метод strptime() может получать различные аргументы, которые описывают, как должна интерпретироваться запись даты. В табл. 16.1 перечислены некоторые из таких аргументов.
Таблица 16.1. Аргументы форматирования даты и времени из модуля datetime
Аргумент | Описание |
%A | Название дня недели — например, Monday |
%B | Название месяца — например, January |
%m | Порядковый номер месяца (от 01 до 12) |
%d | День месяца (от 01 до 31) |
%Y | Год из четырех цифр (например, 2015) |
%y | Две последние цифры года (например, 15) |
%H | Часы в 24-часовом формате (от 00 до 23) |
%I | Часы в 12-часовом формате (от 01 до 12) |
%p | AM или PM |
%M | Минуты (от 00 до 59) |
%S | Секунды (от 00 до 59) |
Представление дат на диаграмме
Научившись обрабатывать данные в файлах CSV, вы сможете улучшить диаграмму температурных данных. Для этого мы извлечем из файла даты ежедневных максимумов и передадим даты и максимумы функции plot():
highs_lows.py
import csv
from datetime import datetime
from matplotlib import pyplot as plt
# Чтение дат и температурных максимумов из файла.
filename = 'sitka_weather_07-2014.csv'
with open(filename) as f:
reader = csv.reader(f)
header_row = next(reader)
. .
(1) . .dates, highs = [], []
for row in reader:
(2) . . . .current_date = datetime.strptime(row[0], "%Y-%m-%d")
. . . .dates.append(current_date)
. . . .
high = int(row[1])
highs.append(high)
# Нанесение данных на диаграмму.
fig = plt.figure(dpi=128, figsize=(10, 6))
(3)plt.plot(dates, highs, c='red')
# Форматирование диаграммы.
plt.title("Daily high temperatures, July 2014", fontsize=24)
plt.xlabel('', fontsize=16)
(4)fig.autofmt_xdate()
plt.ylabel("Temperature (F)", fontsize=16)
plt.tick_params(axis='both', which='major', labelsize=16)
plt.show()
Рис. 16.2. График с датами на оси x стал более понятным
Мы создаем два пустых списка для хранения дат и температурных максимумов из файла (1) . Затем программа преобразует данные, содержащие информацию даты (row[0]), в объект datetime (2), который присоединяется к dates. Значения дат и температурных максимумов передаются plot() в точке (3). Вызов fig.autofmt_xdate() в точке (4) выводит метки дат по диагонали, чтобы они не перекрывались. На рис. 16.2 изображена новая версия графика.
Расширение временного диапазона
Итак, график успешно создан. Добавим на него новые данные для получения более полной картины погоды в Ситке. Скопируйте файл sitka_weather_2014.csv, содержащий погодные данные для Ситки за целый год, в каталог с программами этой главы.
А теперь мы можем сгенерировать график с погодными данными за год:
highs_lows.py
...
# Чтение дат и температурных максимумов из файла.
(1) filename = 'sitka_weather_2014.csv'
with open(filename) as f:
...
# Форматирование диаграммы.
(2)plt.title("Daily high temperatures - 2014", fontsize=24)
plt.xlabel('', fontsize=16)
...
Значение filename изменено, чтобы в программе использовался новый файл данных sitka_weather_2014.csv (1) , а заголовок диаграммы приведен в соответствие с содержимым (2).
На рис. 16.3 изображена полученная диаграмма.
Рис. 16.3. Данные за год
Нанесение на диаграмму второй серии данных
Обновленный график на рис. 16.3 содержит значительное количество полезных данных, но график можно сделать еще полезнее, добавив на него данные температурных минимумов. Для этого необходимо прочитать температурные минимумы из файла данных и нанести их на график:
highs_lows.py
...
# Чтение дат, температурных максимумов и минимумов из файла.
filename = 'sitka_weather_2014.csv'
with open(filename) as f:
reader = csv.reader(f)
header_row = next(reader)
. .
(1) . .dates, highs, lows = [], [], []
for row in reader:
current_date = datetime.strptime(row[0], "%Y-%m-%d")
dates.append(current_date)
. . . .
high = int(row[1])
highs.append(high)
. . . .
(2) . . . .low = int(row[3])
. . . .lows.append(low)
# Нанесение данных на диаграмму.
fig = plt.figure(dpi=128, figsize=(10, 6))
plt.plot(dates, highs, c='red')
(3)plt.plot(dates, lows, c='blue')
# Формат диаграммы.
x plt.title("Daily high and low temperatures - 2014", fontsize=24)
...
Рис. 16.4. Две серии данных на одной диаграмме
В точке (1) создается пустой список lows для хранения температурных минимумов, после чего программа извлекает и сохраняет температурный минимум для каждой даты из четвертой позиции каждой строки данных (row[3]) (2). В точке (3) добавляется вызов plot() для температурных минимумов, которые окрашиваются в синий цвет. Затем остается лишь обновить заголовок диаграммы (4).
На рис. 16.4 изображена полученная диаграмма.
Цветовое выделение части диаграммы
После добавления двух серий данных можно переходить к анализу диапазона температур по дням. Пора сделать последний штрих в оформлении диаграммы: затушевать диапазон между минимальной и максимальной дневной температурой. Для этого мы воспользуемся методом fill_between(), который получает серию значений x и две серии значений y и заполняет область между двумя значениями y:
highs_lows.py
...
# Нанесение данных на диаграмму.
fig = plt.figure(dpi=128, figsize=(10, 6))
(1) plt.plot(dates, highs, c='red', alpha=0.5)
plt.plot(dates, lows, c='blue', alpha=0.5)
(2)plt.fill_between(dates, highs, lows, facecolor='blue', alpha=0.1)
...
Аргумент alpha (1) определяет степень прозрачности вывода. Значение 0 означает полную прозрачность, а 1 (по умолчанию) — полную непрозрачность. Со значением alpha=0.5 красные и синие линии на графике становятся более светлыми.
Рис. 16.5. Область между двумя наборами данных закрашена
В точке (2) fill_between() передается список dates для значений x и две серии значений y highs и lows. Аргумент facecolor определяет цвет закрашиваемой области; мы назначаем ему низкое значение alpha=0.1, чтобы заполненная область соединяла две серии данных, не отвлекая зрителя от передаваемой информации. На рис. 16.5 показана диаграмма с закрашенной областью между highs и lows.
Закрашенная область подчеркивает величину расхождения между двумя наборами данных.
Проверка ошибок
Программа highs_lows.py должна нормально работать для погодных данных любого места. Однако на некоторых метеорологических станциях происходят сбои, и станциям не удается собрать данные (полностью или частично). Отсутствие данных может привести к исключениям; если исключения не будут обработаны, то программа аварийно завершится.
Для примера попробуем построить график температур для Долины Смерти (штат Калифорния). Скопируйте файл death_valley_2014.csv в каталог с программами этой главы, после чего внесите изменения в highs_lows.py для работы с другим набором данных:
highs_lows.py
...
# Чтение дат, температурных максимумов и минимумов из файла.
filename = 'death_valley_2014.csv'
with open(filename) as f:
...
При запуске программы происходит ошибка, как видно из последней строки следующего вывода:
Traceback (most recent call last):
File "highs_lows.py", line 17, in
. .high = int(row[1])
ValueError: invalid literal for int() with base 10: ''
Трассировка показывает, что Python не может обработать максимальную температуру для одной из дат, потому что не может преобразовать пустую строку ('') в целое число. Чтобы понять причину, достаточно заглянуть в файл death_valley_2014.csv:
2014-2-16,,,,,,,,,,,,,,,,,,,0.00,,,-1
Похоже, 16 февраля 2014 года данные не сохранялись; строка максимальной температуры пуста. Чтобы решить проблему, мы будем выполнять проверку ошибок при чтении данных из файла для обработки исключений, которые могут возникнуть при разборе наборов данных. Вот как это делается:
highs_lows.py
...
# Чтение дат, температурных максимумов и минимумов из файла.
filename = 'death_valley_2014.csv'
with open(filename) as f:
reader = csv.reader(f)
header_row = next(reader)
dates, highs, lows = [], [], []
for row in reader:
(1) . . . .try:
. . . . . .current_date = datetime.strptime(row[0], "%Y-%m-%d")
. . . . . .high = int(row[1])
. . . . . .low = int(row[3])
. . . .except ValueError:
(2) . . . . . .print(current_date, 'missing data')
. . . .else:
(3) . . . . . .dates.append(current_date)
. . . . . .highs.append(high)
. . . . . .lows.append(low)
# Plot data.
...
# Форматирование диаграммы
(4)title = "Daily high and low temperatures - 2014\nDeath Valley, CA"
plt.title(title, fontsize=20)
...
При анализе каждой строки данных мы пытаемся извлечь дату, максимальную и минимальную температуру (1) . Если каких-либо данных не хватает, Python выдает ошибку ValueError, а мы обрабатываем ее — выводим сообщение с датой, для которой отсутствуют данные (2). После вывода ошибки цикл продолжает обработку следующей порции данных. Если все данные, относящиеся к некоторой дате, прочитаны без ошибок, выполняется блок else, а данные присоединяются к соответствующим спискам (3). Так как на диаграмме отображается информация для нового места, заголовок изменяется, и в него включается название места (4).
Рис. 16.6. Максимальная и минимальная температура в Долине Смерти
При выполнении highs_lows.py мы видим, что данные отсутствуют только для одной даты:
2014-02-16 missing data
Полученная диаграмма изображена на рис. 16.6.
Сравнивая эту диаграмму с диаграммой для Ситки, мы видим, что в Долине Смерти теплей, чем на юго-востоке Аляски (как и следовало ожидать), но при этом температурный диапазон в пустыне более широкий. Высота закрашенной области наглядно демонстрирует этот факт.
Во многих наборах данных, с которыми вы будете работать, будут встречаться отсутствующие, неправильно отформатированные или некорректные данные. В таких ситуациях воспользуйтесь теми инструментами, которые вы освоили в первой половине книги. В данном примере для обработки отсутствующих данных использовался блок try-except-else. Иногда команда continue используется для пропуска части данных, или же данные удаляются после извлечения вызовом remove() или del. Используйте любое работающее решение — лишь бы в результате у вас получилась осмысленная, точная визуализация.
Упражнения
16-1. Сан-Франциско: к какому месту ближе температура в Сан-Франциско: к Ситке или Долине Смерти? Постройте температурную диаграмму для Сан-Франциско и сравните. (Погодные данные практически для любого места можно загрузить по адресу http://www.wunderground.com/history/. Введите название места и диапазон дат, прокрутите страницу и найдите ссылку Comma-Delimited File. Щелкните правой кнопкой мыши на ссылке и сохраните данные в файле CSV.)
16-2. Сравнение Ситки с Долиной Смерти: разные масштабы температур отражают разные диапазоны данных. Чтобы точно сравнить температурный диапазон в Ситке с температурным диапазоном Долины Смерти, необходимо установить одинаковый масштаб по оси y. Измените параметры оси y для одной или обеих диаграмм на рис. 16.5 и 16.6 и проведите прямое сравнение температурных диапазонов в этих двух местах (или любых других, которые вас интересуют). Также можно попробовать нанести два набора данных на одну диаграмму.
16-3. Осадки: выберите любое место и постройте диаграмму с уровнем осадков. Для начала ограничьтесь данными за один месяц, а когда ваш код заработает, выполните программу для данных за полный год.
16-4. Исследования: постройте еще несколько визуализаций, отражающих любые другие аспекты погоды для интересующих вас мест.