Книга: Изучаем Python: программирование игр, визуализация данных, веб-приложения. 3-е изд. дополненное и переработанное
Назад: 15. Генерирование данных
Дальше: 17. Работа с API

16. Загрузка данных

27673.png

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

В этой главе рассматривается работа с данными в двух популярных форматах: CSV и JSON. Модуль Python csv будет применен для обработки погодных данных в формате CSV (с разделением запятыми) и анализа динамики высоких и низких температур в двух разных местах. Затем библиотека Matplotlib будет использована для создания на базе скачанных данных диаграммы колебания температур в двух разных местах: в Ситке (Аляска) и Долине Смерти (Калифорния). Позднее в этой главе модуль json будет использован для обращения к данным численности населения, хранимым в формате GeoJSON, а с помощью модуля Plotly будет создана карта с данными местоположения и магнитуд недавних землетрясений.

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

Формат CSV

Один из простейших вариантов хранения — запись данных в текстовый файл как серий значений, разделенных запятыми; такой формат хранения получил название CSV (от Comma Separated Values, то есть «значения, разделенные запятыми»). Например, одна строка погодных данных в формате CSV может выглядеть так:

"USW00025333","SITKA AIRPORT, AK US","2021-01-01",,"44","40"

Это погодные данные за 1 января 2021 г. в Ситке (Аляска). В данных указаны максимальная и минимальная температуры, а также ряд других показателей за этот день. У человека могут возникнуть проблемы с чтением данных CSV, но такой формат хорошо подходит для программной обработки и извлечения значений, а это ускоряет процесс анализа.

Начнем с небольшого набора погодных данных в формате CSV, записанного в Ситке; файл с данными можно скачать с https://ehmatthes.github.io/pcc_3e. Создайте папку weather_data в папке, в которой сохраняются программы этой главы. Скопируйте в созданную папку файл sitka_weather_07-2021_simple.csv. (После скачивания дополнительных материалов к книге в вашем распоряжении появятся все необходимые файлы для этого проекта.)

ПРИМЕЧАНИЕ

Погодные данные для этого проекта были скачаны с сайта https://ncdc.noaa.gov/cdo-web/.

Разбор заголовка файлов CSV

Модуль Python csv из стандартной библиотеки разбирает строки файла CSV и позволяет быстро извлечь нужные значения. Начнем с первой строки файла, которая содержит серию заголовков данных. Заголовки описывают информацию, хранящуюся в данных:

sitka_highs.py

from pathlib import Path

import csv

 

❶ path = Path('weather_data/sitka_weather_07-2021_simple.csv')

lines = path.read_text().splitlines()

 

❷ reader = csv.reader(lines)

❸ header_row = next(reader)

print(header_row)

Сначала мы импортируем Path и модуль csv. Затем создаем объект Path, располагаемый в папке weather_data и ссылающийся на специальный файл с информацией о погоде . Интерпретатор считывает файл, а затем метод splitlines() извлекает из него список всех строк и присваивает переменной lines.

Далее создается объект reader . Он используется для парсинга всех строк в файле. Чтобы создать объект reader, мы вызываем функцию csv.reader() и передаем ей список строк из файла CSV.

При передаче объекта reader функция next() возвращает следующую строку из файла, начиная с начала документа. Функция next() вызывается только раз для получения первой строки файла, содержащей заголовки . Возвращенные данные сохраняются в header_row. Как видите, строка включает описательные имена заголовков, которые сообщают, какая информация содержится в каждой строке данных:

['STATION', 'NAME', 'DATE', 'TAVG', 'TMAX', 'TMIN']

Объект reader обрабатывает первую строку значений, разделенных запятыми, и сохраняет все значения из строки в списке. Заголовок STATION представляет код метеорологической станции, зарегистрировавшей данные. Позиция заголовка указывает на то, что первым значением в каждой из следующих строк является код метеостанции. Заголовок NAME указывает на то, что второе значение в каждой строке — это название метеостанции, регистрирующей погоду. Остальные заголовки сообщают, какая информация хранится в соответствующем поле. В данном примере нас интересуют значения даты (DATE), а также высокой и низкой температуры (TMAX и TMIN соответственно). Мы используем простой набор данных, содержащий информацию только об уровне осадков и температуре. Вы также можете скачать собственный набор погодных данных и добавить в обработку другие показатели: скорость и направление ветра, расширенные данные осадков и т.д.

Вывод заголовков и их позиций

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

sitka_highs.py

--пропуск--

reader = csv.reader(lines)

header_row = next(reader)

 

for index, column_header in enumerate(header_row):

    print(index, column_header)

Функция enumerate() возвращает индекс каждого элемента и его значение при переборе списка. (Обратите внимание: строка print(header_row) удалена ради этой более подробной версии.)

Результат с индексами всех заголовков выглядит так:

0 STATION

1 NAME

2 DATE

3 TAVG

4 TMAX

5 TMIN

Из этих данных видно, что даты и максимальные температуры за эти дни находятся в столбцах 2 и 4. Чтобы проанализировать температурные данные, мы обработаем каждую запись данных в файле sitka_weather_07-2021_simple.csv и извлечем элементы с индексами 2 и 4.

Извлечение и чтение данных

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

sitka_highs.py

--пропуск--

reader = csv.reader(lines)

header_row = next(reader)

 

 

# Извлечение максимальных температур.

❶ highs = []

❷ for row in reader:

❸     high = int(row[4])

    highs.append(high)

 

print(highs)

Программа создает пустой список highs и перебирает остальные строки в файле . Объект reader продолжает с того места, на котором он остановился в ходе чтения файла CSV, и автоматически возвращает каждую строку после текущей позиции. Заголовок уже прочитан, поэтому цикл продолжается со второй строки, в которой начинаются фактические данные. При каждом проходе цикла значение с индексом 4 (заголовок TMAX) присваивается переменной high . Функция int() преобразует данные, хранящиеся в строковом виде, в числовой формат, чтобы их можно было использовать в дальнейшем. Значение присоединяется к списку highs.

В результате будет получен список highs со следующим содержимым:

[61, 60, 66, 60, 65, 59, 58, 58, 57, 60, 60, 60, 57, 58, 60, 61, 63, 63, 70,

64, 59, 63, 61, 58, 59, 64, 62, 70, 70, 73, 66]

Мы извлекли максимальную температуру для каждого дня и сохранили полученные данные в списке. Следующим шагом станет создание визуализации этих данных.

Нанесение данных на диаграмму

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

sitka_highs.py

from pathlib import Path

import csv

 

import matplotlib.pyplot as plt

 

path = Path('weather_data/sitka_weather_07-2021_simple.csv')

lines = path.read_text().splitlines()

    --пропуск--

 

# Нанесение данных на диаграмму.

plt.style.use('seaborn')

fig, ax = plt.subplots()

❶ ax.plot(highs, color='red')

 

# Форматирование диаграммы.

❷ ax.set_title("Daily High Temperatures, July 2021", fontsize=24)

❸ ax.set_xlabel('', fontsize=16)

ax.set_ylabel("Temperature (F)", fontsize=16)

ax.tick_params(labelsize=16)

 

plt.show()

Мы передаем при вызове plot() список highs и аргумент c='red' для отображения точек красным цветом . (Максимумы будут выводиться красным цветом, а минимумы — синим.) Затем указываем другие аспекты форматирования (например, размер шрифта и метки) , уже знакомые нам по главе 15. Даты еще не добавлены, поэтому метки для оси X не задаются, но вызов ax.set_xlabel() изменяет размер шрифта, чтобы метки по умолчанию лучше читались . На рис. 16.1 показана полученная диаграмма: это простой график максимальных температур за июль 2021 года в Ситке (штат Аляска).

16_01.tif 

Рис. 16.1. График ежедневных максимальных температур в июле 2021 года в Ситке (штат Аляска)

Модуль datetime

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

"USW00025333","SITKA AIRPORT, AK US","2021-07-01",,"61","53"

Данные будут читаться в строковом формате, поэтому нам понадобится способ преобразовать строку '2021-07-1' в объект, представляющий эту дату. Чтобы создать объект, соответствующий 1 июля 2021 года, мы воспользуемся методом strptime() из модуля datetime. Посмотрим, как работает strptime() в терминальном окне:

>>> from datetime import datetime

>>> first_date = datetime.strptime('2021-07-01', '%Y-%m-%d')

>>> print(first_date)

2021-07-01 00:00:00

Сначала необходимо импортировать класс datetime из модуля datetime. Затем вызывается метод strptime(), первый аргумент которого содержит строку с датой. Второй аргумент сообщает Python, как отформатирована дата. В данном примере благодаря разным значениям Python получает следующие указания:

%Y-' — часть строки, предшествующую первому дефису, интерпретировать как год из четырех цифр;

• '%m-' — часть строки перед вторым дефисом интерпретировать как число из двух цифр, представляющее месяц;

'%d' — последнюю часть строки интерпретировать как день месяца от 1 до 31.

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

Таблица 16.1. Аргументы форматирования даты и времени из модуля datetime

Аргумент

Описание

%A

Название дня недели — например, Monday

%B

Название месяца — например, January

%m

Порядковый номер месяца (от 01 до 12)

%d

День месяца (от 01 до 31)

%Y

Год из четырех цифр (например, 2019)

%y

Две последние цифры года (например, 19)

%H

Часы в 24-часовом формате (от 00 до 23)

%I

Часы в 12-часовом формате (от 01 до 12)

%p

AM или PM

%M

Минуты (от 00 до 59)

%S

Секунды (от 00 до 61)

Представление дат на диаграмме

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

sitka_highs.py

from pathlib import Path

import csv

from datetime import datetime

 

import matplotlib.pyplot as plt

 

path = Path('weather_data/sitka_weather_07-2021_simple.csv')

lines = path.read_text().splitlines()

 

reader = csv.reader(lines)

header_row = next(reader)

 

# Извлечение дат и максимальных температур.

❶ dates, highs = [], []

for row in reader:

❷     current_date = datetime.strptime(row[2], '%Y-%m-%d')

    high = int(row[4])

    dates.append(current_date)

    highs.append(high)

 

# Создание диаграммы максимальных температур.

plt.style.use('seaborn')

fig, ax = plt.subplots()

❸ ax.plot(dates, highs, color='red')

 

# Форматирование диаграммы.

ax.set_title("Daily High Temperatures, July 2021", fontsize=24)

ax.set_xlabel('', fontsize=16)

❹ fig.autofmt_xdate()

ax.set_ylabel("Temperature (F)", fontsize=16)

ax.tick_params(labelsize=16)

 

plt.show()

Мы создаем два пустых списка для хранения дат и максимальных температур из файла . Затем программа преобразует данные, содержащие информацию даты (row[2]), в объект datetime , который присоединяется к dates. Значения дат и максимальных температур передаются plot() в строке . Вызов fig.autofmt_xdate() выводит метки дат по диагонали, чтобы они не перекрывались. На рис. 16.2 изображена новая версия графика.

16_02.tif 

Рис. 16.2. График с датами на оси Х стал более понятным

Расширение временного диапазона

Итак, график успешно создан. Добавим на него новые данные для получения более полной картины погоды в Ситке. Скопируйте файл sitka_weather_2021_simple.csv, содержащий погодные данные для Ситки за целый год, в папку с программами этой главы.

А теперь мы можем сгенерировать график с погодными данными за год:

sitka_highs.py

--пропуск--

path = Path('weather_data/sitka_weather_2021_simple.csv')

lines = path.read_text().splitlines()

--пропуск--

# Форматирование диаграммы.

ax.set_title("Daily High Temperatures, 2021", fontsize=24)

ax.set_xlabel('', fontsize=16)

--пропуск--

Значение filename было изменено, чтобы в программе использовался новый файл данных sitka_weather_2021_simple.csv, а заголовок диаграммы приведен в соответствие с содержимым. На рис. 16.3 изображена полученная диаграмма.

16_03.tif 

Рис. 16.3. Данные за год

Добавление в диаграмму второго набора данных

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

sitka_highs_lows.py

--пропуск--

reader = csv.reader(lines)

header_row = next(reader)

 

# Извлечение дат, минимальных и максимальных температур из файла.

❶ dates, highs, lows = [], [], []

for row in reader:

    current_date = datetime.strptime(row[2], '%Y-%m-%d')

    high = int(row[4])

❷     low = int(row[5])

    dates.append(current_date)

    highs.append(high)

    lows.append(low)

 

# Создание диаграммы высоких и низких температур.

plt.style.use('seaborn')

fig, ax = plt.subplots()

ax.plot(dates, highs, color='red')

❸ ax.plot(dates, lows, color='blue')

 

# Форматирование диаграммы.

❹ ax.set_title("Daily High and Low Temperatures, 2021", fontsize=24)

--пропуск--

Сначала создается пустой список lows для хранения минимальных температур , после чего программа извлекает и сохраняет температурный минимум для каждой даты из шестой позиции каждой строки данных (row[5]) . Далее добавляется вызов plot() для минимальных температур, которые окрашиваются в синий цвет . Затем остается лишь обновить заголовок диаграммы . На рис. 16.4 изображена полученная диаграмма.

16_04.tif 

Рис. 16.4. Два набора данных на одной диаграмме

Цветовое выделение частей диаграммы

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

sitka_highs_lows.py

--пропуск--

# Создание диаграммы высоких и низких температур.

plt.style.use('seaborn')

fig, ax = plt.subplots()

❶ ax.plot(dates, highs, color='red', alpha=0.5)

ax.plot(dates, lows, color='blue', alpha=0.5)

❷ ax.fill_between(dates, highs, lows, facecolor='blue', alpha=0.1)

--пропуск--

Аргумент alpha определяет степень прозрачности вывода . Значение 0 говорит о полной прозрачности, а 1 (по умолчанию) — о полной непрозрачности. Со значением alpha=0.5 красные и синие линии на графике становятся более светлыми.

Затем fill_between() передается список dates для значений x и две серии значений y highs и lows . Аргумент facecolor определяет цвет закрашиваемой области; мы задаем ему низкое значение alpha=0.1, чтобы заполненная область соединяла две серии данных, не отвлекая зрителя от передаваемой информации. На рис. 16.5 показана диаграмма с закрашенной областью между highs и lows.

16_05.tif 

Рис. 16.5. Область между двумя наборами данных закрашена

Закрашенная область подчеркивает величину расхождения между двумя наборами данных.

Проверка ошибок

Программа sitka_highs_lows.py должна нормально работать для погодных данных любого места. Однако одни метеорологические станции собирают данные по особым правилам, а другим не удается собрать данные из-за сбоев (полных или частичных). Отсутствие данных может привести к исключениям; если последние не обработать, то программа завершится со сбоем.

Для примера попробуем создать диаграмму температур для Долины Смерти (штат Калифорния). Скопируйте файл death_valley_2021_simple.csv в папку с программами этой главы.

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

death_valley_highs_lows.py

from pathlib import Path

import csv

 

path = Path('weather_data/death_valley_2021_simple.csv')

lines = path.read_text().splitlines()

 

reader = csv.reader(lines)

header_row = next(reader)

 

for index, column_header in enumerate(header_row):

    print(index, column_header)

Результат выглядит так:

0 STATION

1 NAME

2 DATE

3 TMAX

4 TMIN

5 TOBS

Дата остается в той же позиции с индексом 2. Но температурные максимумы и минимумы находятся в позициях с индексами 3 и 4, поэтому нам придется изменить индексы в программе в соответствии с новыми позициями. Вместо того чтобы добавлять средние показания температуры за день, эта станция регистрирует TOBS — данные за конкретное время наблюдений.

Внесите изменения в sitka_highs_lows.py, чтобы создать график температур для Долины Смерти по только что определенным индексам, и проследите за происходящим:

death_valley_highs_lows.py

--пропуск--

path = Path('weather_data/death_valley_2021_simple.csv')

lines = path.read_text().splitlines()

    --пропуск--

# Извлечение дат, минимальных и максимальных температур из файла.

dates, highs, lows = [], [], []

for row in reader:

    current_date = datetime.strptime(row[2], '%Y-%m-%d')

    high = int(row[3])

    low = int(row[4])

    dates.append(current_date)

--пропуск--

Код изменен так, чтобы программа считывала данные из файла с погодой в Долине Смерти; изменены и индексы, чтобы соответствовать позициям TMAX и TMIN этого файла.

При запуске программы происходит ошибка:

Traceback (most recent call last):

  File "death_valley_highs_lows.py", line 17, in <module>

    high = int(row[3])

❶ ValueError: invalid literal for int() with base 10: ''

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

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

death_valley_highs_lows.py

--пропуск--

for row in reader:

    current_date = datetime.strptime(row[2], '%Y-%m-%d')

❶     try:

        high = int(row[3])

        low = int(row[4])

    except ValueError:

❷         print(f"Missing data for {current_date}")

❸     else:

        dates.append(current_date)

        highs.append(high)

        lows.append(low)

 

# Создание диаграммы высоких и низких температур.

--пропуск--

 

# Форматирование диаграммы.

❹ title = "Daily High and Low Temperatures, 2021\nDeath Valley, CA"

ax.set_title(title, fontsize=20)

ax.set_xlabel('', fontsize=16)

--пропуск--

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

При выполнении death_valley_highs_lows.py мы видим, что данные отсутствуют только для одной даты:

Missing data for 2021-05-04 00:00:00

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

16_06.tif 

Рис. 16.6. Максимальная и минимальная температуры в Долине Смерти

Сравнивая эту диаграмму с диаграммой для Ситки, мы видим, что в Долине Смерти теплее, чем на юго-востоке Аляски (как и следовало ожидать), но при этом температурный диапазон в пустыне более широкий. Высота закрашенной области наглядно демонстрирует этот факт.

Во многих наборах данных в реальной работе будут встречаться отсутствующие, неправильно отформатированные или некорректные данные. В таких ситуациях воспользуйтесь теми инструментами, которые вы освоили, изучая первую половину книги. В данном примере для обработки отсутствующих данных использовался блок try-except-else. Иногда оператор continue применяется для пропуска части данных, или же данные удаляются после извлечения путем вызова метода remove() или оператора del. Используйте любое работающее решение — лишь бы в результате у вас получилась имеющая смысл точная визуализация.

Скачивание собственных данных

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

1. Посетите сайт NOAA Climate Data Online по адресу https://www.ncdc.noaa.gov/cdo-web/. В разделе Discover Data (Поиск данных) нажмите кнопку Search Tool (Инструменты поиска). В поле Select a Dataset (Выбор набора данных) выберите вариант Daily Summaries (Ежедневные сводки).

2. Выберите диапазон дат. В разделе Search For (Поиск) выберите вариант ZIP Codes (Почтовые индексы). Введите индекс интересующего вас места и нажмите кнопку Search (Поиск).

3. На следующей странице отображаются карта и информация об области, на которой вы желаете сосредоточиться. Под названием места нажмите кнопку View Full Details (Просмотреть полную информацию) либо на карте, а затем выберите вариант Full Details (Полная информация).

4. Прокрутите данные и нажмите кнопку Station List (Список станций), чтобы просмотреть список метеорологических станций в этой области. Выберите одну из станций и нажмите кнопку Add to Cart (Добавить на карту). Данные распространяются бесплатно, несмотря на то что на сайте используется обозначение покупательской корзины. Щелкните на изображении корзины в правом верхнем углу.

5. Выберите Output (Выходные данные), затем Custom GHCN-Daily CSV (Пользовательский файл CSV GHCN-Daily). Проверьте на правильность диапазон дат и нажмите кнопку Continue (Продолжить).

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

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

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

Упражнения

16.1. Осадки в Ситке. Ситка находится в зоне умеренных лесов, так что в этой местности выпадает достаточно осадков. В файле данных sitka_weather_2021_full.csv есть заголовок PRCP, представляющий величину ежедневных осадков. Создайте диаграмму по данным этого столбца. Повторите упражнение для Долины Смерти, если вас интересует, сколько осадков выпадает в пустыне.

16.2. Сравнение Ситки с Долиной Смерти. Разные масштабы температур отражают разные диапазоны данных. Чтобы точно сравнить температурный диапазон в Ситке с температурным диапазоном Долины Смерти, необходимо установить одинаковый масштаб по оси Y. Измените параметры оси Y для одной или обеих диаграмм на рис. 16.5 и 16.6 и проведите прямое сравнение температурных диапазонов в этих двух местах (или любых других, которые вас интересуют).

16.3. Сан-Франциско. К какому месту ближе температура в Сан-Франциско: к Ситке или Долине Смерти? Скачайте данные для этого города, создайте температурную диаграмму для него и сравните.

16.4. Автоматические индексы. В этом разделе индексы, соответствующие столбцам TMIN и TMAX, были жестко зафиксированы в коде. Используйте строку данных заголовка для определения индексов этих значений, чтобы ваша программа работала как для Ситки, так и Долины Смерти. Используйте название станции, чтобы автоматически сгенерировать подходящий заголовок для вашей диаграммы.

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

Создание карт с глобальными наборами данных: формат GeoJSON

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

Скачивание данных о землетрясениях

Создайте папку eq_data в папке, в которой хранятся программы для этой главы. Скопируйте файл q_1_day_m1.geojson в эту новую папку. Землетрясения классифицируются по магнитуде на основании шкалы Рихтера. Файл содержит данные по всем землетрясениям с магнитудой M1 и выше, произошедшим за последние 24 часа (на момент написания книги). Информация взята из одного из каналов данных Геологического управления США, доступных по адресу https://earthquake.usgs.gov/earthquakes/feed/.

Знакомство с форматом GeoJSON

Открыв файл eq_1_day_m1.json, вы увидите, что данные упакованы очень плотно и читать их сложно:

{"type":"FeatureCollection","metadata":{"generated":1649052296000,...

{"type":"Feature","properties":{"mag":1.6,"place":"63 km SE of Ped...

{"type":"Feature","properties":{"mag":2.2,"place":"27 km SSE of Ca...

{"type":"Feature","properties":{"mag":3.7,"place":"102 km SSE of S...

{"type":"Feature","properties":{"mag":2.92000008,"place":"49 km SE...

{"type":"Feature","properties":{"mag":1.4,"place":"44 km NE of Sus...

--пропуск--

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

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

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

eq_explore_data.py

from pathlib import Path

import json

 

# Считывает данные в строковом формате и преобразует в объект Python.

path = Path('eq_data/eq_data_1_day_m1.geojson')

contents = path.read_text()

❶ all_eq_data = json.loads(contents)

 

# Создает удобную для чтения версию файла данных.

❷ path = Path('eq_data/readable_eq_data.geojson')

❸ readable_contents = json.dumps(all_eq_data, indent=4)

path.write_text(readable_contents)

Мы считываем данные из файла в строковом формате и вызываем функцию json.loads() для преобразования строкового представления файла в объект Python . Такой же подход мы использовали в главе 10. В этом случае весь набор данных преобразуется в один словарь, который мы присваиваем переменной all_eq_data. Затем мы определяем новую переменную path, в которой можем сохранить полученные данные в формате, более удобном для чтения . Функция json.dumps(), которая упоминалась в главе 10, поддерживает необязательный аргумент indent , позволяющий указать размер отступа вложенных элементов в структуре данных.

Перейдите в каталог eq_data и откройте файл readable_eq_data.json. Начальная часть выглядит так:

readable_eq_data.json

{

    "type": "FeatureCollection",

❶     "metadata": {

        "generated": 1649052296000,

        "url": "https://earthquake.usgs.gov/earthquakes/.../1.0_day.geojson",

        "title": "USGS Magnitude 1.0+ Earthquakes, Past Day",

        "status": 200,

        "api": "1.10.3",

        "count": 160

    },

❷     "features": [

    --пропуск--

В первую часть файла включена секция с ключом "metadata" . По ней можно определить, когда файл был сгенерирован и где можно найти данные в Интернете. Кроме того, в ней содержатся понятный заголовок и количество землетрясений, внесенных в файл. За этот 24-часовой период было зарегистрировано 160 землетрясений.

Структура файла GeoJSON хорошо подходит для географических данных. Информация хранится в списке, связанном с ключом "features" . В файле содержится информация о землетрясениях, так что эти данные имеют форму списка, в котором каждый элемент соответствует одному землетрясению. На первый взгляд структура кажется запутанной, но она весьма полезна. Например, геолог может сохранить в словаре столько информации о каждом землетрясении, сколько потребуется, а затем объединить все словари в один большой список.

Рассмотрим словарь, представляющий одно землетрясение:

readable_eq_data.json

    --пропуск--

        {

            "type": "Feature",

❶             "properties": {

                "mag": 1.6,

                --пропуск--

❷                 "title": "M 1.6 - 27 km NNW of Susitna, Alaska"

            },

❸             "geometry": {

                "type": "Point",

                "coordinates": [

❹                     -150.7585,

❺                     61.7591,

                    56.3

                ]

            },

            "id": "ak0224bju1jx"

        },

Ключ "properties" содержит подробную информацию о каждом землетрясении . Нас прежде всего интересует их магнитуда, связанная с ключом "mag". Представляет интерес и заголовок каждого землетрясения, содержащий удобную сводку магнитуды и координат .

Ключ "geometry" помогает определить, где произошло землетрясение . Эта информация потребуется для географической привязки событий. Долгота и широта каждого землетрясения содержатся в списке, связанном с ключом "coordinates".

Уровень вложенности в этом коде намного выше, чем мы использовали бы в своем коде, и если он покажется запутанным — не огорчайтесь; Python берет на себя обработку большей части сложности. В любой момент времени мы будем работать с одним или двумя уровнями. Мы начнем с извлечения словаря для каждого землетрясения, зарегистрированного за 24-часовой период.

ПРИМЕЧАНИЕ

В географических координатах часто сначала указывается широта, а затем долгота. Вероятно, эта система обозначений возникла из-за того, что люди открыли широту задолго до того, как была создана концепция долготы. Тем не менее во многих геопространственных библиотеках сначала указывается долгота, а потом широта, поскольку этот порядок соответствует системе обозначений (x, y), используемой в математических представлениях. Формат GeoJSON использует систему записи (долгота, широта), и если вы будете работать с другой библиотекой, то очень важно заранее узнать, какая система в ней предусмотрена.

Создание списка всех землетрясений

Начнем с создания списка, содержащего всю информацию обо всех произошедших землетрясениях.

eq_explore_data.py

from pathlib import Path

import json

 

# Чтение данных в строковом формате и преобразование в объект Python.

path = Path('eq_data/eq_data_1_day_m1.geojson')

contents = path.read_text()

all_eq_data = json.loads(contents)

 

# Обработка всех землетрясений в наборе данных.

all_eq_dicts = all_eq_data['features']

print(len(all_eq_dicts))

Мы берем данные, связанные с ключом 'features', и сохраняем их в all_eq_data. Известно, что файл содержит данные 160 землетрясений, а вывод подтверждает, что были прочитаны данные всех землетрясений:

160

Обратите внимание на то, каким коротким получился код. Отформатированный файл readable_eq_data.json содержит более 6000 строк. Всего в нескольких строках кода мы прочитали все данные и сохранили их в списке Python. Теперь извлечем данные магнитуд по каждому землетрясению.

Извлечение магнитуд

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

eq_explore_data.py

--пропуск--

all_eq_dicts = all_eq_data['features']

 

❶ mags = []

for eq_dict in all_eq_dicts:

❷     mag = eq_dict['properties']['mag']

    mags.append(mag)

 

print(mags[:10])

Создадим пустой список для хранения магнитуд, а затем переберем в цикле словарь all_eq_dicts . Внутри цикла каждое землетрясение представляется словарем eq_dict. Магнитуда каждого землетрясения хранится в секции 'properties' словаря с ключом 'mag' . Каждая магнитуда сохраняется в переменной mag и присоединяется к списку mags.

Выведем первые десять магнитуд, чтобы убедиться в том, что были получены правильные данные:

[1.6, 1.6, 2.2, 3.7, 2.92000008, 1.4, 4.6, 4.5, 1.9, 1.8]

Далее мы извлечем данные местоположения (то есть координаты) для каждого землетрясения, а затем создадим карту землетрясений.

Извлечение данных о местоположении

Данные о местоположении хранятся с ключом "geometry". В словаре geometry есть ключ "coordinates", первыми двумя значениями которого являются долгота и широта. Извлечение данных происходит следующим образом:

eq_explore_data.py

--пропуск--

all_eq_dicts = all_eq_data['features']

 

mags, lons, lats = [], [], []

for eq_dict in all_eq_dicts:

    mag = eq_dict['properties']['mag']

❶     lon = eq_dict['geometry']['coordinates'][0]

    lat = eq_dict['geometry']['coordinates'][1]

    mags.append(mag)

    lons.append(lon)

    lats.append(lat)

 

print(mags[:10])

print(lons[:5])

print(lats[:5])

Для долгот и широт создаются пустые списки. Выражение eq_dict['geometry'] обращается к словарю, представляющему элемент geometry данных землетрясения . Второй ключ 'coordinates' извлекает список значений, связанных с ключом 'coordinates'. Наконец, индекс 0 запрашивает первое значение в списке координат, соответствующее долготе землетрясения.

При выводе первых пяти долгот и широт становится видно, что данные были извлечены правильно:

[1.6, 1.6, 2.2, 3.7, 2.92000008, 1.4, 4.6, 4.5, 1.9, 1.8]

[-150.7585, -153.4716, -148.7531, -159.6267, -155.248336791992]

[61.7591, 59.3152, 63.1633, 54.5612, 18.7551670074463]

Имея эти данные, можно переходить к нанесению координат землетрясений на географическую карту.

Создание карты мира

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

eq_world_map.py

from pathlib import Path

import json

 

import plotly.express as px

 

--пропуск--

for eq_dict in all_eq_dicts:

    --пропуск--

 

title = 'Global Earthquakes'

❶ fig = px.scatter_geo(lat=lats, lon=lons, title=title)

fig.show()

Мы импортируем модуль plotly.express под псевдонимом px, как и в главе 15. Функция scatter_geo() позволяет наложить на карту диаграмму разброса географических данных. В простейшем варианте диаграммы вам нужно предоставить только список широт и долгот. Мы передаем список lats в качестве аргумента lat, а lons — в качестве аргумента lon.

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

16_07.tif 

Рис. 16.7. Простая карта с информацией о землетрясениях, произошедших за последние 24 часа

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

Представление магнитуд землетрясений

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

--пропуск--

# Чтение данных в строковом формате и преобразование в объект Python.

path = Path('eq_data/eq_data_30_day_m1.geojson')

contents = path.read_text()

--пропуск--

 

title = 'Global Earthquakes'

fig = px.scatter_geo(lat=lats, lon=lons, size=mags, title=title)

fig.show()

Мы загружаем файл eq_data_30_day_m1.geojson, чтобы добавить на карту данные о землетрясениях за полные 30 дней. Мы также используем аргумент size в вызове функции px.scatter_geo(), чтобы изменять размер точек на карте. С помощью size мы передаем список магнитуд mags, и теперь землетрясения с большей магнитудой будут отображаться на карте в виде более крупной точки.

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

16_08.tif 

Рис. 16.8. Теперь на карте отображаются магнитуды всех землетрясений за последние 30 дней

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

Настройка цвета точек на карте

С помощью Plotly мы можем настроить цвет каждого маркера в зависимости от магнитуды соответствующего землетрясения. Мы также изменим проекцию для самой карты.

eq_world_map.py

--пропуск--

fig = px.scatter_geo(lat=lats, lon=lons, size=mags, title=title,

❶         color=mags,

❷         color_continuous_scale='Viridis',

❸         labels={'color':'Magnitude'},

❹         projection='natural earth',

    )

fig.show()

Все существенные изменения вносятся в вызов функции px.scatter_geo(). Аргумент color определяет, какие значения цветовой шкалы следует использовать для окрашивания каждого маркера . Мы используем список mags для определения цвета каждой точки, как и в случае с аргументом size.

Аргумент color_continuous_scale ссылается на используемую цветовую шкалу . Viridis — это цветовая шкала с оттенками от темно-синего до ярко-желтого, которая хорошо подходит для нашего набора данных. По умолчанию цветовая шкала в правой части карты сопровождается меткой color; это слово не отражает предназначение цветовой дифференциации. Аргумент labels, продемонстрированный впервые в главе 15, принимает словарь в качестве значения . Нам нужна только одна пользовательская метка на нашей карте, чтобы цветовая шкала была обозначена Magnitude вместо color.

Мы использовали еще один аргумент, меняющий карту землетрясений. Аргумент projection принимает одну из распространенных картографических проекций . В нашем примере мы используем проекцию 'natural earth', которая округляет края карты. Обратите также внимание на запятую после этого аргумента. Если вызов функции содержит длинный список аргументов, занимающий несколько строк, то обычно в конце ставят запятую, чтобы при необходимости добавить еще один аргумент на следующей строке.

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

16_09.tif 

Рис. 16.9. Цвета и размеры маркеров представляют магнитуду землетрясений за последние 30 дней

Другие цветовые шкалы

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

>>> import plotly.express as px

>>> px.colors.named_colorscales()

['aggrnyl', 'agsunset', 'blackbody', ..., 'mygbm']

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

Добавление подсказки

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

Для этого нужно извлечь из файла еще немного данных:

eq_world_map.py

--пропуск--

❶ mags, lons, lats, eq_titles = [], [], [], []

    mag = eq_dict['properties']['mag']

    lon = eq_dict['geometry']['coordinates'][0]

    lat = eq_dict['geometry']['coordinates'][1]

❷     eq_title = eq_dict['properties']['title']

    mags.append(mag)

    lons.append(lon)

    lats.append(lat)

    eq_titles.append(eq_title)

 

title = 'Global Earthquakes'

fig = px.scatter_geo(lat=lats, lon=lons, size=mags, title=title,

        --пропуск--

        projection='natural earth',

❸         hover_name=eq_titles,

    )

fig.show()

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

В вызове px.scatter_geo() мы передаем содержимое переменной eq_titles в качестве аргумента hover_name . Теперь Plotly добавит информацию о каждом землетрясении в текст подсказки для каждой точки. Запустив готовую программу, вы сможете навести указатель мыши на любой маркер, увидеть описание местоположения землетрясения и узнать его точную магнитуду. Пример показан на рис. 16.10.

16_10.tif 

Рис. 16.10. Подсказка, появляющаяся при наведении указателя мыши, содержит краткое описание каждого землетрясения

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

Упражнения

16.6. Рефакторинг. В цикле, извлекающем данные из словаря all_eq_dicts, используются переменные для сохранения магнитуды, долготы, широты и заголовка каждого землетрясения перед присоединением этих значений к соответствующим спискам. Такой подход был выбран для того, чтобы процесс извлечения данных из файла GeoJSON был более понятным, но в вашем коде он необязателен. Вместо того чтобы использовать временные переменные, извлеките каждое значение из словаря eq_dict и присоедините его к соответствующему списку в одной строке. В результате тело цикла сократится до четырех строк.

16.7. Автоматизированный заголовок. В этом разделе мы использовали общий заголовок Global Earthquakes. Вместо этого можно воспользоваться заголовком набора данных из метаданных файла GeoJSON. Извлеките это значение и присвойте его переменной title.

16.8. Недавние землетрясения. В Интернете доступны файлы данных с информацией о последних землетрясениях за 1-часовой, 1-дневный, 7-дневный и 30-дневный период. Откройте страницу https://earthquake.usgs.gov/earthquakes/feed/v1.0/geojson.php и найдите список ссылок на наборы данных за разные перио­ды времени. Скачайте один из этих наборов и создайте визуализацию последней сейсмической активности.

16.9. Пожары. В дополнительных материалах к этой главе есть файл world_fires_1_day.csv. Он содержит информацию о пожарах по всему миру, в том числе долготу, широту и площадь каждого пожара. Используя процедуру обработки данных из первой части этой главы и картографические средства из этого раздела, создайте карту с информацией о том, в каких частях мира происходят пожары.

Обновленные версии этих данных можно скачать по адресу https://earthdata.na­sa.gov/earth-observation-data/near-real-time/irms/active-fire-data/. Ссылки на данные в формате CSV находятся в разделах SHP, KML и TXT.

Резюме

В этой главе вы научились работать с реальными наборами данных. Вы узнали, как обрабатывать файлы CSV и GeoJSON и как извлечь данные, на которых вы хотите сосредоточиться. На примере реальных погодных данных вы освоили новые возможности работы с библиотекой Matplotlib, такие как использование модуля datetime и добавление нескольких наборов данных в одну диаграмму. Вы узнали, как нанести данные на карту мира с помощью Plotly и как изменить оформление карт.

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

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

Назад: 15. Генерирование данных
Дальше: 17. Работа с API