В этом разделе мы загрузим данные о странах в формате JSON и будем работать с ними при помощи модуля json. Используя удобные средства Pygal для работы с географическими данными, мы построим визуализации, отражающие распределение населения по странам.
Загрузка демографических данных
Скопируйте файл population_data.json, содержащий данные о численности населения большинства стран мира с 1960 по 2010 год, в каталог с программами этой главы. Информация взята из многочисленных наборов данных, бесплатно публикуемых фондом Open Knowledge Foundation (http://data.okfn.org/).
Извлечение необходимых данных
Взглянем на файл population_data.json и попробуем понять, как взяться за обработку данных файла:
population_data.json
[
{
. ."Country Name": "Arab World",
. ."Country Code": "ARB",
. ."Year": "1960",
. ."Value": "96388069"
},
{
. ."Country Name": "Arab World",
. ."Country Code": "ARB",
. ."Year": "1961",
. ."Value": "98882541.4"
},
...
]
Фактически перед нами один длинный список Python. Каждый элемент списка представляет собой словарь с четырьмя ключами: название страны (Country Name), код страны (Country Code), год (Year) и значение (Value), представляющее численность населения. В нашей программе будут использоваться названия стран и численность населения только за 2010 год, поэтому для начала напишем программу, которая выводит только эту информацию:
world_population.py
import json
# Список заполняется данными.
filename = 'population_data.json'
with open(filename) as f:
(1) . .pop_data = json.load(f)
# Вывод населения каждой страны за 2010 год.
(2)for pop_dict in pop_data:
(3) . .if pop_dict['Year'] == '2010':
(4) . . . .country_name = pop_dict['Country Name']
. . . .population = pop_dict['Value']
. . . .print(country_name + ": " + population)
Сначала программа импортирует модуль json, чтобы иметь возможность загружать данные из файла. Загруженные данные сохраняются в списке pop_data (1) . Функция json.load() преобразует данные в формат, с которым может работать Python: в данном случае это список. В точке (2) создается цикл, перебирающий все элементы pop_data. Каждый элемент представляет собой словарь с четырьмя парами «ключ—значение», который сохраняется в переменной pop_dict.
В точке (3) ключ 'Year' каждого словаря проверяется на значение 2010. (Так как все значения population_data.json заключены в кавычки, выполняется сравнение строк.) Если словарь относится к 2010 году, то значение, связанное с ключом 'Country Name', сохраняется в переменной country_name, а значение, связанное с ключом 'Value', сохраняется в переменной population (4). Затем программа выводит название каждой страны и ее население.
Программа выводит последовательность названий стран и численности их населения:
Arab World: 357868000
Caribbean small states: 6880000
East Asia & Pacific (all income levels): 2201536674
...
Zimbabwe: 12571000
Не все данные включают точные названия стран, но это неплохое начало для дальнейшей работы. Теперь данные необходимо преобразовать в формат, с которым может работать Pygal.
Преобразование строк в числовые значения
Все ключи и значения в population_data.json хранятся в строковом формате. Чтобы работать с данными численности населения, необходимо преобразовать строковые значения в числа. Для этого в программе используется функция int():
world_population.py
...
for pop_dict in pop_data:
if pop_dict['Year'] == '2010':
country_name = pop_dict['Country Name']
(1) . . . .population = int(pop_dict['Value'])
(2) . . . .print(country_name + ": " + str(population))
После преобразования (1) все данные численности населения хранятся в числовом формате. При выводе численность населения должна быть преобразована в строку (2). Впрочем, для некоторых значений это изменение приводит к ошибке:
Arab World: 357868000
Caribbean small states: 6880000
East Asia & Pacific (all income levels): 2201536674
...
Traceback (most recent call last):
File "print_populations.py", line 12, in
. .population = int(pop_dict['Value'])
(1) ValueError: invalid literal for int() with base 10: '1127437398.85751'
Необработанные данные часто форматируются непоследовательно, поэтому ошибки в них встречаются достаточно часто. В данном случае ошибка происходит из-за того, что Python не может преобразовать строку с дробным значением '1127437398.85751' в целое число (1) . (Вероятно, дробное значение было получено в результате интерполяции в те годы, в которые перепись населения не производилась.) Чтобы решить эту проблему, мы сначала преобразуем строку в вещественное число, а затем преобразуем вещественное число в целое:
world_population.py
...
for pop_dict in pop_data:
if pop_dict['Year'] == '2010':
country = pop_dict['Country Name']
. . . .population = int(float(pop_dict['Value']))
print(country + ": " + str(population))
Функция float() преобразует строку в целое число, а функция int() отсекает дробную часть и возвращает целое число. Теперь можно вывести полный набор данных численности населения за 2010 год без ошибок:
Arab World: 357868000
Caribbean small states: 6880000
East Asia & Pacific (all income levels): 2201536674
...
Zimbabwe: 12571000
Каждая строка успешно преобразуется сначала в вещественное, а затем в целое число. Обратите внимание: данные хранятся в числовом формате, чтобы их можно было использовать для построения карты распределения населения.
Получение кодов стран
Прежде чем переходить к построению карты, необходимо разобраться еще с одним аспектом данных. Инструментарий Pygal для работы с географическими картами ожидает получить данные в четко определенном формате: страны должны задаваться кодами стран, а численность населения — значениями. Существует несколько стандартных наборов кодов стран, часто применяемых при работе с геополитическими данными; коды, включенные в population_data.json, состоят из трех букв, но Pygal использует систему с двухбуквенными кодами. Нужно найти способ получения двухбуквенных кодов стран по их названиям.
Коды стран Pygal хранятся в модуле i18n (сокращение от «internationalization»). В словаре COUNTRIES двухбуквенные коды стран являются ключами, а названия стран — значениями. Чтобы просмотреть коды, импортируйте словарь из модуля i18n и выведите его ключи и значения:
countries.py
from pygal.i18n import COUNTRIES
(1) for country_code in sorted(COUNTRIES.keys()):
. .print(country_code, COUNTRIES[country_code])
В цикле for ключи сортируются в алфавитном порядке (1) . Затем программа выводит каждый код страны и страну, с которой этот код связан:
ad Andorra
ae United Arab Emirates
af Afghanistan
...
zw Zimbabwe
Напишем функцию, которая перебирает COUNTRIES и возвращает коды стран. Функция будет размещаться в отдельном модуле с именем country_codes, чтобы ее можно было позднее импортировать в программу визуализации:
country_codes.py
from pygal.i18n import COUNTRIES
(1) def get_country_code(country_name):
. ."""Возвращает для заданной страны ее код Pygal, состоящий из 2 букв."""
(2) . .for code, name in COUNTRIES.items():
(3) . . . .if name == country_name:
. . . . . .return code
. .# Если страна не найдена, вернуть None.
(4) . .return None
. . . . . .
print(get_country_code('Andorra'))
print(get_country_code('United Arab Emirates'))
print(get_country_code('Afghanistan'))
Название страны передается функции get_country_code() и сохраняется в параметре country_name (1) . Затем программа перебирает пары «код—название» в COUNTRIES (2). Если название страны будет найдено, функция возвращает код страны (3), а если нет — после цикла добавляется строка, возвращающая None (4). Наконец, программа передает названия трех стран для проверки функции. Как и ожидалось, программа выводит три двухбуквенных кода:
ad
ae
af
Прежде чем переходить к использованию функции, удалите три команды print из country_codes.py.
Затем функция get_country_code() импортируется в world_population.py:
world_population.py
import json
from country_codes import get_country_code
...
# Вывод населения каждой страны за 2010 год.
for pop_dict in pop_data:
if pop_dict['Year'] == '2010':
country_name = pop_dict['Country Name']
population = int(float(pop_dict['Value']))
(1) . . . .code = get_country_code(country_name)
. . . .if code:
(2) . . . . . .print(code + ": "+ str(population))
(3) . . . .else:
. . . . . .print('ERROR - ' + country_name)
После извлечения названия и населения в code сохраняется код страны — или None, если код недоступен (1) . Если код получен, то код и население страны выводятся командой print (2). Если код недоступен, выводится сообщение об ошибке с названием страны, для которого не удалось найти код (3). Запустите программу, и вы увидите коды стран с населением и несколько сообщений об ошибках:
ERROR - Arab World
ERROR - Caribbean small states
ERROR - East Asia & Pacific (all income levels)
...
af: 34385000
al: 3205000
dz: 35468000
...
ERROR - Yemen, Rep.
zm: 12927000
zw: 12571000
Ошибки происходят по двум причинам. Во-первых, классификация в наборе данных не всегда осуществляется по странам; часть статистики относится к регионам или экономическим группам. Во-вторых, в части статистики используется другая запись полных названий стран (Yemen, Rep. вместо Yemen). Пока опустим данные стран, вызывающие ошибки, и посмотрим, как будет выглядеть карта с успешно прочитанными данными.
Построение карты мира
С имеющимися кодами стран карта мира строится легко и просто. В Pygal поддерживается тип диаграммы Worldmap, упрощающий работу с географическими наборами данных. В качестве примера использования Worldmap мы создадим простую карту с данными по Северной, Центральной и Южной Америке:
americas.py
import pygal
(1) wm = pygal.Worldmap()
wm.title = 'North, Central, and South America'
(2)wm.add('North America', ['ca', 'mx', 'us'])
wm.add('Central America', ['bz', 'cr', 'gt', 'hn', 'ni', 'pa', 'sv'])
wm.add('South America', ['ar', 'bo', 'br', 'cl', 'co', 'ec', 'gf',
. .'gy', 'pe', 'py', 'sr', 'uy', 've'])
. .
(3)wm.render_to_file('americas.svg')
В точке (1) мы создаем экземпляр класса Worldmap и задаем атрибут title объекта карты. В точке (2) используется метод add(), который получает метку и список кодов стран, на которых вы хотите сосредоточиться. Каждый вызов add() создает новый цвет для набора стран и добавляет этот цвет в список условных обозначений в левой части диаграммы с заданным текстом. Весь регион Северной Америки будет представлен одним цветом, поэтому мы включаем коды 'ca', 'mx' и 'us' в список, передаваемый первому вызову add(), для единого представления на карте Канады, Мексики и Соединенных Штатов. Затем то же самое делается для стран Центральной и Южной Америки.
Метод render_to_file() в точке (3) создает файл .svg с диаграммой; вы можете открыть этот файл в своем браузере. На полученной карте Северная, Центральная и Южная Америка выделены другими цветами (рис. 16.7).
Рис. 16.7. Простой экземпляр диаграммы Worldmap
Теперь вы знаете, как создать карту с цветными областями, условные обозначения и аккуратные метки. Добавим на карту данные для вывода информации о стране.
Нанесение числовых данных на карту мира
Чтобы потренироваться с нанесением числовых данных на карту, создайте карту с населением трех стран Северной Америки:
na_populations.py
import pygal
wm = pygal.Worldmap()
wm.title = 'Populations of Countries in North America'
(1) wm.add('North America', {'ca': 34126000, 'us': 309349000, 'mx': 113423000})
. .
wm.render_to_file('na_populations.svg')
Рис. 16.8. Численность населения стран Северной Америки
Сначала мы создаем экземпляр Worldmap и назначаем заголовок. Далее снова следует вызов add(), но на этот раз во втором аргументе передается словарь вместо списка (1) . Словарь содержит двухбуквенные коды стран Pygal (ключи) и численность населения (значения). Pygal автоматически использует числа для окраски стран от светлых (менее населенные) до темных (наиболее населенные). На рис. 16.8 показана полученная карта.
Эта карта интерактивна: если вы наведете указатель мыши на каждую страну, то увидите ее население. Добавим на карту побольше данных.
Построение полной карты населения
Чтобы нанести на карту данные численности населения для других стран, обработанные ранее данные необходимо преобразовать в формат словаря Pygal: с двухбуквенными кодами стран и численностью населения, образующими пары «ключ—значение». Добавьте следующий код в world_population.py:
world_population.py
import json
import pygal
from country_codes import get_country_code
# Список заполняется данными.
...
# Построение словаря с данными численности населения.
(1) cc_populations = {}
for pop_dict in pop_data:
if pop_dict['Year'] == '2010':
country = pop_dict['Country Name']
population = int(float(pop_dict['Value']))
code = get_country_code(country)
if code:
(2) . . . . . .cc_populations[code] = population
(3)wm = pygal.Worldmap()
wm.title = 'World Population in 2010, by Country'
(4)wm.add('2010', cc_populations)
. .
wm.render_to_file('world_population.svg')
Сначала импортируется модуль pygal. В точке (1) создается пустой словарь для хранения кодов стран и численности населения в формате, принятом Pygal. В точке (2) для полученных кодов строится очередной элемент словаря cc_populations; ключом пары становится код страны, а значением — численность населения. Также из программы удаляются все команды print.
Мы создаем экземпляр Worldmap и задаем его атрибут title (3). При вызове add() передается словарь с кодами стран и значениями численности населения (4).
На рис. 16.9 изображена полученная карта.
Несколько стран, для которых данные отсутствуют, окрашены в черный цвет, но большинство стран раскрашено в соответствии с размером населения. Проблемой отсутствующих данных мы займемся позднее в этой главе, а сначала приведем тон закраски в соответствие с населением стран. В настоящее время на карте слишком
Рис. 16.9. Численность мирового населения в 2010 году
много стран окрашено в светлые тона, а стран с темной окраской всего две. Контраст между большинством стран попросту недостаточен для того, чтобы зритель мог понять, в какой стране больше или меньше население. Чтобы решить эту проблему, мы сгруппируем страны по уровням населения и окрасим каждую группу по отдельности.
Группировка стран по населению
Китай и Индия по численности населения опережают все остальные страны, поэтому нашей карте не хватает контраста. И в Китае, и в Индии проживает свыше миллиарда человек, тогда как в следующей по численности населения стране — Соединенных Штатах — население составляет около 300 миллионов. Вместо того чтобы наносить на диаграмму все страны в одной группе, разделим страны на три уровня населения: менее 10 миллионов, от 10 миллионов до 1 миллиарда и более 1 миллиарда:
world_population.py
...
# Построение словаря с данными численности населения.
cc_populations = {}
for pop_dict in pop_data:
if pop_dict['Year'] == '2010':
--snip--
if code:
cc_populations[code] = population
# Группировка стран по 3 уровням населения.
(1) cc_pops_1, cc_pops_2, cc_pops_3 = {}, {}, {}
(2)for cc, pop in cc_populations.items():
. .if pop < 10000000:
. . . .cc_pops_1[cc] = pop
. .elif pop < 1000000000:
. . . .cc_pops_2[cc] = pop
. .else:
. . . .cc_pops_3[cc] = pop
# Проверка количества стран на каждом уровне.
(3)print(len(cc_pops_1), len(cc_pops_2), len(cc_pops_3))
wm = pygal.Worldmap()
wm.title = 'World Population in 2010, by Country'
x wm.add('0-10m', cc_pops_1)
wm.add('10m-1bn', cc_pops_2)
wm.add('>1bn', cc_pops_3)
. .
wm.render_to_file('world_population.svg')
Чтобы сгруппировать страны, мы создаем пустой словарь для каждой категории (1) . Затем программа перебирает cc_populations и проверяет население каждой страны (2). Блок if-elif-else добавляет элемент в соответствующий словарь (cc_pops_1, cc_pops_2 или cc_pops_3) для каждой пары «код страны—население».
Рис. 16.10. Численность мирового населения в 2010 году
В точке (3) выводится длина каждого словаря для определения размеров групп. При нанесении данных на диаграмму (4) все три группы добавляются на диаграмму Worldmap. При запуске программы сначала выводятся размеры всех групп:
85 69 2
Вывод показывает, что существуют 85 стран с населением менее 10 миллионов, 69 стран с населением от 10 миллионов до 1 миллиарда и две особые страны с населением свыше 1 миллиарда. Разбиение получается достаточно равномерным для получения содержательной карты. Полученная карта изображена на рис. 16.10.
Три разных цвета помогают подчеркнуть различия между уровнями населения. В каждом из трех уровней страны окрашиваются от светлого к темному оттенку в соответствии с ростом численности населения.
Оформление карты мира в Pygal
Группировка стран на карте работает эффективно, но цвета по умолчанию выбираются довольно странно: например, в нашем примере Pygal выбирает схему с ярко-розовым и зеленым цветом. Директивы оформления Pygal помогут решить проблему с цветами.
В новой версии мы снова прикажем Pygal использовать один базовый цвет, но на этот раз выберем цвет и применим более выразительные оттенки для трех групп численности населения:
world_population.py
import json
import pygal
(1) from pygal.style import RotateStyle
...
# Группировка стран по 3 уровням населения.
cc_pops_1, cc_pops_2, cc_pops_3 = {}, {}, {}
for cc, pop in cc_populations.items():
. .if pop < 10000000:
. . . ....
(2)wm_style = RotateStyle('#336699')
(3)wm = pygal.Worldmap(style=wm_style)
wm.title = 'World Population in 2010, by Country'
...
Стили Pygal хранятся в модуле style, из которого программа импортирует стиль RotateStyle (1) . Этот класс получает один аргумент — цвет RGB в шестнадцатеричном формате (2). Затем Pygal выбирает цвета каждой группы на основании переданного цвета. Цвет в шестнадцатеричном формате представляет собой строку из символа решетки (#), за которым следуют шесть символов: первые два представляют красную составляющую цвета, следующие два — зеленую и последние два — синюю. Значения составляющих лежат в диапазоне от 00 (нулевая интенсивность) до FF (максимальная интенсивность). В Интернете можно легко найти приложение для экспериментов с цветами и получения соответствующих значений RGB. Цвет, используемый в данном случае (#336699), содержит немного красного (33), чуть больше зеленого (66) и еще больше синего (99). В результате RotateStyle назначается светло-синий базовый цвет для выполнения дальнейших операций.
RotateStyle возвращает объект стиля, который сохраняется в переменной wm_style. Чтобы использовать объект стиля, передайте его в именованном аргументе при создании экземпляра Worldmap (3). На рис. 16.11 изображена обновленная диаграмма.
Рис. 16.11. Три уровня численности населения в общей цветовой теме
Стилевое оформление придает карте целостный внешний вид с хорошо различимыми группами.
Осветление темы
По умолчанию Pygal использует темные темы оформления. Для печати я осветлил стиль своих диаграмм при помощи класса LightColorizedStyle. Этот класс изменяет общую тему оформления диаграммы, включая фон и метки, а также цвета отдельных стран. Чтобы использовать его, сначала необходимо импортировать стиль:
from pygal.style import LightColorizedStyle
Затем вы сможете использовать LightColorizedStyle в программе:
wm_style = LightColorizedStyle
Однако этот класс не позволяет напрямую управлять используемым цветом, поэтому Pygal выбирает базовый цвет по умолчанию. Чтобы назначить цвет, используйте LightColorizedStyle в качестве базового стиля для RotateStyle. Импортируйте LightColorizedStyle и RotateStyle:
from pygal.style import LightColorizedStyle, RotateStyle
Создайте стиль с использованием RotateStyle, но передайте дополнительный аргумент base_style:
wm_style = RotateStyle('#336699', base_style=LightColorizedStyle)
В результате для карты используется светлая общая тема, но цвета стран выбираются на основе цвета, переданного в аргументе. При использовании этого стиля ваши диаграммы будут больше похожи на снимки экрана на иллюстрациях.
Пока вы экспериментируете с поиском стилевых директив, хорошо подходящих для тех или иных визуализаций, попробуйте использовать псевдонимы в командах import:
from pygal.style import LightColorizedStyle as LCS, RotateStyle as RS
Определения стилей с псевдонимами получаются более короткими:
wm_style = RS('#336699', base_style=LCS)
Как видите, даже небольшой набор стилевых директив открывает широкие возможности для управления внешним видом диаграмм и карт в Pygal.
Упражнения
16-5. Все страны: на картах, построенных в этом разделе, наша программа не смогла автоматически найти двухбуквенные коды примерно для 12 стран. Определите, что это за страны, и найдите коды в словаре COUNTRIES. Добавьте блок if-elif в get_country_code(), чтобы функция возвращала правильные коды для этих конкретных стран:
if country_name == 'Yemen, Rep.'
return 'ye'
elif ...
Разместите этот код после цикла COUNTRIES, но перед командой return None. Когда это будет сделано, карта станет более полной.
16-6. Валовый внутренний продукт: Фонд Open Knowledge Foundation предоставляет набор данных с величиной валового внутреннего продукта (ВВП) по каждой стране мира; его можно загрузить по адресу http://data.okfn.org/data/core/gdp/. Загрузите версию этого набора данных в формате JSON и нанесите на карту ВВП каждой страны мира за самый последний год в наборе данных.
16-7. Выберите данные самостоятельно: Всемирный банк предоставляет различные наборы данных, разбитые по странам. Откройте страницу http://data.worldbank.org/indicator/ и найдите набор данных, который покажется вам интересным. Щелкните на наборе данных, щелкните по ссылке Download Data и выберите формат CSV. Вы получите три файла CSV, два из которых снабжены пометкой Metadata; используйте третий файл CSV. Напишите программу для генерирования словаря; ключами словаря являются двухбуквенные коды стран Pygal, а значениями — выбранные вами данные из файла. Нанесите данные на диаграмму Worldmap и оформите карту на свое усмотрение.
16-8. Тестирование модуля country_codes: во время разработки модуля country_codes мы использовали команды print для проверки работоспособности функции get_country_code(). Напишите нормальный тест для этой функции, используя информацию из главы 11.