/
Tags: языки программирования компьютерные технологии
ISBN: 978-5-9775-1846-8
Text
Санкт-Петербург
«БХВ-Петербург»
2024
УДК 004.43
ББК 32.973.26-018.1
М15
М15
Макгрегор С.
Обработка данных на Python. Data Wrangling и Data Quality: Пер. с англ. —
СПб.: БХВ-Петербург, 2024. — 432 с.: ил.
ISBN 978-5-9775-1846-8
Книга посвящена первичной обработке данных (Data Wrangling) на Python и
оценке их качества (Data Quality). Материал содержит основополагающие концепции,
экспертные советы и ресурсы, необходимые для первичной обработки, извлечения,
оценки и анализа данных. Все темы раскрыты на простых и наглядных примерах
из практики. Даны необходимые и достаточные сведения о языке программирования Python 3.8+ для чтения, записи и преобразования данных из различных источников, а также для обработки данных в больших масштабах. Приведены лучшие
практики документирования и структурирования кода. Описан сбор данных из
файлов, веб-страниц и API. Рассмотрены приемы проведения базового статистического анализа наборов данных, а также наглядные и убедительные способы визуализации и представления данных. Изложение рассчитано как на новичков по обработке данных, так и на профессионалов.
Электронный архив на сайте издательства содержит цветные иллюстрации к книге.
Для специалистов по обработке данных
УДК 004.43
ББК 32.973.26-018.1
Научный редактор:
Архитектор решений, IT-компания «Яндекс»
Дмитрий Бардин
Группа подготовки издания:
Руководитель проекта
Зав. редакцией
Перевод с английского
Редактор
Компьютерная верстка
Оформление обложки
Евгений Рыбаков
Людмила Гауль
Михаила Райтмана
Наталья Смирнова
Натальи Смирновой
Зои Канторович
© 2023 BHV
Authorized Russian translation of the English edition of Practical Python Data Wrangling and Data Quality
ISBN 9781492091509 © 2022 Susan McGregor.
This translation is published and sold by permission of O’Reilly Media, Inc., which owns or controls all
rights to publish and sell the same.
Авторизованный перевод с английского языка на русский издания
Practical Python Data Wrangling and Data Quality
ISBN 9781492091509 © 2022 Susan McGregor.
Перевод опубликован и продается с разрешения компании-правообладателя O’Reilly Media, Inc.
"БХВ-Петербург", 191036, Санкт-Петербург, Гончарная ул., 20.
ISBN 978-1-492-09150-9 (англ.)
ISBN 978-5-9775-1846-8 (рус.)
© Susan McGregor, 2022
© Перевод на русский язык, оформление.
ООО "БХВ-Петербург", ООО "БХВ", 2023
Содержание
ВВЕДЕНИЕ .............................................................................................................................. 11
Для кого предназначена эта книга? ..................................................................................... 12
Поедете сами или возьмете такси? ...................................................................................... 12
Кому не следует читать эту книгу?...................................................................................... 13
Что ожидать от этой книги ................................................................................................... 13
Типографские соглашения.................................................................................................... 14
Использование примеров кода ............................................................................................. 15
Цветные иллюстрации .......................................................................................................... 16
Возможности онлайнового обучения от компании O'Reilly ............................................. 16
Как связаться с нами ............................................................................................................. 16
Благодарности........................................................................................................................ 17
ГЛАВА 1. Введение в первичную обработку (выпас) и качество данных ............... 19
Что такое выпас данных?...................................................................................................... 20
Что такое качество данных? ................................................................................................. 22
Целостность данных ...................................................................................................... 23
Соответствие данных..................................................................................................... 23
Почему мы выбрали Python? ................................................................................................ 25
Универсальность............................................................................................................ 25
Доступность.................................................................................................................... 25
Удобочитаемость ........................................................................................................... 26
Сообщество .................................................................................................................... 26
Альтернативы языку Python ................................................................................................. 27
Написание и исполнение программ на языке Python......................................................... 27
Работа с кодом Python на локальном устройстве ............................................................... 30
Введение в работу с командной строкой..................................................................... 30
Установка языка Python, среды Jupyter Notebook и редактора кода ......................... 33
Работа с кодом Python в режиме онлайн............................................................................. 39
Hello World!............................................................................................................................ 39
Создание файла автономного сценария Python при помощи Atom .......................... 39
Создание блокнота кода Python в Jupyter Notebook ................................................... 41
Создание блокнота кода Python в Google Colab.......................................................... 42
Создаем программу ............................................................................................................... 43
В файле автономного сценария .................................................................................... 43
В блокноте ...................................................................................................................... 43
6
| Содержание
Исполняем программу .......................................................................................................... 43
В файле автономного сценария .................................................................................... 43
В блокноте ...................................................................................................................... 44
Документирование, сохранение и управление версиями своего кода ............................. 44
Документирование......................................................................................................... 44
Сохранение ..................................................................................................................... 46
Управление версиями .................................................................................................... 46
Заключение ............................................................................................................................ 56
ГЛАВА 2. Введение в Python ............................................................................................. 57
"Части речи" языков программирования ............................................................................ 58
Существительные ≈ переменные.................................................................................. 59
Важно ли конкретное имя? ........................................................................................... 61
Наилучшие практики для именования переменных................................................... 62
Глаголы ≈ функциям...................................................................................................... 62
Применение пользовательских функций..................................................................... 67
Библиотеки: занимаем пользовательские функции у других программистов ......... 68
Структуры управления: циклы и условные операторы ..................................................... 69
Циклы.............................................................................................................................. 69
Условные операторы ..................................................................................................... 72
Понимание ошибок ............................................................................................................... 77
Синтаксические ошибки ............................................................................................... 78
Ошибки времени исполнения ....................................................................................... 79
Логические ошибки ....................................................................................................... 83
Отправляемся в путь с данными Citi Bike........................................................................... 85
Начинаем с создания псевдокода ................................................................................. 86
Масштабирование.......................................................................................................... 92
Заключение ............................................................................................................................ 93
ГЛАВА 3. Понимание качества данных ......................................................................... 95
Оценка соответствия данных ............................................................................................... 97
Достоверность данных .................................................................................................. 98
Надежность данных ..................................................................................................... 100
Репрезентативность данных........................................................................................ 101
Оценка целостности данных .............................................................................................. 104
Необходимые, но недостаточные ............................................................................... 106
Важные.......................................................................................................................... 108
Достижимость .............................................................................................................. 111
Улучшение качества данных.............................................................................................. 114
Очистка данных ........................................................................................................... 115
Аугментация данных ................................................................................................... 115
Заключение .......................................................................................................................... 116
Содержание
| 7
ГЛАВА 4. Работа с файловыми и канальными данными
на языке Python.................................................................................................................. 117
Структурированные и неструктурированные данные ..................................................... 119
Работа со структурированными данными......................................................................... 123
Файловые табличные форматы данных..................................................................... 124
Выпас табличных данных посредством языка Python ............................................. 126
Выпас реальных данных: понимание безработицы ......................................................... 133
XLSX, ODS и все остальные....................................................................................... 136
Данные фиксированной ширины ............................................................................... 143
Канальные данные — интерактивные обновления через Интернет ....................... 147
Выпас канальных данных средствами языка Python ................................................ 150
Формат JSON: данные следующего поколения ........................................................ 160
Работа с неструктурированными данными....................................................................... 165
Текст на основе изображений: доступ к данным в формате PDF ........................... 165
Выпас PDF-данных, используя Python....................................................................... 166
Обращение к таблицам PDF посредством Tabula ..................................................... 171
Заключение .......................................................................................................................... 171
ГЛАВА 5. Доступ к интернет-данным........................................................................... 173
Доступ к веб-данным XML и JSON ................................................................................... 175
Знакомство с API-интерфейсами ....................................................................................... 178
Базовые API-интерфейсы на примере поисковой системы ............................................. 179
Специализированные API-интерфейсы: добавление простой
аутентификации ........................................................................................................... 181
Получение ключа для API-интерфейса FRED........................................................... 181
Использование ключа API для запроса данных ........................................................ 182
Чтение документации по API-интерфейсу........................................................................ 183
Защита своего ключа API при использовании сценариев Python ................................... 186
Создание файла учетных данных ............................................................................... 188
Использование учетных данных в отдельном сценарии.......................................... 189
Основы работы с файлом .gitignore............................................................................ 190
Специализированные API-интерфейсы: работа с протоколом OAuth ........................... 193
Получение учетной записи разработчика Twitter ..................................................... 194
Создание приложения и учетных данных Twitter..................................................... 196
Кодирование ключа API и ключа секрета API .......................................................... 201
Запрос токена доступа и данных из API-интерфейса Twitter .................................. 202
Этические нормы при работе с API-интерфейсами ......................................................... 206
Извлечение веб-данных: источник данных последней надежды.................................... 207
Осторожно извлекаем данные с веб-сайта УГПТ ..................................................... 210
Использование средств инспектирования браузера ................................................. 211
Решение Python для извлечения данных из веб-страницы:
библиотека Beautiful Soup........................................................................................... 214
Заключение .......................................................................................................................... 218
8
| Содержание
ГЛАВА 6. Оценка качества данных .............................................................................. 219
Пандемия и программа PPP................................................................................................ 221
Оценка целостности данных .............................................................................................. 222
Имеют ли данные известное происхождение?.......................................................... 223
Актуальны ли данные? ................................................................................................ 223
Полные ли данные?...................................................................................................... 224
Хорошо ли данные аннотированы?............................................................................ 236
Являются ли данные крупномасштабными? ............................................................. 242
Непротиворечивы ли данные? .................................................................................... 244
Многомерны ли наши данные? .................................................................................. 248
Атомарны ли данные? ................................................................................................. 250
Понятны ли данные?.................................................................................................... 250
Размерностно структурированы ли данные?............................................................. 252
Оценка соответствия данных ............................................................................................. 253
Достоверность данных ................................................................................................ 253
Надежность данных ..................................................................................................... 257
Репрезентативность данных........................................................................................ 258
Заключение .......................................................................................................................... 259
ГЛАВА 7. Очистка, преобразование и дополнение данных...................................... 261
Выбор подмножества данных системы Citi Bike ............................................................. 262
Простое разбиение ....................................................................................................... 263
Регулярные выражения: супермощное средство сопоставления строк .................. 265
Создание дат................................................................................................................. 270
Удаление хлама из файлов данных.................................................................................... 272
Декодирование дат Excel .................................................................................................... 276
Создание настоящих данных CSV из данных фиксированной ширины........................ 279
Исправление разнообразности написаний ........................................................................ 282
Тернистый путь к "простым" решениям ........................................................................... 288
Опасные подводные камни................................................................................................. 290
Дополнение данных ............................................................................................................ 292
Заключение .......................................................................................................................... 294
ГЛАВА 8. Структурирование и рефакторинг кода .................................................... 296
Обзор пользовательских функций ..................................................................................... 296
Многократное использование кода ............................................................................ 297
Аккуратное и понятное документирование .............................................................. 297
Недостаточная функциональность по умолчанию ................................................... 298
Область видимости.............................................................................................................. 298
Определение параметров функции .................................................................................... 301
Доступные опции ......................................................................................................... 302
Предоставление аргументов ....................................................................................... 303
Возвращаемые значения ..................................................................................................... 303
Работа со стеком .................................................................................................................. 305
Содержание
| 9
Рефакторинг для получения удовольствия и прибыли .................................................... 306
Функция для определения рабочих дней................................................................... 306
Опрятные метаданные ........................................................................................................ 309
Использование pydoc для документирования сценариев и пользовательских
функций................................................................................................................................ 317
О полезности аргументов командной строки ................................................................... 321
Отличия во взаимодействии со сценариями в автономных файлах и блокнотах ......... 325
Заключение .......................................................................................................................... 325
ГЛАВА 9. Введение в анализ данных ............................................................................ 327
Вся суть — в контексте....................................................................................................... 328
Одинаковые, но не совсем .................................................................................................. 329
Что типично? Оценка центральной тенденции ................................................................ 329
Что это значит? ............................................................................................................ 330
Поразмыслим нестандартно: выявляем выбросы............................................................. 332
Визуализация для анализа данных..................................................................................... 332
Какова форма наших данных? Учимся понимать гистограммы ............................. 336
Вопрос за $2 миллиона ....................................................................................................... 346
Пропорциональный ответ................................................................................................... 359
Заключение .......................................................................................................................... 362
ГЛАВА 10. Представление данных ................................................................................ 364
Основы визуального красноречия ..................................................................................... 365
Сформулируйте свои данные ............................................................................................. 367
Диаграммы, графики и картограммы — вот это да! ........................................................ 368
Круговые диаграммы................................................................................................... 369
Линейчатые и столбчатые диаграммы ....................................................................... 372
Линейные диаграммы.................................................................................................. 377
Диаграмма рассеяния................................................................................................... 380
Картограммы ................................................................................................................ 383
Элементы красноречивых визуальных эффектов ............................................................ 386
"Мелкие" детали действительно имеют значение .................................................... 386
Доверяйте своим глазам (и экспертам) ...................................................................... 387
Выбор масштаба........................................................................................................... 388
Выбор цветовой гаммы ............................................................................................... 389
Прежде всего делайте аннотации!.............................................................................. 389
От базового к красивому: настройка визуализации с помощью seaborn и matplotlib... 390
Выйдите за рамки основ ..................................................................................................... 395
Заключение .......................................................................................................................... 396
ГЛАВА 11. За пределами Python..................................................................................... 397
Дополнительные инструменты для анализа данных........................................................ 398
Программы для работы с электронными таблицами................................................ 398
OpenRefine .................................................................................................................... 399
10
| Содержание
Дополнительные инструменты для обмена и представления данных ........................... 402
Редактирование изображений в форматах JPG, PNG и GIF .................................... 402
Программное обеспечение для редактирования SVG и других векторных
форматов ....................................................................................................................... 402
Размышления об этике ........................................................................................................ 404
Заключение .......................................................................................................................... 405
ПРИЛОЖЕНИЕ А. Другие ресурсы по программированию на Python....................... 406
Официальная документация Python................................................................................... 406
Установка ресурсов Python................................................................................................. 407
Где искать библиотеки ................................................................................................ 407
Следите за остротой своих инструментов......................................................................... 408
Где получить больше информации.................................................................................... 409
ПРИЛОЖЕНИЕ Б. Еще несколько слов о Git................................................................... 410
Вы запускаете команду git push/pull и оказываетесь в странном текстовом
редакторе .............................................................................................................................. 410
Ваша команда git push/pull отклоняется............................................................................ 412
Выполните команду git pull ........................................................................................ 412
Краткое руководство по Git................................................................................................ 414
ПРИЛОЖЕНИЕ В. Поиск
данных ....................................................................................... 416
Репозитории данных и API................................................................................................. 416
Эксперты по предметным вопросам.................................................................................. 417
Запросы FOIA/L................................................................................................................... 418
Кастомные методы сбора данных...................................................................................... 419
ПРИЛОЖЕНИЕ Г. Ресурсы
для визуализации и информационного дизайна............ 421
Основополагающие книги по визуализации информации .............................................. 421
Краткое руководство, за которым вы потянетесь ............................................................ 422
Источники вдохновения ..................................................................................................... 422
ОБ АВТОРЕ............................................................................................................................ 423
КОЛОФОН ............................................................................................................................. 424
ПРЕДМЕТНЫЙ УКАЗАТЕЛЬ ................................................................................................. 425
Введение
Добро пожаловать! Если вы взяли в руки эту книгу, вы, вероятно, один из многих
миллионов людей, заинтересованных процессами и возможностями, связанными с
"данными" — этой невероятной, неуловимой новой "валютой", которая меняет наш
образ жизни: как мы живем, работаем и даже общаемся друг с другом. Например,
большинство из нас осознают то обстоятельство, что данные, собираемые в процессе использования наших электронных устройств, используются для формирования
рекламы, рекомендуемого для прослушивания или просмотра различного аудиовизуального материала, а также для выбора результатов поиска — что разместить в
начале списка. Но при этом многие люди могут не знать, что им вполне доступны
инструменты и навыки для доступа, преобразования и получения информации на
основе данных. Цель этой книги — помочь таким людям, в число которых можете
входить и вы, научиться пользоваться данными инструментами.
Данные могут быть доступными или полезными не только для крупных компаний
или государственных статистических организаций. Умение получать доступ к данным, понимать их и извлекать из них полезную информацию является ценным навыком не только для специалистов по работе с данными. В настоящее время средства для эффективного использования данных более доступны, чем когда-либо.
Серьезную работу с данными можно осуществлять, используя бесплатное программное обеспечение и языки программирования, при этом вам не потребуется дорогостоящее оборудование. Например, все упражнения в этой книге были рассчитаны
для выполнения на ноутбуке Chromebook стоимостью менее 500 долларов. Более
того, можно просто использовать бесплатные онлайновые платформы, получая
доступ к ним посредством бесплатного интернет-подключения.
Цель этой книги — предоставить новичкам в области обработки и анализа данных
руководство и уверенность, необходимые, чтобы начать исследовать мир данных —
сначала получая доступ к ним, а затем оценивая их качество. Обеспечив эти фундаментальные навыки, далее мы перейдем к рассмотрению базовых методов анализа и представления данных для извлечения из них значащей информации. В дальнейших главах мы рассмотрим основные навыки, необходимые для выполнения
подробного и содержательного анализа данных и визуализации полученных результатов, используя очищенную версию исходных данных.
12
|
Введение
Для кого предназначена эта книга?
Эта книга предназначена для самых что ни на есть начинающих — вам нужны
лишь самые базовые знания работы с компьютерами (как загрузить файл, запустить
на исполнение программу, копировать, вставлять и т. п.), открытость к новому обучению и желание экспериментировать. Особенно я рекомендую эту книгу тем, кого
пугают данные или программирование, кто думает, что он "слаб в математике", или
кто считает работу с данными или обучение программированию слишком трудной
для себя задачей. Я почти десять лет обучал людей, которые не считали себя технически подкованными, именно тем навыкам, которые рассматриваются в этой книге,
и никогда среди них не было ни одного, для кого овладение этим материалом было
бы действительно не по силам. Исходя из моего опыта, самое трудное в программировании и работе с данными — это не сложность преподаваемого материала, а
качество его преподавания1. Я благодарна всем моим студентам, вопросы которых
в течение многих лет безмерно помогли мне в отыскании способов лучшего представления этого материала, а также за возможность поделиться тем, чему я научилась от них. И хотя никакая книга не может по-настоящему заменить поддержку,
предоставляемую живым преподавателем, я надеюсь, что данная книга, по крайней
мере, даст вам инструменты, необходимые для овладения базовыми знаниями, и,
возможно, вдохновит вас на развитие этих навыков до следующего уровня.
Книга будет полезна для тех, кто уже имеет некоторый опыт по первичной обработке (выпасу) данных, но достигли предела возможностей средств электронных
таблиц, и для тех, кто планирует расширить диапазон форматов данных, поддающихся легкому доступу и манипулированию. Материал будет полезен и для тех,
кто обладает навыками программирования фронтенда (используя, например,
JavaScript или PHP) и ищет способа начать работать с языком Python.
Поедете сами или возьмете такси?
В предисловии к своей книге Program or Be Programmed (Программируй сам или
запрограммируют тебя) из серии OR Books теоретик медиа Дуглас Рушкофф (Douglas Rushkoff) сравнивает программирование с вождением автомобиля. Если вы не
научитесь программированию, пишет он, то вы будете вечным пассажиром в цифровом мире, которого "с одного места в другое перевозят в такси. Но только это
такси без окон, и если водитель говорит вам, что в стране только один супермаркет,
то у вас нет выбора, кроме как верить ему".
"Вы можете поручить задачу программирования другим, — продолжает Рушкофф,
— но тогда вам нужно верить им, что их программы в действительности делают то,
1
Долгое время большой преградой была установка требуемых инструментов. Теперь же для этого
нужно лишь подключение к Интернету.
Введение
|
13
что вы хотите, и таким образом, который отвечает вашим насущным интересам".
В настоящее время последнее предположение все в большей степени стоит под вопросом.
За годы моего преподавания я спросила у нескольких сотен студентов, верили ли
они, что водить автомобиль может научиться любой человек, и они всегда давали
положительный ответ на этот вопрос. В то же самое время мне повстречалось очень
мало людей, которые по-настоящему верят, что научиться программированию может любой человек. Но вождение транспортного средства в действительности намного более сложная задача, чем программирование. Почему же тогда многие из
нас полагают, что программирование будет для них "слишком трудным"?
В этом для меня проявляется настоящая сила аналогии Рушкоффа, поскольку его
"безоконное такси" не только скрывает внешний мир от пассажира, но также скрывает "водителя" от прохожих. Верить в то, что любой может научиться водить автомобиль, легко по той причине, что мы каждый день видим, как это делают самые
разные люди.
Но когда речь заходит о программировании, нам редко выпадает возможность увидеть тех, кто "за рулем". Это означает, что наше представление о том, кто может и
должен заниматься программированием, достаточно ошибочно. Поскольку, если вы
можете водить автомобиль или, скажем, расставить знаки препинания в предложении, я обещаю, что вы также можете и научиться программированию.
Кому не следует читать эту книгу?
Как говорилось ранее, эта книга предназначена для начинающих. Поэтому основные ее разделы полезны для новичков в области анализа или визуализации данных
и вряд ли заинтересуют читателей, обладающих опытом программирования на языке Python или на другом языке, направленном на работу с данными. К счастью, издательство O'Reilly предоставляет большое количество специализированных книг
по сложным темам и библиотекам языка Python, включая книги Python for Data
Analysis (Язык Python для анализа данных), автор Уэс Маккинни (Wes McKinney), и
Python Data Science Handbook (Настольная книга по работе с данными на языке
Python), автор Джейк ВандерПлас (Jake VanderPlas).
Что ожидать от этой книги
Содержимое данной книги следует изучать в порядке его изложения, поскольку
понятия и упражнения каждой главы основаны на материале предыдущих глав. Но
на всем ее протяжении упражнения представлены двумя способами: в виде "блокнотов" с кодом и в виде "автономных" файлов программ — листингов. Этот метод
преследует двойную цель. Во-первых, он позволяет читателям использовать любой
14
|
Введение
предпочитаемый ими или удобный для них подход, а во-вторых, предоставляет
способ для сравнения этих двух методов взаимодействия с кодом на языке Python,
управляемым данными. Исходя из моего личного опыта, "блокноты" с кодом на
языке Python чрезвычайно полезны для быстрого создания рабочих программ, но
этот метод может стать утомительным в случае желания многократного исполнения какой-либо надежной программы. Так как часто код в одном из этих форматов
нельзя просто скопировать и вставить в другой формат, в репозитории GitHub для
данной книги программы предоставляются в обоих этих форматах. Файлы данных
также предоставляются посредством службы Google Drive. По мере решения упражнений вы сможете использовать предпочитаемый вами формат, а также у вас
будет возможность лично увидеть разницу между кодом в этих двух форматах.
Хотя язык Python является основным используемым в этой книге инструментом,
эффективную первичную обработку и анализ данных можно облегчить, используя
ряд инструментов, от текстовых редакторов (программы, используемые для написания кода) до электронных таблиц.
По этой причине некоторые упражнения в этой книге кроме языка Python используют другие бесплатные инструменты и/или инструменты с открытым исходным
кодом. Во всех таких случаях предоставляется объяснение причины использования
конкретного инструмента, а также достаточные инструкции для выполнения задачи
примера.
Типографские соглашения
В книге используются следующие типографские соглашения:
Курсив
Этим шрифтом выделяются новые термины.
Полужирный шрифт
Используется для выделения адресов электронной почты, элементов интерфейса.
Моноширинный шрифт
Выделены листинги программ. Внутри параграфов используется для обозначения элементов программы, таких как имена переменных, функции и типы данных.
Моноширинный шрифт (полужирный)
Обозначает команды или другой текст, который вводится пользователем.
Моноширинный шрифт (курсив)
Используется для обозначения текста, который нужно заменить предоставляемыми пользователем значениями или значениями, определяемыми контекстом.
Введение
|
15
Этот элемент означает совет или предложение.
Данный элемент обозначает общее примечание.
Этот элемент обозначает предупреждение или предостережение.
Использование примеров кода
Дополнительный материал для книги (примеры кода, упражнения и т. п.) можно
загрузить по адресу https://github.com/PracticalPythonDataWranglingAndQuality.
Вопросы по техническим аспектам примеров кода или проблемам с их использованием можно задать, отправив сообщение электронной почтой по адресу
bookquestion@oreily.com.
Код в этой книге предназначен для того, чтобы помочь вам развить свои навыки.
Предоставляемые в этой книге примеры кода можно использовать в своих программах и документации, без необходимости спрашивать нашего разрешения, если
только речь не идет о значительном объеме кода. Например, использование нескольких фрагментов кода из этой книги в своей программе разрешения не требует,
а вот для продажи или распространения примеров из книг издательства O'Reilly
разрешение необходимо. Цитирование текста из этой книги, включая код, разрешения не требует, но включение значительного объема кода из книги в документацию
своего продукта требует разрешения.
При цитировании материалов из наших книг мы будем признательны за предоставление ссылки на источник, но обычно не требуем этого. Формат такой ссылки может включать название, имя автора, издателя и ISBN книги. Например: "Practical
Python Data Wrangling and Data Quality by Susan E. McGregor (O'Reilly). Copyright
2022 Susan E. McGregor, 978-1-492-09150-9".
Если вы считаете, что использование примеров кода выходит за рамки добросовестного использования или указанного выше разрешения, вы можете уточнить этот
вопрос, обратившись к нам по адресу permissions@oreilly.com.
16
|
Введение
Цветные иллюстрации
PDF-файл с цветными рисунками из книги можно скачать по ссылке:
https://zip.bhv.ru/9785977518468.zip, а также со страницы книги на сайте bhv.ru.
Возможности онлайнового обучения
от компании O'Reilly
Свыше 40 лет компания O'Reilly Media предоставляет средства обучения в технологической и предпринимательской областях, необходимые компаниям для достижения успехов в своей деятельности.
Наша уникальная сеть экспертных специалистов и новаторов делятся своими знаниями и опытом посредством книг, статей и нашей онлайновой обучающей платформы. Онлайновая система обучения компании O'Reilly предоставляет доступ (по
требованию) к учебным курсам в режиме реального времени, доскональным методам обучения, интерактивным средам кодирования, а также к огромному собранию
печатного и видеоматериала от самой компании и от свыше 200 других издательств. Чтобы получить дополнительную информацию, посетите наш веб-сайт по
адресу http://oreilly.com.
Как связаться с нами
Если у вас есть какие-либо замечания или вопросы по этой книге, вы можете задать
их издателю по следующему адресу:
O'Reilly Media, Inc.
1005 Gravenstein Highway North
Sebastopol, CA 95472
800-998-9938 (в США или Канаде)
707-829-0515 (международный или местный)
707-829-0104 (факс)
Для этой книги создан веб-сайт, который содержит список ошибок, где приводятся
примеры и предоставляется другая дополнительная информация. Веб-сайт находится
по адресу https://www.oreilly.com/library/view/practical-python-data/9781492091493.
Свои комментарии о книге и технические вопросы присылайте электронной почтой
по адресу bookquestions@oreilly.com.
Чтобы узнать последние новости и информацию о наших книгах и курсах, посетите
наш веб-сайт по адресу http://oreilly.com.
Введение
|
17
Мы на Facebook: http://facebook.com/oreilly.
И в Twitter: http://twitter.com/oreillymedia.
А также на YouTube: http://www.youtube.com/oreillymedia.
Благодарности
Как я уже упоминала ранее, эта книга появилась благодаря моим студентам, которые в процессе обучения были достаточно смелыми, чтобы попробовать что-то новое или изучить различные вопросы. Процесс написания этой книги (не говоря о
самом ее тексте) был безмерно улучшен моим редактором, Джеффом Блайалем
(Jeff Bleiel), который своей дружелюбностью, гибкостью и легкостью подхода
умерил мои крайности, оставляя при этом место для моего личного стиля. Я также
благодарна за содержательные и любезные комментарии моим рецензентам: Джоанне С. Као (Joanna S. Kao), Анн Боннер (Anne Bonner) и Рэнди Ау (Randy Au).
Кроме этого, я хочу сказать спасибо Джесс Хаберман (Jess Haberman), которая помогла мне подготовить материал, а также Джаклин Казил (Jackueline Kazil) и Катрин Джармул (Katharine Jarmul), которые помогли мне познакомиться с Джесс.
Дополнительно я хочу поблагодарить Джаннетт Уинг (Jeannette Wing) и Клифа
Штейна (Cliff Stein), чья заинтересованность в этой работе помогла сделать книгу
увлекательнее. И конечно же, я хочу поблагодарить моих друзей и родственников
за их заинтересованность и поддержку, особенно в тех случаях, когда у них не было ни малейшего представления о том, о чем я вела речь.
Наконец, я хочу поблагодарить всех членов моей семьи (включая детей, которые
еще слишком молоды, чтобы прочитать это) за их поддержку даже в те дни, когда
мной овладевали печаль и грусть. Вы делаете мою работу стоящей и важной.
ГЛАВА 1
Введение в первичную обработку
(выпас) и качество данных
Кажется, что в настоящее время данные предоставляют ответ на все случаи жизни.
Например, мы используем данные в обзорах товаров и ресторанов, чтобы решить,
что купить и куда пойти покушать. Компании используют данные о наших действиях в Интернете — что мы читаем, смотрим и выбираем, для принятия решения,
какой контент создавать и какую рекламу показывать. Агентства по подбору кадров
используют данные для принятия решения, каким соискателям работы назначать
собеседование. Ну и, конечно же, государственные учреждения используют данные
для принятия широкого круга решений, от распределения средств на строительство
дорог до выбора школы для вашего ребенка. Данные, будь они в виде простой таблицы личных финансов или массива чисел для систем искусственного интеллекта,
проникают во все аспекты нашей жизни. Именно во всепроникающем воздействии
данных на наши ежедневные действия и возможности состоит причина, почему
первичная обработка данных является жизненно важным навыком для любого заинтересованного в понимании принципов работы управляемых данными систем и
влиянии на эти системы. Умение оценивать и улучшать качество данных необходимо для улучшения работы этих систем, которые зачастую содержат достаточно
серьезные изъяны.
Но поскольку термины первичная обработка данных или выпас данных1 (data
wrangling) и качество данных (data quality) могут истолковываться разными людьми по-разному, мы начнем эту главу с краткого обзора трех главных тем, рассматриваемых в этой книге: выпас данных, качество данных и язык программирования
Python. Цель этого обзора — пояснить мой подход к этим темам, для того чтобы вы
смогли решить, подходит ли вам эта книга. После этого мы уделим некоторое время на рассмотрение логистики доступа к программным инструментам и другим ресурсам, которые будут нам нужны для решения упражнений в этой книге. Хотя все
упоминаемые в этой книге ресурсы предлагаются на бесплатной основе, во многих
книгах и руководствах по программированию автоматически предполагается, что
их читателям для разработки программ будут доступны собственные компьютеры,
1
https://robotdreams.cc/blog/80-funkciya-poter-n-grammy-i-embedding
20
|
Глава 1
часто довольно дорогие. Но поскольку я по-настоящему верю в то, что любой желающий может научиться выполнять выпас данных, используя язык программирования Python, я хотела обеспечить возможность использования материала в этой
книге даже при отсутствии доступа к собственному полнофункциональному компьютеру. С этой целью все решения в этой и последующих главах были разработаны и протестированы на ноутбуке типа Chromebook. Их также можно исполнять,
используя бесплатные онлайновые инструменты на своем собственном компьютере
или компьютере для общего пользования, например, в школе или публичной библиотеке. Я надеюсь, что демонстрация высокого уровня доступности не только
знаний, но также и средств для выпаса данных, воодушевит вас на дальнейшее исследование этой захватывающей и полезной деятельности.
Что такое выпас данных?
Выпас данных (data wrangling) — это процесс преобразования "сырых" или "обнаруженных" данных в форму, которую можно использовать для получения знаний и
смысла. В основе всех усилий по выпасу данных лежит вопрос: есть ли что-то в
мире, что вы хотите исследовать. Конечно же, если вы решили заниматься по этой
книге с целью научиться программированию, тогда выпас данных будет хорошей
начальной точкой. Но я настоятельно советую не пытаться переходить напрямую к
программированию, не разобравшись с процессами обеспечения качества данных,
излагаемых в последующих главах. И хотя обладание навыками программирования
может быть полезным для выпаса данных, для полноты успеха в этой области требуется намного больше, чем просто знания, как получать доступ к данным и манипулировать ими. Как иллюстрируется в этой книге, большинство доступных данных имеет не особенно хорошее качество, поэтому выпас данных невозможно выполнять, не принимая решений, влияющих на суть полученных данных. Пытаться
выполнять выпас данных, не принимая во внимание качество данных, сродни попытке вести автомобиль без рулевого управления: вы можете доехать куда-либо, и
достаточно быстро, но, скорее всего, не туда, куда вам нужно. Если вы собираетесь
тратить время на разбор и анализ данных, надо убедиться, что это по крайней мере
будет стоить затраченных усилий.
Однако не менее важно и то, что нет лучшего способа освоить новый навык, чем
делать это во взаимосвязи с чем-либо, что вы очень хотите сделать правильно. Такая личная заинтересованность позволит вам пережить неизбежные моменты разочарования от неудач. Это что-либо не обязательно должно быть чем-то особенно
важным, а просто вопросом, например, о ваших любимых видеоиграх, музыкальных группах, типах чая, вашей школе, вашем квартале, вашей жизни в социальных
сетях, экономике, политике, вере или финансах. То есть это может быть вопрос о
чем угодно, важно, что это должно по-настоящему интересовать вас.
Определившись с интересующим вас вопросом, можно начинать процесс выпаса
данных. В зависимости от специфики проекта, конкретные шаги этого процесса
Введение в первичную обработку (выпас) и качество данных
|
21
может потребоваться корректировать (или повторять), в целом процесс выпаса
данных содержит следующие шаги:
1. Обнаружение или сбор данных.
2. Обзор данных.
3. Очистка, стандартизация, преобразование и/или дополнение данных.
4. Анализ данных.
5. Визуализация данных.
6. Предоставление данных.
Надо помнить, что время и усилия, необходимы для реализации каждого из этих
шагов, могут быть разными. Например, если вы хотите ускорить исполнение какойлибо задачи по выпасу данных, которую вы уже выполняете на работе, то у вас уже
может быть в наличии набор данных и вы, в основном, можете знать, в чем она состоит. С другой стороны, если вы пытаетесь выяснить объем финансов, которые
городские власти расходуют в вашем городе, сбор данных может стать наиболее
трудоемкой частью вашего проекта.
Кроме этого, несмотря на то, что шаги в предыдущем списке изложены в пронумерованном порядке, процесс выпаса данных представляет собой скорее цикл, нежели
линейный набор шагов. Зачастую, по мере получения дополнительных знаний о
значении и контексте используемых данных, вам нужно будет возвратиться к предыдущим шагам. Например, при анализе крупного набора данных могут встретиться неожиданные комбинации или значения данных, вынуждающие вас ставить под
вопрос предположения, которые вы могли сделать до этого. Это почти всегда будет
означать необходимость поиска дополнительной информации (или из исходного
источника данных, или из полностью новых источников), чтобы понять, что в действительности происходит, прежде чем вы сможете перейти к анализу или визуализации данных. Наконец, хотя в вышеприведенном списке это и не указано явно,
было бы более точным начинать каждый шаг в нем с "Исследование и", например,
"Исследование и обзор данных". Тогда как части "первичной обработки" нашей
работы будут в основном фокусироваться на имеющихся у нас наборах данных,
часть "обеспечение качества" почти полностью сводится к исследованию и контексту, и оба эти аспекта являются неотъемлемыми составляющими каждого этапа
процесса выпаса данных.
Если на данном этапе у вас возникают трудности с пониманием всего этого, не переживайте. Примеры в этой книге построены на основе настоящих наборов данных,
и по мере освоения процессов кодирования и оценки данных все это будет становиться для вас все более естественным. Если в процессе работы над своим собственным проектом выпаса данных вы начнете испытывать затруднения, просто постоянно напоминайте себе о вопросе, на который вы пытаетесь получить ответ. Это
не только напомнит вам, почему вы морочитесь с пониманием всех мельчайших
22
|
Глава 1
подробностей форматов данных и ключей доступа API-интерфейсов2, но также позволит вам интуитивно перейти к следующему шагу в процессе выпаса данных,
будь то визуализация данных или просто выполнение дополнительных исследований с целью улучшения контекста и качества данных.
Что такое качество данных?
Окружающий нас мир содержит большие объемы самых разнообразных данных,
получить доступ к которым и организовать их извлечение можно разными методами. Но не все данные являются равными. Понимание качества данных является
важной частью выпаса данных, поскольку любые знания, полученные на основе
данных, хороши лишь настолько, насколько хороши эти данные3. Поэтому, если вы
пытаетесь использовать данные, чтобы получить какие-либо значимые знания об
окружающем мире, сначала нужно удостовериться в том, что используемые вами
данные правильно отображают соответствующий аспект мира. Как мы увидим в
последующих главах (в частности, в главах 3 и 6), задача по улучшению качества
данных почти никогда не является такой четко определенной, как опрятно помеченные аккуратные строки и столбцы данных, с которыми вы будете работать.
Это объясняется тем, что, несмотря на такие термины, как "машинное обучение" и
"искусственный интеллект", вычислительные инструменты могут только следовать
заданным им указаниям, используя предоставленные им данные. А даже самые
сложные и абстрактные данные всегда по своей сути человеческие, поскольку они
являются результатом человеческих решений, что измерять и как. Кроме этого, даже наиболее продвинутые современные компьютерные технологии делают "предсказания" и принимают "решения" посредством метода, который, по сути, сводится
к крупномасштабному поиску структур данных, совпадающих с образцами в определенных выборках данных, которые предоставляются людьми, "тренирующими"
эти системы. У компьютеров не возникает никаких оригинальных идей и у них не
бывает творческих озарений; они в своей основе плохо справляются со многими
задачами, которые люди решают на интуитивном уровне (например, объяснение
сути дискуссии или сюжета рассказа). С другой стороны, компьютеры преуспевают
в области повторяющихся вычислений, которые они выполняют очень быстро, без
устали, не отвлекаясь на посторонние раздражители и не теряя интерес к выполняемой работе. Иными словами, компьютеры фантастически дополняют человеческие способности к анализу и мышлению, но они могут только усилить, но никак
не заменить их.
Это означает, что задача обеспечения качества данных, чтобы получить в результате их обработки значащие результаты, возлагается на людей, выполняющих сбор и
анализ данных. Тема качества данных подробно рассматривается в главе 3, а пока я
2
3
Эти аспекты рассматриваются более подробно в главах 4 и 5, соответственно.
В мире вычислений это часто описывается как "мусор на входе — мусор на выходе".
Введение в первичную обработку (выпас) и качество данных
|
23
хочу представить две отдельные (но одинаково важные) оси для оценки качества
данных: целостность данных (data integrity) и соответствие данных (data fit) конкретной задаче или вопросу.
Целостность данных
Для наших целей оценка целостности набора данных осуществляется, используя
значения и описания составляющих его данных. Например, если набор данных содержит значения измерений, выполненных в течение периода времени, то выясняется, выполнялись ли эти измерения через регулярные или нерегулярные временные интервалы. Представляют ли значения непосредственные отдельные измерения
или же только средние значения? Существует ли словарь данных, содержащий
подробную информацию о том, каким образом осуществлялся сбор или запись данных или как их следует интерпретировать (например, предоставляя релевантные
единицы измерения)? В общем, полные, неделимые и качественно аннотированные
(среди прочих аспектов) данные считаются обладающими более высокой целостностью, поскольку эти характеристики позволяют выполнять более широкий диапазон однозначных анализов. Но в большинстве случаев в конкретном наборе данных
будет недоставать определенного количества размерностей целостности данных,
означая, что задача по выяснению его ограничений и по его улучшению возлагается
на вас. Решение этой задачи часто означает улучшение данного набора данных посредством нахождения других наборов данных, которые могут дополнить, расширить или поместить его в контекст. Но почти всегда это состоит в выходе за рамки
"данных" любого типа и обращении за помощью к экспертам: людям, которые разработали эти данные, собрали их, работали с ними ранее или обладают большими
знаниями в предметной области, задачу в которой эти данные должны помочь решить.
Соответствие данных
Но даже если набор данных имеет превосходную целостность, его можно считать
высококачественным только тогда, когда он подходит и для решения конкретной
задачи. Предположим, что нас интересует, в какой из городских станций проката
велосипедов сети Citi Bike было взято в прокат и возвращено наибольшее количество велосипедов за данные сутки. Хотя API-интерфейс реального времени вебсайта сети Citi Bike содержит данные с высоким уровнем целостности, эти данные
плохо подходят для предоставления ответа на конкретный вопрос, какая из станций
этой сети имела наибольший оборот за определенный день. В данном случае было
бы лучше попытаться получить ответ на этот вопрос, используя историю поездок
этой сети проката.
Конечно же, задача соответствия данных может быть решена так просто только в
редких случаях; чаще нам приходится выполнять большой объем работы по повышению уровня целостности данных, прежде чем мы сможем быть уверены в том,
24
|
Глава 1
что наш набор данных в действительности соответствует конкретному вопросу или
задаче. Но избежать этих расходов времени невозможно, поскольку "срезание углов" при решении задачи целостности или соответствия данных неизбежно отрицательно скажется на качестве и соответствии работы по выпасу данных в целом.
В действительности, вред, создаваемый современными вычислительными системами, во многих случаях связан с проблемами с соответствием данных. Например,
использование данных, описывающих одно явление (например, доход) для получения ответа о потенциально связанных, но фундаментально других явлениях (например, достижениях в образовании), может вести к искаженным заключениям о
происходящем в мире, иногда с разрушительными последствиями. Конечно же, в
некоторых случаях использования таких посреднических измерений избежать невозможно. Например, для предоставления неотложной медицинской помощи будет
оправдан первичный диагноз на основе наблюдаемых симптомов пациента, пока не
будут получены результаты более подробных тестов. Такие промежуточные этапы
иногда приемлемы на индивидуальном уровне, но разрыв между косвенными измерениями и настоящим явлением увеличивается прямо пропорционально масштабу
данных и системы, в которой эти данные используются. В таких случаях в конечном итоге мы получим чрезвычайно искаженное представление явления, которое
мы хотим прояснить при помощи нашей первичной обработки и анализа данных.
К счастью, существует несколько способов защиты от ошибок такого типа, которые
мы подробно рассмотрим в главе 3.
Распаковка системы COMPAS
Один из громких примеров вреда, который может быть причинен использованием неверных данных в крупномасштабной вычислительной системе, был
продемонстрирован несколько лет тому назад группой журналистов из некоммерческой расследовательской новостной организации ProPublica4. В серии
Machine Bias (Машинная предвзятость) своих публикаций эти журналисты исследовали разные предсказания возможности повторного совершения преступления черными и белыми заключенными, рассматриваемые для условнодосрочного освобождения, выдаваемые алгоритмическим инструментом COMPAS
(Correctional Offender Management Profiling for Alternative Sanctions — Исправительно-управленческое профилирование правонарушителей для определения
целесообразности применения альтернативного наказания). Этот инструмент
генерировал более высокие значения вероятности повторного совершения преступления для черных правонарушителей, чем для белых с подобным уголовным прошлым. В значительной степени это обуславливалось тем, что данные,
используемые для предсказания (или "моделирования") риска повторного правонарушения, рассматривали количество арестов как косвенный коэффициент
склонности к совершению правонарушений.
4
Разглашение: многие из штатных сотрудников организации ProPublica, включая ведущего репортера
этой серии, в прошлом были коллегами автора.
Введение в первичную обработку (выпас) и качество данных
|
25
Но модель арестов с самого начала проявляла предвзятость против черных, т. е.
черные арестовывались за такие "преступления" (например, за добирание на
работу пешком), которые не предъявлялись белым. Поэтому создаваемые этим
инструментом оценки степени риска также были предвзятыми5.
И можно с легкостью найти другие примеры возможности нанесения большого
вреда вследствие использования некачественного соответствия данных. Поэтому оценка данных как на их целостность, так и на соответствие задаче, является
важной частью процесса выпаса данных: если используются неподходящие
данные, то полученные на их основе результаты могут не просто быть неправильными, но даже нанести вред.
Почему мы выбрали Python?
Если вы читаете эту книгу, то по всей вероятности вы уже знаете о программировании на языке Python и даже можете быть довольно уверенными в том, что это правильный инструмент для начала или продолжения работы в области выпаса данных. Но даже если это так, я полагаю, что будет иметь смысл выполнить краткий
обзор, что делает язык Python и чем он подходит для того типа работы по первичной обработке и обеспечению качества данных, которой мы будем заниматься в
этой книге. Если вы ничего не знаете о языке Python, считайте это введением в рассмотрение факторов, делающих его одним из наиболее мощных и популярных современных языков программирования.
Универсальность
Наверное, одной из самых сильных сторон языка Python является его универсальность: его можно с легкостью использовать для доступа к API-интерфейсам, извлечения данных из веб-страниц, выполнения статистических анализов и генерирования значащих визуализаций. Другие языки программирования могут выполнять
некоторые из этих задач, но очень немногие из них могут справиться с ними всеми
так хорошо, как это делает Python.
Доступность
Согласно одному из создателей языка Python, Гвидо ван Россуму (Guido Van
Rossum), целью создания этого языка было получить "язык программирования, который можно понимать как обычный английский язык". В этом языке используют5
Серия публикаций "Machine Bias" вызвала значительное обсуждение в научном сообществе, некоторые члены которого не были согласны с определением предвзятости, используемым организацией
ProPublica. Но важен тот факт, что эти разногласия породили полностью новую область научных
исследований: беспристрастность и прозрачность в машинном обучении и интеллекте.
26
|
Глава 1
ся английские ключевые слова в тех случаях, когда многие языки сценариев (например, язык R и JavaScript) используют знаки препинания. В результате этого для
англоязычных пользователей изучение языка Python может быть легче и более интуитивно, чем изучение других языков написания сценариев.
Удобочитаемость
Одна из центральных доктрин языка Python гласит, что "удобочитаемость важна".
В большинстве языков программирования визуальная организация кода не влияет
на его функционирование, и ход исполнения программы определяется специальными символами. В противоположность им язык Python полагается на отступы и
табуляцию для задания хода исполнения, в результате чего неправильная расстановка этих символов будет вызывать сообщения об ошибке. Такой подход требует
некоторого привыкания, но он обеспечивает определенный уровень удобочитаемости исходного кода программ на языке Python, позволяющий более легкое понимание кода, написанного другими (или, что более уместно, — собственного кода, по
истечении некоторого времени после его написания). Другим аспектом удобочитаемости кода является комментирование и прочее документирование своей работы, которое рассматривается более подробно далее в этой главе в разд. "Документирование, сохранение и управление версиями своего кода".
Сообщество
У Python очень большое и активное сообщество пользователей, многие из которых
помогают создавать и поддерживать "библиотеки" кода, расширяющие возможности, которые вы можете быстро выполнить с помощью вашего собственного кода
на Python. Например, популярные высокоразвитые библиотеки NumPy и Pandas позволяют выполнять очистку и анализ данных, а библиотеки Matplotlib и Seaborn
позволяют создавать визуализации. А еще более мощные библиотеки Scikit-Learn и
NLNK позволяют решать задачи даже в таких сложных областях, как машинное
обучение и обработка текстов на естественных языках. Многие из этих библиотек
используются в рассматриваемых в этой книге основных принципах выпаса данных
с использованием языка Python. Но с их помощью можно получить намного более
продвинутые результаты, используя всего лишь несколько строк кода. Люди, которые разрабатывают эти библиотеки, часто ведут блоги, создают видеоруководства и
предоставляют примеры кода, которые можно использовать для расширения возможностей ваших программ на языке Python.
Большое сообщество пользователей языка Python и их энтузиазм помогают легко и
быстро найти решения по наиболее распространенным (и даже не очень распространенным) проблемам и ошибкам, с которыми вам придется столкнуться, используя публикуемые в Интернете решения. В результате поиск ошибок в коде Python
может быть более легким, чем для более специализированных языков с сообществами пользователей меньшего размера.
Введение в первичную обработку (выпас) и качество данных
|
27
Альтернативы языку Python
Хотя язык Python имеет много достоинств, выпас данных можно выполнять и с помощью других средств. Далее приводится краткий обзор некоторых из этих инструментов, включая объяснение причины, по которой я отдала предпочтение именно
языку Python.
Язык R
Язык программирования R является, наверное, наиболее близким конкурентом
языка Python для работы с данными. Этот язык используется многими организациями и командами разработчиков для выпаса данных, продвинутого статистического моделирования и визуализации. Но по таким характеристикам, как доступность и удобочитаемость, этот язык несколько уступает языку Python.
Язык SQL
Язык SQL (Simple Query Language — простой язык запросов) предназначен для
работы с данными баз данных. Этот язык обладает мощными полезными возможностями, но требует, чтобы обрабатываемые данные были в специальном
формате, что сразу же ограничивает его полезность для выпаса данных.
Язык Scala
Язык программирования Scala хорошо подходит для работы с наборами данных
большого размера, но его кривая обучения намного круче, чем у языка Python.
Кроме этого, у него намного меньше сообщество пользователей. То же самое
относится и к языку Julia.
Языки Java и C/C++
Хотя эти языки программирования высокоуниверсальны и имеют большие сообщества, они не обладают свойствами естественного языка и удобочитаемости,
присущими языку Python. Кроме этого, они больше направлены на разработку
программного обеспечения, нежели на первичную обработку и анализ данных.
Язык JavaScript
Язык JavaScript неоценим в веб-среде, и многие популярные средства визуализации (например, D3) созданы, используя его разновидности. В то же самое время, этот язык не обладает таким же обширным набором возможностей анализа
данных, как язык Python, и, в целом, программы на нем исполняются более медленно.
Написание и исполнение программ
на языке Python
Для выполнения упражнений в этой главе вам нужно знать, как работать с инструментами Python для написания и исполнения программ на этом языке. Также рекомендуется организовать систему для резервного копирования и документирования
28
|
Глава 1
кода, чтобы не потерять свою работу вследствие ошибочного нажатия клавиши6, и
чтобы можно было с легкостью напомнить себе, что и как делает ваш код, даже после значительного перерыва в работе с ним. Поскольку для решения этих задач существует много разных наборов инструментов, я рекомендую начать с ознакомления с материалом следующих разделов, а затем выбрать подход или комбинацию
подходов, наилучшим образом подходящих вашим предпочтениям и ресурсам.
Сначала вам нужно будет принять ключевое решение, хотите ли вы работать только в онлайновом режиме (т. е. используя инструменты и службы, доступные через
Интернет) или же вы хотите и можете работать без доступа к Интернету, установив
эти инструменты на локальной машине.
В зависимости от обстоятельств, мы все пишем по-разному: стиль и структура сообщения электронной почты будут, скорее всего, иными, чем SMS-сообщения, а
формат и тон сопроводительного письма заявления о приеме на работу будет полностью другим, нежели в этих сообщениях. Лично я, в зависимости от решаемой
задачи, также использую разные письменные инструменты. Например, для текстов,
над которыми я работаю совместно с моими сослуживцами и коллегами, я использую онлайновые инструменты, но для написания книг и других сочинений я предпочитаю пользоваться предельно простым текстовым редактором, установленным
на моем устройстве. А для контрактов и других важных документов, которые мы
хотим защитить от изменения посторонними лицами, применяются более специфичные форматы, например PDF.
Так же как и естественные человеческие языки, язык программирования Python можно использовать в документах разных типов, каждый из которых поддерживает слегка иной стиль написания, тестирования и исполнения кода. Основными средствами
работы с кодом на языке Python являются блокноты и автономные сценарии. Тогда
как для первичной обработки, анализа и визуализации данных можно использовать
документы обоих этих типов, они слегка отличаются своими сильными сторонами и
требованиями. Поскольку преобразование между этими форматами требует некоторой модификации исходного документа, упражнения в этой книге предоставляются в
обоих этих форматах. Я применила этот подход не только для того, чтобы предоставить вам возможность выбора наиболее легкого и полезного для вас формата, но
также для того, чтобы вы могли сравнить их и увидеть своими глазами воздействие,
оказываемое на код процессом трансляции. Далее приводится краткий обзор типов
документов кода Python, чтобы помочь вам сделать начальный выбор.
Блокнот
Блокнот (notebook) Python представляет собой интерактивный документ, позволяющий исполнять фрагменты кода, используя в качестве интерфейса окно веббраузера. В этой книге мы будем использовать инструмент Jupyter для создания,
редактирования и исполнения наших блокнотов Python7. Ключевым достоинст-
6
Не забывайте, что в языке Python даже неправильно поставленный пробел может вызвать ошибку.
Это программное обеспечение можно также использовать для создания блокнотов на языке R и
других языках написания сценариев.
7
Введение в первичную обработку (выпас) и качество данных
|
29
вом использования блокнотов для программирования на языке Python является
то, что они предоставляют простой способ для написания, документирования и
исполнения исходного кода на языке Python — все в одном месте. Этот метод
может быть подходящим для тех, кто предпочитает программировать в стиле
"указал и щелкнул" или для кого важно работать исключительно в онлайновом
режиме. К тому же одни и те же блокноты кода Python можно использовать с
минимальными изменениями, как в онлайновой среде разработки, так и на локальном устройстве. Это означает, что этот подход может быть удобным для
вас, если: 1) у вас нет устройства, на котором можно было бы установить соответствующее программное обеспечение, или 2) у вас есть такое устройство, но
вы также хотите иметь возможность работать со своим кодом, когда этого устройства нет при себе.
Файлы автономных сценариев
Файл автономного сценария Python представляет собой любой текстовый файл,
содержащий исходный код на языке Python. Такие файлы можно создавать при
помощи любого простого текстового редактора, но я настоятельно рекомендую
использовать редактор, предназначенный специально для работы с кодом, например редактор Atom. Я пошагово рассмотрю установку редактора кода далее в
этой главе в разд. "Установка языка Python, среды Jupyter Notebook и редактора кода". Тогда как у вас есть большая свобода выбора программы для написания и редактирования кода, исполнять файлы автономных сценариев Python,
созданные посредством выбранного вами редактора, можно только на устройстве (например, компьютере или смартфоне), на котором установлен язык программирования Python. Распознать файлы автономных сценариев Python можно
по их расширению .py. Хотя подход с использованием файлов автономных сценариев поначалу может казаться более ограничивающим, в действительности он
может иметь некоторые преимущества. В частности, для исполнения этих файлов не требуется подключение к Интернету, а также не нужно загружать данные
в облачное хранилище. Тогда как оба эти аспекта присущи и локальному исполнению блокнотов кода Python, для исполнения файлов автономных сценариев
Python также не нужно ожидать запуска какой-либо программы. На компьютере
с установленным языком Python файлы автономных сценариев исполняются
сразу же после их запуска из командной строки (этот вопрос рассматривается
чуть далее). Это особенно полезно в тех случаях, когда нужно регулярно исполнять какой-либо сценарий на языке Python. Возможность исполнения фрагментов кода в блокнотах Python независимо друг от друга может делать этот подход
чуть более удобным. Но то обстоятельство, что код в файлах автономных сценариев Python всегда исполняется "с нуля", может помочь вам избежать ошибок
или непредсказуемых результатов, которые могут возникнуть в результате исполнения фрагментов кода в блокноте в неправильном порядке.
Конечно же, вам совсем не обязательно выбирать только один из этих подходов.
Благодаря их интерактивному и удобочитаемому формату большинство пользователей находят блокноты Python особенно полезными для исследования и объяснения данных. А файлы автономных сценариев Python лучше подходят для получения
30
|
Глава 1
доступа к данным и их очистки и преобразования, поскольку, например, один и тот
же код в них можно более быстро и легче исполнять с разными наборами данных.
Наверное, более важным критерием будет вопрос, хотите ли вы работать в онлайновом или локальном режиме. Если у вас нет устройства, на которое можно установить язык Python, вам придется работать только с облачными блокнотами кода; в
противном случае можно использовать на своем устройстве или блокноты кода
Python, или файлы автономных сценариев, или то и другое. Как упоминалось ранее,
блокноты кода Python, которые можно использовать или в онлайновом режиме, или
локально, а также файлы автономных сценариев Python предоставляются для всех
упражнений в этой книге. Это делается для того, чтобы позволить вам наибольшую
гибкость в их выполнении, а также для того, чтобы предоставить вам возможность
сравнить решение одной и той же задачи, используя каждый из этих подходов.
Работа с кодом Python на локальном устройстве
Чтобы понимать и исполнять код на языке Python, вам нужно установить этот язык
на свое устройство. В зависимости от используемого вами устройства, это можно
сделать, загрузив и запустив на исполнение установочный файл или же исполнив
соответствующую команду в текстовом интерфейсе, называющимся командной
строкой (при использовании локально установленного языка Python вам рано или
поздно придется работать с этим интерфейсом). Нашей целью является установка
по крайней мере версии 3.9 языка Python8. После установки на своем устройстве
языка Python необходимо для работы с блокнотами кода установить программу
среды разработки Jupyter Notebook, а также редактор кода (далее в этой главе приводятся инструкции по установке редактора кода Atom). Если вы планируете работать только в онлайновом режиме, то для ознакомления с информацией, как приступить к работе в этом режиме, можете пропустить следующий материал и перейти сразу же к разд. "Работа с кодом Python в режиме онлайн".
Введение в работу с командной строкой
Если вы планируете работать с языком Python локально, то вам нужно научиться
использовать интерфейс командной строки (который также называется терминалом или консолью), который представляет собой способ подачи команд компьютеру
в виде текстовых строк. В принципе, все, что можно делать посредством командной строки, можно делать и при помощи мыши, однако командная строка особенно
удобна для установки кода и программного обеспечения (особенно библиотек
Python, которые мы будем использовать на всем протяжении этой книги), а также
8
Номера версий языка Python последовательно возрастают по мере его изменения и обновления с
течением времени. Первая цифра (в данном случае 3) обозначает основной номер версии, а вторая (в
данном случае 9) — дополнительный. Точка в обозначении версии не является десятичным разделителем, и дополнительный номер версии может быть больше, чем 9. Поэтому вполне возможно, что
в будущем мы увидим версию, например, 3.12.
Введение в первичную обработку (выпас) и качество данных
|
31
для создания резервных копий и исполнения кода. Хотя к работе с командной строкой нужно немного привыкнуть, для выполнения многих задач программирования
этот метод более быстрый и прямолинейный чем использование мыши. Я буду
предоставлять инструкции для использования, как командной строки, так и мыши,
в случаях, допускающих использование обоих методов. Таким образом, у вас будет
возможность выбрать тот метод, который для вас будет более удобным для решения конкретной задачи.
Для начала работы откроем интерфейс командной строки (или, как его еще называют, терминал) и с его помощью создадим папку для хранения всей нашей работы
по выпасу данных. Для Chromebook, macOS или Linux, выполните поиск по
"terminal" и в результатах поиска выберите приложение Terminal. Для Windows
выполните поиск по "powershell" и выберите программу Windwos PowerShell.
Для использования Linux на устройстве Chromebook нужно открыть меню
настроек операционной системы Chrome, щелкнув по значку шестеренки
в меню Start или выполнив поиск по "settings" в строке запуска программ.
В открывшемся окне настроек щелкните по небольшому значку пингвина,
обозначенному "Linux (Beta)", и следуйте соответствующим инструкциям.
Чтобы продолжить работу, может потребоваться перезагрузить устройство.
Прежде чем создать нашу папку, рассмотрим вкратце несколько основных команд
для работы с файловой системой.
ls
Сокращение слова list (список). Выводит список файлов и папок в текущей папке. Это текстовая версия содержимого, отображаемого в окне Finder (или проводника Windows).
cd название_папки
Сокращение от change directory (изменить директорию/папку). Выполняет переход из текущей папки в папку, указанную в параметре название_папки, при условии, что целевая папка находится в списке, выводимом командой ls. Это эквивалентно двойному щелчку мышью по значку папки в окне Finder (или проводника Windows).
cd ../
Та же самая команда для перехода в другую папку, но параметр ../ означает
папку высшего уровня, содержащую текущую папку.
cd ~/
Еще одна команда для перехода в другую папку, но параметр ~/ означает корневую (или домашнюю) папку.
mkdir название_папки
Сокращение от make directory (создать директорию/папку). Создает в текущей
папке новую папку с названием название_папки. Это эквивалентно выбору последовательности команд Создать | Папку в контекстном меню окна Finder (или
проводника Windows) с последующим присвоением названия созданной папке.
32
|
Глава 1
При работе с командной строкой необязательно указывать полное название файла или папки. Можно просто ввести несколько начальных символов названия (не забывая при этом, что в Linux названия чувствительны к
регистру), а затем нажать клавишу <Tab>. В результате система насколько возможно автоматически заполнит остальные символы.
Предположим, что текущая папка содержит два файла, один из которых
называется xls_parsing.py, а другой — xlsx_parsing.py. Чтобы запустить
один из этих файлов, в командной строке можно начать вводить команду
python xl, а затем нажать клавишу <Tab>. В результате система автоматически завершит название файла, но всего лишь до части python xls. Это
объясняется тем, что система не знает, какой из двух возможных файлов
мы имеем в виду, и нам нужно указать его самим, добавив символ "_" для
первого файла или "x" для второго, а затем снова нажать клавишу <Tab>.
При открытии окна терминала текущей папкой становится корневая папка данного
пользователя. На Linux, maxOs и Windows такой папкой в большинстве случаев
будет папка, называющаяся по имени текущего пользователя, а не область рабочего
стола, отображающаяся при входе в систему. Поначалу это может немного сбивать
с толку, поскольку список файлов и папок, выводимый при первом исполнении команды ls в окне терминала, будет, скорее всего, выглядеть для вас незнакомым. Но
это легко исправить, просто выполнив переход на рабочий стол, введя в терминал
команду:
cd ~/Desktop
и нажав клавишу <Enter>.
На Chromebook программы Python (а также другие требующиеся нам программы)
можно исполнять только из папки Linux, поэтому в действительности у вас не получится перейти на рабочий стол и нужно будет запустить терминал (т. е. интерфейс командной строки).
Теперь выполните в терминале следующую команду:
mkdir data_wrangling
В результате должна создаться папка data_wranging. Если же что-то пошло не так, и
папка не была создана, еще раз проверьте правильность ввода команды создания
папки. Также убедитесь, что вы находитесь на рабочем столе, проверив, что слева
от символа командной строки ($ — в Chromebook, % — в macOS и > — в Windows)
стоит слово Desktop. Если нет, то снова выполните команду cd ~/Desktop, а затем команду создания папки mkdir data_wrangling.
Пробелы — зло!
Почти все операционные системы позволяют использовать пробелы в названии файлов и папок, но я настоятельно рекомендую не использовать в них ни
пробелов, ни прочих знаков препинания, кроме символа подчеркивания (_).
Введение в первичную обработку (выпас) и качество данных
|
33
Как мы увидим далее в главе 2, пробелы и знаки пунктуации используются в
командной строке и языке Python для обозначения специальных функциональностей. Это означает, что для использования их в названиях файлов или
папок они должны предшествоваться управляющими символами (escape
symbols), обычно символом обратной косой черты (\). Поэтому мы не сможем
создать папку data wrangling из командной строки. В результате попытки сделать это следующей командой:
mkdir data wrangling
будет создано две папки, одна с названием data, а другая с названием wrangling.
Папку с пробелом в названии можно создать в проводнике файловой системы
с графическим интерфейсом, но для доступа к этой папке из командной строки
нужно будет использовать управляющий символ перед пробелом:
cd data\ wrangling/
Так что, хотя использование пробелов в названиях элементов файловой системы
в принципе возможно, от этой возможности больше проблем, чем пользы. Во избежание этих проблем будет легче просто выработать привычку не использовать
пробелов и символов препинания в названиях объектов файловой системы, а
также в названиях переменных языка Python.
Теперь, когда у нас есть немного опыта работы с командной строкой, можно попробовать применить этот опыт для установки и тестирования языка Python на локальной машине.
Установка языка Python, среды Jupyter Notebook
и редактора кода
Ради простоты для установки требуемого нам программного обеспечения мы воспользуемся диспетчером распределения программного обеспечения Miniconda, который автоматически установит язык Python и среду разработки Jupyter Notebook.
Даже если вы не планируете использовать блокноты кода Python в своей работе,
они достаточно популярны, поэтому будет полезным иметь возможность просматривать и исполнять блокноты других пользователей. Кроме этого, соответствующее
приложение не займет слишком много места на вашем жестком диске. Кроме установки языка Python и среды разработки Jupyter Notebook, диспетчер Miniconda также создает новую функцию командной строки conda для быстрого и легкого обновления этих программ9. Подробная информация о том, как выполнять эти обновления, приводится в приложении А.
Даже если вы планируете выполнять большую часть работы программирования на
языке Python, используя блокноты, я все равно рекомендую вам установить редактор кода. Даже если вы никогда не напишете в нем ни одной строки кода, он будет
9
Диспетчер Miniconda представляет собой урезанную версию диспетчера Anaconda. Мы используем
его, чтобы сэкономить дисковое пространство, не устанавливая язык программирования R и
некоторые другие инструменты, которые нам не нужны, но устанавливаются диспетчером Anaconda.
34
|
Глава 1
незаменим для просмотра, редактирования и даже создания файлов данных более
эффективно, чем это возможно, используя встроенные текстовые редакторы большинства устройств. Что более важно, редактор кода обладает функциональностью
выделения синтаксических элементов кода, что, по сути, является встроенным
средством проверки синтаксиса кода и данных. Хотя это может казаться не такой и
важной возможностью, в действительности она значительно ускорит процессы кодирования и диагностирования, поскольку в случае ошибок в коде позволит точно
определить, где они находятся. Благодаря этой комбинации возможностей хороший
редактор кода является одним из самых важных инструментов для программирования на языке Python в частности и для выпаса данных в целом.
В этой книге используется многоплатформный бесплатный редактор кода с открытым исходным кодом Atom (https://atom.io). Редактируя параметры этого редактора, его можно с большой точностью настроить под свои требования. Упоминаемые
в этой книге цвета определенных символов или фрагментов кода относятся к используемой по умолчанию теме One Dark редактора Atom, но вы можете использовать любые предпочитаемые вами настройки.
Для реализации последующих процессов установки программного обеспечения вам потребуется стабильное подключение к Интернету с высокой пропускной способностью и около 30-60 минут времени. Я настоятельно рекомендую подключить свое устройство к сетевому источнику
питания, а не использовать питание от встроенной батареи.
Установка на устройство Chromebook
Для установки набора инструментов для выпаса данных на устройство Chromebook
первым делом нужно определить разрядность его операционной системы Chrome
OS, т. е. является ли она 32- или 64-разрядной.
Чтобы получить эту информацию, нужно открыть меню настроек операционной
системы Chrome (щелкнув значок шестеренки в меню Start или выполнив поиск по
строке "settings" в строке запуска программ), а затем щелкнуть по ссылке About
Chrome OS в левой нижней части окна. В верхней части окна отобразится сообщение с названием операционной системы и указанием разрядности, как показано на
рис. 1.1.
Запишите или запомните эту информацию, прежде чем продолжать установку.
Рис. 1.1. Сообщение с информацией о версии
и разрядности операционной системы Chrome OS
Введение в первичную обработку (выпас) и качество данных
|
35
Установка языка Python и среды разработки Jupyter Notebook. Первым делом
загрузите версию установщика для Linux диспетчера Miniconda, соответствующего
разрядности вашей операционной системы Chrome OS (https://docs.conda.io/en/
latest/miniconda.html#latest-miniconda-installer-links). Затем переместите загруженный файл установщика (который будет иметь расширение .sh) из папки
Downloads в папку Linux files.
Далее запустите программу терминала и исполните в нем команду ls, чтобы убедиться в наличии файла установщика диспетчера Miniconda в папке. Если файл на
месте, запустите установщик, исполнив следующую команду (если вы помните, то
можно ввести только первые несколько символов названия файла, а затем нажать
клавишу <Tab>, и остальная часть будет заполнена автоматически):
bash _Miniconda_installation_filename_.sh
Следуйте инструкциям, выводимым в окне терминала (в частности, согласитесь с
условиями лицензии и папкой для установки диспетчера). По завершению установки выполните следующую команду:
conda init
После этого закройте и снова откройте окно терминала и установите среду разработки Jupyter Notebook, исполнив следующую команду:
conda install jupyter
Ответьте "yes" на все последующие запросы и по завершению установки закройте
окно терминала. Установка языка Python и среды разработки Jupyter Notebook завершена.
Установка редактора кода Atom. Первым делом загрузите файл .deb установщика
редактора Atom с его веб-сайта (https://atom.io) и сохраните (или переместите) его
в папку Linux files вашего устройства Chromebook.
Чтобы установить программу, откройте терминал, перейдите в папку, содержащую
файл установщика, и исполните в терминале следующую команду10:
sudo dpkg -i atom-amd64.deb
Установка будет завершена, когда в окне терминала перестанет прокручиваться
текст и возвратится приглашение на ввод команды терминала (заканчивающееся
символом $).
Можно также выполнить щелчок правой кнопкой мыши по файлу установщика и в
открывшемся контекстном меню выбрать опцию Install with Linux, щелкнуть
Install в следующем окне, а затем нажать кнопку OK. В процессе установки в нижней правой части экрана будет отображаться индикатор хода установки, а по ее завершению выведется соответствующее сообщение.
Независимо от выбранного метода установки, по ее завершению в разделе Linux
apps меню запуска команд должен появиться зеленый значок редактора кода Atom.
10
Для 32-разрядных систем Chrome OS это название файла может быть немного другим.
36
|
Глава 1
Установка на macOS
Установить диспетчер Miniconda на macOS можно двумя способами: из терминала,
используя файл установщика с расширением .sh, или в графическом интерфейсе,
загрузив пакет установщика с расширением .pkg, а затем выполнив по нему двойной щелчок мышью.
Установка языка Python и среды разработки Jupyter Notebook. Первым делом
откройте в браузере страницу со ссылками на файлы установщика диспетчера
Miniconda (https://docs.conda.io/en/latest/miniconda.html#latest-miniconda-installerlinks). Для установки из терминала загрузите файл установщика с расширением .sh,
а для установки из графического интерфейса загрузите файл установщика с расширением .pkg. (В процессе загрузки файла установщика может отобразиться сообщение с предупреждением, что This type of file can harm your computer (Этот файл
может причинить вред вашему компьютеру). Игнорируйте его, нажав кнопку
Keep.)
Для обоих методов установки переместите загруженный файл установщика из папки Downloads на свой рабочий стол.
Для установки диспетчера Miniconda при помощи терминала откройте окно терминала и перейдите на рабочий стол, выполнив следующую команду:
cd ~/Desktop
Затем исполните команду ls, чтобы убедиться в том, что файл .sh установщика диспетчера Miniconda действительно находится в папке рабочего стола. Если файл на
месте, то запустите установщик, исполнив следующую команду (если вы помните,
то можно ввести только первые несколько символов названия файла, а затем нажать клавишу <Tab>, и остальная часть будет заполнена автоматически):
bash _Miniconda_installation_filename_.sh
Далее следуйте выводимым в окне терминала инструкциям:
нажимая клавишу пробела, пролистайте лицензионное соглашение до конца
(пока не увидите слово (END), а затем нажмите клавишу <Enter>;
cогласитесь с условиями лицензионного соглашения, введя yes и нажав клавишу
<Enter>;
cогласитесь с папкой установки по умолчанию, нажав клавишу <Enter>, а затем
введите yes и нажмите клавишу <Enter>, чтобы принять приглашение conda init.
Наконец, закройте окно терминала.
Для установки с использованием графического пользовательского интерфейса
просто выполните двойной щелчок по файлу .pkg установщика и следуйте выводимым инструкциям.
Завершив установку диспетчера Miniconda, снова откройте окно терминала и выполните в нем следующую команду:
conda init
Введение в первичную обработку (выпас) и качество данных
|
37
Теперь можно установить среду разработки Jupyter Notebook. Для этого закройте и
снова откройте окно терминала и исполните следующую команду:
conda install jupyter
На все выводимые запросы отвечайте yes.
Установка редактора кода Atom. Первым делом загрузите файл установщика редактора Atom с его веб-сайта (https://atom.io), нажав большую желтую кнопку
Download.
В папке Downloads распакуйте файл архива atom-mac.zip, а затем перетащите приложение Atom (обозначенное зеленым значком в виде стилистического изображения атома) в папку Applications (для этого может потребоваться ввести свой пароль).
Установка на Windows 10+
Для установки набора инструментов для выпаса данных на Windows 10+ первым
делом нужно определить разрядность используемой операционной системы, т. е.
является ли она 32- или 64-разрядной.
Для этого откройте меню Пуск и нажмите в нем на значок в виде шестеренки,
чтобы открыть меню настроек. В левой части окна настроек выберите последовательность команд Система | О системе. Информация о разрядности операционной системы указывается в элементе Тип системы в разделе Параметры устройства. Официальные инструкции компании Microsoft для решения этой задачи
приводятся в соответствующем разделе ответов на часто задаваемые вопросы
(https://support.microsoft.com/en-us/windows/32-bit-and-64-bit-windowsfrequently-asked-questions-c6ca9541-8dce-4d48-0415-94a3faa2e13d).
Запишите или запоните эту информацию, прежде чем продолжать установку.
Установка языка Python и среды разработки Jupyter Notebook. Первым делом
загрузите со страницы загрузок Miniconda (https://docs.conda.io/en/latest/
miniconda.html#latest-miniconda-installer-links) файл установщика Python 3.9, соответствующий разрядности операционной системы используемого вам компьютера (32- или 64-разрядная). Запустите загруженный файл на исполнение и следуйте
выводимым инструкциям, принимая все предустановленные опции. (Можно пропустить просмотр рекомендуемых руководств и подписку на Anaconda Nucleus в
конце процесса установки.)
По завершении установки вверху списка раздела Недавно добавленные (Recently
added) меню Пуск должны появиться два новых элемента: Anaconda Prompt
(miniconda3) и Anaconda Powershell Prompt (miniconda3), как показано на рис. 1.2.
Для наших целей можно использовать любое из этих двух приложений, но лично я
рекомендую использовать интерфейс терминала Powershell для всей работы в этой
книге.
38
|
Глава 1
Рис. 1.2. Опции интерфейса Anaconda в меню Пуск
Завершив установку диспетчера Miniconda, откройте окно терминала Powershell и
выполните в нем следующую команду:
conda init
Теперь можно установить среду разработки Jupyter Notebook. Для этого закройте и
снова откройте окно терминала и исполните следующую команду:
conda install jupyter
В последующем ходе установки дайте утвердительный ответ на все запросы, вводя
с клавиатуры yes (или y).
Установка редактора кода Atom. Первым делом загрузите файл установщика редактора Atom с его веб-сайта (https://atom.io), нажав большую желтую кнопку
Download.
Запустите загруженный установщик на исполнение, выполнив двойной щелчок
мышью по его файлу, и следуйте выводимым инструкциям. По завершении установки должен автоматически запуститься редактор Atom. На запрос о том, зарегистрировать ли редактор Atom в качестве обработчика по умолчанию идентификаторов URI atom://, можно дать утвердительный ответ.
Проверка успеха установки
Чтобы проверить, что установленные программные средства Python и Jupyter
Notebook работают должным образом, откройте окно терминала и перейдите в нем
в папку data_wrangling, которую мы создали в разд. "Введение в работу с командной строкой"11:
cd ~/Desktop/data_wrangling
Затем исполните следующую команду:
python --version
В результате в окне терминала должна вывестись строка:
Python 3.9.4
Это означает, что установка языка Python была успешной. Далее проверим установку
среды разработки Jupyter Notebook, исполнив в терминале следующую команду:
jupyter notebook
11
Если не указано обратное, то в конце всех команд терминала нужно нажимать клавишу <Enter> (или
<Return>).
Введение в первичную обработку (выпас) и качество данных
|
39
Если в результате откроется окно12, как показано на рис. 1.3, то установка среды
Jupyter Notebook была выполнена успешно.
Рис. 1.3. Средство Jupyter Notebook,
исполняющееся с пустой папкой
Работа с кодом Python
в режиме онлайн
Если вы не хотите морочиться с установкой языка Python и редактора кода на локальной машине (и если вы планируете использовать Python только при наличии
надежного подключения к Интернету с хорошей пропускной способностью), то работать с блокнотами Jupyter можно в онлайновом режиме, используя средство
Google Colab. Для этого вам нужна лишь неограниченная (unrestricted) учетная запись Google. (Для работы с Jupyter Notebook можно создать отдельную учетную
запись. Только не забудьте пароль к ней.)
Hello World!
Теперь, когда мы имеем в своем распоряжении все необходимые средства для выпаса данных, мы можем попробовать написать и исполнить нашу первую программу на языке Python. Мы не будем оригинальничать с решением этой задачи, а последуем установившейся традиции и создадим простую программу, которая выводит на экран текст Hello World! (Здравствуй, мир!). Для начала нам нужно создать
файл для написания и сохранения соответствующего кода.
Создание файла автономного сценария Python
при помощи Atom
Редактор кода Atom работает так же, как и любая другая программа для редактирования текста; ее можно запустить при помощи мыши или из командной строки
(терминала).
12
Если отобразится запрос о том, как открыть данный файл, я рекомендую выбрать Google Chrome.
40
|
Глава 1
Для запуска программы при помощи мыши нужно найти ее значок на используемом устройстве:
Chromebook
Значок находится в рамке Приложения Linux (Linux apps).
Mac
Значок находится в папке Приложения (Applications) или в средстве Launchpad.
Windows
Значок находится в меню Пуск или его можно найти, выполнив поиск по слову
Atom в строке поиска этого меню. Если после первой установки на Windows 10
редактора кода Atom его значок отсутствует в меню Пуск и в результатах поиска, выяснить причину этой проблемы можно с помощью диагностических видеоклипов на YouTube (https://youtube.com/watch?v=N4liFqsK9nM).
Также редактор Atom можно запустить, просто исполнив в окне терминала соответствующую команду:
atom
При первом запуске редактора на устройстве Chromebook выводится сообщение о
создании пароля для нового набора ключей Choose password for new keyring. Поскольку мы будем использовать редактор Atom только для редактирования кода и
данных, нам такой набор ключей не нужен. Поэтому просто закройте это сообщение, нажав кнопку Cancel. На компьютерах macOS выводится предупреждение о
том, что данная программа была загружена из Интернета. Это сообщение также
можно игнорировать.
Рис. 1.4. Окно приветствия редактора кода Atom
Введение в первичную обработку (выпас) и качество данных
|
41
В конечном итоге мы получим окно приветствия редактора, пример которого показан на рис. 1.4.
По умолчанию при запуске редактор кода Atom показывает одно или несколько
вкладок приветствия. Эти вкладки можно просто закрыть обычным способом,
щелкнув по значку крестика справа от названия вкладки. (В старых версиях Atom
этот значок появляется при наведении курсора мыши на название вкладки.) В результате в центре окна останется только вкладка нового файла untitled. При желании можно также свернуть и панель Project с левой стороны окна. Для этого нужно
навести курсор мыши на эту панель, а затем щелкнуть по значку <, который появится посредине правой стороны этой панели.
Прежде чем приступать к написанию какого-либо кода, сохраним наш файл там,
где мы будем знать, где найти его, — в нашей папке data_wrangling. Для этого в
меню File выбираем опцию Save as, в открывшемся окне навигации по файловой
системе переходим в эту папку, затем в поле названия файла File name внизу окна
вводим название файла HelloWorld.py и нажимаем кнопку Save.
При сохранении файлов автономных сценариев Python в конце их названия необходимо добавлять расширение .py (это английские буквы py,
а не русские ру). Хотя код Python будет работать и без этого расширения, его наличие позволит редактору кода Atom выполнять выделение
синтаксических элементов кода, которое упоминалось ранее в этой главе в разд. "Установка языка Python, среды Jupyter Notebook и редактора кода". Эта возможность значительно облегчит правильное написание
кода с первого же раза.
Создание блокнота кода Python в Jupyter Notebook
Как вы, возможно, заметили при тестировании установки среды разработки Jupyter
Notebook в разд. "Проверка успеха установки", в этой среде используется интерфейс в виде окна простого браузера. В действительности, при запуске этой среды
установки посредством исполнения команды jupyter notebook на локальном компьютере создается миниатюрный веб-сервер13. В результате можно создавать новые
файлы Python и исполнять другие команды прямо в браузере.
Первым делом запустите программу терминала и перейдите в папку data_wranlging,
исполнив команду:
cd ~/Desktop/data_wrangling/
Затем запустите среду разработки Jupyter Notebook, исполнив команду:
jupyter notebook
Сначала в окне терминала будут отображаться разные информационные сообщения
о ходе запуска программы, и в конечном итоге в используемом по умолчанию браузере откроется новая вкладка, содержащая список папок и файлов. В данном случае
13
Не волнуйтесь, это веб-сервер, не видимый из Интернета.
42
|
Глава 1
этот список будет пустой. В правом конце заголовка списка нажмите меню New и в
открывшемся списке выберите опцию Python3 (ipykernel). Откроется новая вкладка браузера, называющаяся Untitled. Выполните двойной щелчок мышью по названию вкладки справа от логотипа Jupyter в левом верхнем углу страницы и в открывшемся диалоговом окне переименуйте файл блокнота на HelloWorld.
Поскольку, как упоминалось ранее, при запуске Jupyter Notebook также
запускается и локальный веб-сервер, то важно не закрывать окно терминала, из которого была запущена эта среда разработки, пока вы работаете в ней. Закрытие этого окна остановит работу локального сервера, и доступ к среде будет утерян.
К счастью, блокноты Jupyter автоматически сохраняются каждые две
минуты, поэтому даже в случае сбоя приложения объем потерянной работы не будет слишком большим. При всем этом может быть полезным
свернуть окно терминала, из которого была запущена среда разработки
Jupyter Notebook, чтобы случайно не закрыть его в процессе работы.
Создание блокнота кода Python в Google Colab
Первым делом войдите в свою учетную запись Google, которую вы хотите использовать для работы по выпасу данных, а затем посетите веб-сайт Colab
(https://colab.research.google.com). Откроется веб-страница с наложенным на нее
окном, как показано на рис. 1.5.
В правом нижнем углу наложенного окна щелкните по ссылке New notebook. Откроется новая страница Untitled0.ipynb. Выполните двойной щелчок по названию
страницы в левой верхней части страницы и переименуйте ее на HelloWorld.ipynb14.
Рис. 1.5. Домашняя страница Google Colab
14
Ранняя версия Jupyter Notebook называлась iPython Notebook, что объясняет использование расширения имени файла .ipynb.
Введение в первичную обработку (выпас) и качество данных
|
43
Создаем программу
Теперь создадим нашу первую программу, которая просто выводит на экран текст
"Hello World!". Независимо от используемого вами подхода для создания программы, код для нее одинаковый (листинг 1.1).
Листинг 1.1. Программа hello_world.py
# следующий код выводит на экран текст "Hello World!"
print("Hello World!")
В файле автономного сценария
Просто скопируйте (или введите вручную) в файл код в листинге 1.1 и сохраните
файл.
В блокноте
При создании нового файла блокнота Python в нем по умолчанию создается пустая
ячейка для кода. В Jupyter Notebook эта ячейка обозначена меткой In [] слева от
нее, а в Google Colab — значком исполнения кода (направленным вправо треугольником). Скопируйте (или введите вручную) в эту ячейку код в листинге 1.1.
Исполняем программу
Теперь, когда мы создали и сохранили нашу программу на языке Python, мы можем
исполнить ее.
В файле автономного сценария
Запустите программу терминала и перейдите в папку data_wrangling, исполнив следующую команду:
cd ~/Desktop/data_wrangling
Исполните команду ls, чтобы убедиться в том, что папка содержит файл программы HelloWorld.py. Наконец, запустите программу на исполнение, исполнив в терминале следующую команду15:
python HelloWorld.py
В результате на экране в новой строке должен отобразиться текст "Hello World!",
после чего снова возвратится строка запроса команды (означая завершение исполнения программы).
15
Возможно, вместо python нужно будет ввести python3.
44
|
Глава 1
В блокноте
Нажмите кнопку запуска на исполнение (находится в панели инструментов в
Jupyter Notebook и слева от ячейки кода в Google Colab). Под ячейкой кода должен
отобразиться текст "Hello World!".
Если все сработало, как описано, тогда я вас поздравляю с вашей первой программой на языке Python.
Документирование, сохранение
и управление версиями своего кода
Прежде чем забираться в настоящие дебри программирования на языке Python в
главе 2, нам нужно выполнить несколько приготовлений. Это может показаться вам
нудным и утомительным, но подготовка к правильному документированию своей
работы сэкономит вам в будущем десятки часов напрасного труда и убережет вас
от расстройства. Более того, аккуратное комментирование, сохранение и управление версиями своего кода является важной частью обеспечения надежной работы
первичной обработки данных. И хотя на данном этапе это не совсем увлекательное
занятие, довольно скоро все эти шаги станут для вас второй натурой (я обещаю), и
вы увидите, насколько это повысит скорость и эффективность вашей работы с данными.
Документирование
Возможно, вы заметили, что при исполнении нашей первой программы (см. листинг 1.1) на экране отобразился только текст "Hello World!", но не ее первая строка
кода. Это объясняется тем, что эта строка кода является комментарием, который
простым языком объясняет назначение данного кода. Почти все языки программирования (а также некоторые типы данных) поддерживают возможность вставки
комментариев, поскольку это отличный способ предоставить любому пользователю16, исследующему ваш код, необходимый контекст и объяснение, чтобы он мог
понять назначение вашей программы или раздела кода в ней.
Хотя многие программисты имеют склонность обделять вниманием (читай игнорировать) процесс комментирования своего кода, это, наверное, самая ценная привычка в программировании, которую можно выработать. Комментирование не
только сэкономит вам и любому другому вашему товарищу по работе огромное
количество усилий и времени при исследовании кода Python. Это также наилучший
способ по-настоящему усвоить все, что вы изучаете о программировании. Поэтому,
хотя примеры кода в этой книге уже снабжены комментариями, я настоятельно рекомендую переписать их своими словами. Таким образом, если в будущем вам
16
И вам самим впоследствии.
Введение в первичную обработку (выпас) и качество данных
|
45
придется снова просматривать эти файлы, они будут содержать подробный анализ
вашего понимания каждой задачи кодирования при встрече с ней.
Другим важным процессом документирования для выпаса данных является ведение, как я его называю, "дневника данных". Подобно личному дневнику, дневник
данных можно вести и упорядочивать любым предпочитаемым вами образом. Здесь
главной задачей является зафиксировать то, что вы делаете, по мере того, как вы
это делаете. Будь то просмотр различных веб-страниц в поисках данных, отправление сообщений электронной почты экспертам по интересующим вас вопросам или
разработка программы, вам нужно вести учет всех эти активностей, поскольку вы
вскоре забудете, когда и зачем вы их выполняли.
Первой записью в таком дневнике для любого проекта по выпасу данных должен
быть вопрос, на который вы пытаетесь получить ответ. Хотя это может быть и
трудно, пытайтесь сформулировать этот вопрос в одном предложении и запишите
его вверху своего дневника проекта. Почему важно сформулировать этот вопрос
одним предложением? Потому что в процессе настоящего выпаса данных вы опуститесь в достаточно большое количество "заячьих нор" (например, в поиске ответа
на вопрос источника ваших данных или пытаясь решить какую-либо задачу программирования), что будет легко потерять из виду, чего вы исходно пытались добиться и почему. Но когда этот вопрос занимает место вверху вашего дневника
данных, вы всегда легко вспомните главную задачу.
Целевой вопрос дневника данных также окажет вам значительную помощь в принятии решений о том, на что тратить свое время в процессе выпаса данных. Например, если обрабатываемый набор данных содержит незнакомые вам термины, то
следует ли пытаться выяснить значение каждого из них? Да, если это поможет получить ответ на ваш вопрос. В противном же случае может быть пора переходить к
другой задаче.
Успешно ответив на свой вопрос (хотя бы частично), вы почти наверняка обнаружите наличие других вопросов, на которые вы захотите ответить, Или вы захотите
ответить на этот же вопрос спустя неделю, месяц или год. Наличие вашего дневника данных под рукой поможет вам в следующий раз сделать это намного быстрее и
легче. Но это не означает, что такой подход не требует усилий. Исходя из моего
опыта, ведение дневника данных увеличивает время первого исполнения проекта
на 40%, но ускоряет его повторное исполнение (например, с новой версией набора
данных) по крайней мере в два раза. Дневник данных также можно использовать в
качестве ценного доказательства проделанной работы: если вам когда-либо потребуется информация о процессе, посредством которого вы получили свои результаты выпаса данных, вы (или любой другой) сможете найти эту информацию в своем
дневнике данных.
Что касается формата, в котором вести свой дневник данных, то этот вопрос полностью на ваше усмотрение. Некоторым нравится использовать большой объем вычурного форматирования, тогда как для других достаточно простого текстового
файла. Можно даже использовать простую тетрадь — используйте то, что удобно
46
|
Глава 1
для вас. Тогда как ваш дневник данных будет ценным источником справочной информации при обсуждении с другими ваших данных (и процесса их обработки),
вести его нужно в подходящем именно вам формате.
Сохранение
Кроме тщательного документирования своей работы посредством комментариев в
коде и ведения дневника данных, обязательно регулярно сохраняйте ее. К счастью,
процесс сохранения, по сути, встроен в рабочий поток: блокноты выполняют сохранение автоматически через регулярные интервалы времени, а для исполнения
кода в файле автономного сценария сначала нужно сохранить все выполненные
изменения. Сохранение можно выполнять, как используя меню графического интерфейса, так и с помощью клавиатуры, нажатием комбинации клавиш <Ctrl>+<S>
(что для меня вошло в привычку). В любом случае, следует сохранять свою работу
как минимум каждые 10 минут.
Для файлов автономных сценариев будет полезным узнать, как используемый редактор кода указывает на наличие несохраненных изменений.
Например, в редакторе кода Atom это обозначается небольшой цветной
точкой вместо крестика для закрытия документа (на вкладке справа от
названия файла). Если код не исполняется ожидаемым образом, проверьте, что вы сохранили выполненные изменения, а затем повторите
его исполнение.
Управление версиями
Подобно большинству типов письменной деятельности, написание программ является итерационным процессом. Я всегда предпочитала написать небольшой фрагмент кода, протестировать его, в случае успешных результатов тестирования добавить еще немного кода, снова протестировать объединенный код, и так далее. Одна
из целей такого подхода — иметь возможность легко возвратиться к более ранней
версии кода в случае, если последний добавленный код не работает должным образом17.
В то же самое время не всегда можно гарантировать, что ваш код будет в рабочем
состоянии, когда вам нужно временно прекратить работать с ним по какой-либо
причине, не имея при этом возможности протестировать его. Поэтому всегда стоит
иметь надежную рабочую версию кода, с которой можно возобновить работу над
программой. Эта задача решается посредством управления версиями кода.
17
Означает, что результаты исполнения кода отличаются от ожидаемых, при исполнении кода выдаются сообщения об ошибке или вообще не выдается никаких результатов.
Введение в первичную обработку (выпас) и качество данных
|
47
Введение в работу с GitHub
Управление версиями — это, по сути, система для резервного копирования своего
кода как на локальном компьютере, так и в облачном хранилище. В этой книге для
управления версиями мы будем использовать пользующийся огромной популярностью веб-сайт GitHub, на котором можно бесплатно сохранять резервные копии
своего кода. Хотя с этим веб-сайтом можно взаимодействовать разными способами,
мы будем использовать для этого командную строку, поскольку таким образом код
можно быстро и надежно сохранить до следующего раза всего лишь несколькими
простыми командами.
Рис. 1.6. Создание нового репозитория на GitHub.com
Для начала вам нужно создать учетную запись на веб-сайте GitHub, установить
приложение Git на свой компьютер, а затем подключиться к учетной записи на
GitHub.
1. Посетите веб-сайт GitHub по адресу https://github.com и нажмите кнопку Sign
Up. Введите имя пользователя (скорее всего, вам придется перепробовать не-
48
|
Глава 1
сколько разных имен, пока не найдете еще не занятое), адрес своей электронной
почты и пароль. Не забудьте записать свой пароль или сохранить его в менеджере паролей, поскольку вскоре он вам потребуется.
2. Создав учетную запись, войдите в нее и нажмите кнопку New в левой верхней
части страницы. Откроется окно Create a new repository (рис. 1.6).
3. Присвойте репозиторию название. Название может быть любым по вашему усмотрению, но я рекомендую использовать описательное название, например,
data_wrangling_exercises.
4. Установите переключатель Private и флажок Add a README file.
5. Нажмите кнопку Create repository.
Откроется новая страница с названием репозитория data_wrangling_exercises крупным шрифтом и небольшим значком карандаша в правом верхнем углу. Щелчок
мышью по этому значку открывает интерфейс редактирования — это ваш файл
README, который вы можете использовать для описания вашего репозитория. Поскольку мы будем использовать этот репозиторий для хранения упражнений из
этой книги, мы и назовем его соответствующим образом, как показано на рис. 1.7.
Рис. 1.7. Обновление файла README в репозитории GitHub
Прокрутите страницу вниз до самого конца. В левой нижней части страницы должен быть значок профиля, справа от которого находится редактируемая область с
заголовком Commit changes, под которым находится текстовая строка, содержащая
текст по умолчанию Update README.md, и текстовое поле. В текстовой строке
замените текст по умолчанию кратким описанием выполненных изменений. Это
будет коммит-сообщение. Например, я ввела текст "Added description of repo
contents", как показано на рис. 1.8. Затем нажмите кнопку Commit changes.
После обновления содержания страницы на ней внизу начального заголовка
data_wrangling_exercises должен отображаться текст, добавленный вами к основному файлу. А непосредственно над этим текстом должно находиться содержимое
вашего коммит-сообщения, а также время, приблизительно истекшее после нажатия вами кнопки Commit changes. Щелчок по тексту "2 commits" справа от этого
Введение в первичную обработку (выпас) и качество данных
|
49
текста выводит на экран историю коммитов (commit history), содержащую все изменения (на данном этапе всего лишь два), выполненные с данным репозиторием
(repo), как показано на рис. 1.9.
Рис. 1.8. Добавление коммит-сообщения
в изменения файла README
Рис. 1.9. Краткая история коммитов
для нашего нового репозитория
Чтобы просмотреть эффект какого-либо коммита на определенный файл, щелкните
по шестизначному коду в правом конце строки с названием коммита. В результате
откроется так называемое представление diff (сокращение от difference) данного
файла. В левой панели будет отображаться версия файла до выполнения коммита, а
в правой — версия после коммита. (Для двухпанельного представления может потребоваться нажать кнопку Split справа над содержимым файла.)
На этом этапе вы можете задаваться вопросом, как все это связано с резервным копированием кода, поскольку мы только нажимаем разные кнопки и редактируем
50
|
Глава 1
тот или иной текст. Все очень просто. Теперь, когда у нас есть репозиторий (repo)
на GitHub, мы можем создать его копию на локальной машине, а затем посредством
всего лишь нескольких команд терминала выполнять коммиты18 разрабатываемого
кода и сохранять их резервные копии на этом веб-сайте.
Для резервного копирования локальных файлов: установка и настройка средства Git. Подобно языку Python, программное обеспечение Git устанавливается на
локальном компьютере и управляется командами из терминала. Поскольку управление версиями является неотъемлемой частью большинства процессов кодирования, средство Git встраивается в операционные системы macOS и Linux. На
Windows это средство можно установить как программное обеспечение. Также его
можно установить и на устройства Chromebook, используя для этого приложение
Termux. Установив Git на свой компьютер, запустите программу терминала и исполните в нем следующую команду:
git --version
На экране должна отобразиться версия установленного средства Git, что означает
его успешную установку на локальном компьютере. Но нам еще нужно задать имя
пользователя и электронную почту (для этих параметров можно использовать любые значения), выполнив следующие команды:
git config --global user.email ваша_электронная_почта@domain.com
git config --global user.name ваше_имя_пользователя
Следующим шагом нам нужно создать ключ аутентификации на локальном устройстве, чтобы при создании резервной копии на GitHub система сайта знала, что ее
создаете действительно вы, а не кто-либо, кто каким-то образом узнал ваши имя
пользователя и пароль.
Для этого нам нужно создать так называемый ключ SSH — длинную, однозначную
строку символов, хранящуюся на локальном устройстве, посредством которой система GitHub может идентифицировать это устройство. Этот ключ можно легко создать, исполнив в окне терминала следующую команду:
ssh-keygen -t rsa -b 4096 -C "ваша_электронная_почта@domain.com"
Когда в ходе исполнения этой команды отобразится сообщение "Enter a file in which
to save the key" (Введите название файла для сохранения ключа), просто нажмите
клавишу <Enter>, чтобы сохранить ключ в файле по умолчанию. Таким образом
будет легче найти этот файл, когда чуть далее нам нужно будет скопировать ключ в
нашу учетную запись GitHub. Затем отобразится следующее сообщение:
Enter passphrase (empty for no passphrase):
Введите парольную фразу (оставьте пустым, чтобы продолжить без пароля):
Обязательно введите парольную фразу. При этом не используйте тот же самый пароль, что для вашей учетной записи GitHub или любой другой учетной записи. Но
поскольку вам нужно будет предоставлять этот пароль при каждом сохранении ре18
Англ. commit — в данном контексте это операция, которая сохраняет самые последние изменения
кода в репозитории.
Введение в первичную обработку (выпас) и качество данных
|
51
зервной копии на GitHub19, то он должен быть легко запоминаемым. Поэтому попробуйте использовать, к примеру, первые три слова второго куплета вашей любимой песни. В любом случае любой пароль длиной в 8–10 символов будет удовлетворительным.
Далее нужно будет снова ввести этот пароль, чтобы подтвердить его. После этого
можно скопировать ваш ключ в вашу учетную запись GitHub. Это позволит системе
GitHub удостоверять вашу личность, сравнивая эту копию с оригиналом на вашем
локальном компьютере. Для этого сначала щелкните по значку вашего профиля в
правом верхнем углу страницы GitHub и выберите в выпадающем меню опцию
Settings. Затем в левой панели щелкните по опции SSH and GPG Keys и в правой
верхней области открывшейся панели нажмите кнопку New SSH key (рис. 1.10).
Откроется страница Add new.
Рис. 1.10. Страница веб-сайта GitHub
для создания ключей
Далее нам нужно перейти в основную папку пользователя на своем устройстве (это
папка, в которой открывается новое окно терминала) и отобразить в ней скрытые
файлы:
На устройствах Chromebook
Основной папкой пользователя на этих устройствах является папка Linux files.
Чтобы отобразить скрытые файлы, щелкните по значку из трех вертикальных
точек в правой верхней части окна и выберите опцию Show hidden files.
19
В зависимости от вашего устройства, этот пароль можно сохранить в так называемом брелке для
ключей (keychain). Дополнительную информацию по этому вопросу см. в документации на GitHub.
52
|
Глава 1
macOS
Скрытые файлы можно отобразить (или скрыть уже отображающиеся), нажав
комбинацию клавиш <Command>+<Shift>+<.>.
Windows
В Проводнике Windows выполните последовательность команд меню View |
Options | Change folder and search options. На вкладке View в разделе Advanced
settings установите флажок Show hidden files, folders, and drives.
Найдите и откройте папку (это действительно папка, а не расширение), называющуюся .ssh, а затем откройте (используя любой базовый текстовый редактор, например Atom) файл id_rsa.pub. Используя команды клавиатуры, выберите и скопируйте все содержимое этого файла, а затем вставьте его в пустое текстовое поле
Key на странице Add new веб-сайта GitHub (рис. 1.11).
Рис. 1.11. Страница Add new для загрузки ключа SSH
в учетную запись GitHub
Наконец, присвойте этому ключу имя, чтобы знать, к какому устройству он относится, а затем нажмите кнопку Add SSH key. Возможно, что для завершения процесса потребуется ввести пароль вашей учетной записи GitHub. Вот и все. Теперь
можно снова скрыть папки и файлы на локальном устройстве и завершить подключение учетной записи GitHub к локальному устройству и/или учетной записи Colab.
Я рекомендую использовать команды клавиатуры для копирования и
вставки ключа SSH, поскольку важно скопировать точную последовательность символов (включая пробелы) этого ключа, а при выполнении
этих операций при помощи мыши существует возможность нечаянно
прихватить или пропустить какие-либо символы. Если при попытке до-
Введение в первичную обработку (выпас) и качество данных
|
53
бавления нового ключа SSH система GitHub выдает сообщение об ошибке, попробуйте следующие два способа ее решения:
убедитесь, что вы скопировали содержимое файла с расширением
.pub, а не другого файла в папке .ssh с таким же самым названием, но
без расширения;
закройте файл, не сохраняя его, а затем повторите попытку.
Если это не устранит проблему, можно просто полностью удалить папку
.ssh, а затем сгенерировать новые ключи. Поскольку ничего еще не было
загружено на веб-сайт GitHub, то не будет утеряно никакой информации.
Сводим все в одно целое. В завершение нам нужно создать на локальном компьютере связанную копию нашего репозитория GitHub. Это можно легко сделать посредством следующего процесса:
1. Запустите программу терминала и перейдите в папку data_wrangling.
2. В своей учетной записи
data_wrangling_exercises.
на
веб-сайте
GitHub
откройте
репозиторий
3. На странице репозитория нажмите кнопку Code в правом верхнем углу панели
репозитория.
4. В открывшемся контекстном меню Clone выберите опцию SSH, а затем щелкните по значку буфера обмена справа от URL-адреса репозитория (рис. 1.12).
Рис. 1.12. Копирование адреса размещения репозитория GitHub
5. Обратно в окне терминала введите команду git clone, пробел, а затем вставьте
скопированный URL-адрес (или введите его с клавиатуры). Полностью эта команда должна выглядеть следующим образом:
git clone git@github.com:susanemcg/data_wrangling_exercises.git
6. Нажмите клавишу <Enter>. В процессе исполнении команды может отобразиться сообщение, хотите ли вы добавить узел назначения в список известных хос-
54
|
Глава 1
тов. Введите yes и нажмите клавишу <Enter>. Далее может отобразиться запрос
ввести ваш пароль SSH. Введите его и снова нажмите клавишу <Enter>.
7. После завершение копирования репозитория, что будет обозначено выводом слова
done, исполните команду ls. Среди прочих элементов выведенный список должен
содержать папку скопированного репозитория GitHub data_wrangling_exercises.
8. Перейдите в эту папку, выполнив команду cd data_wrangling_exercises. Исполните команду ls. В списке должен отобразиться файл README.md.
Вот, наконец, и все. Возможно, что этот процесс выглядит довольно сложным, но
имейте в виду, что ключ SSH нужно создавать только один раз. Также процесс клонирования для каждого репозитория нужно выполнять тоже только один раз. А для
всех упражнений в этой книге можно использовать один и тот же репозиторий.
Теперь давайте посмотрим, как все это работает в действии, добавив в наш репозиторий файл Python. В окне Finder перейдите в папку data_wrangling. Сохраните и
закройте файл HelloWorld.py или HelloWorld.ipynb, а затем перетащите его в папку
data_wrangling_exercises. Затем в окне терминала проверьте наличие этого файла в
данной папке, выполнив команду ls.
Одним из последних шагов нам нужно проинформировать программу Git, что мы
хотим включить наш файл Python в резервную копию, которая будет сохраняться
на веб-сайте GitHub. Затем мы сохраним текущую версию файла, а затем собственно загрузим его в репозиторий на GitHub.
Начнем этот процесс с исполнения команды git status в окне терминала. В результате должно отобразиться сообщение со списком неотслеживаемых файлов
Untracked files: (), содержащим наш файл Python. Это было ожидаемо, но удостовериться в этом будет не лишним. Теперь приступим к реализации вышеописанного процесса сохранения резервной копии в репозитории на веб-сайте GitHub. Обратите внимание на то, что последующие команды выдают выходные сообщения в
терминале:
1. В окне терминала исполните команду git add название_вашего_файла_python.
2. Затем исполните команду git commit -m "Adding my Hello World Python file." название_вашего_файла_python. Параметр –m указывает, что текст в кавычках должен
использоваться в качестве коммит-сообщения. Это эквивалент командной строки сообщения, которое мы предоставили на веб-сайте GitHub для файла
README некоторое время ранее.
3. Наконец, исполните команду git push.
Последняя команда, собственно, и загружает файл на веб-сайт GitHub. Обратите
внимание на то, что последняя команда, понятное дело, не будет работать без наличия подключения к Интернету. Но предыдущие две команды можно исполнять в
любое время, а команду push — дождавшись наличия подключения. Чтобы удостовериться в том, что резервная копия была создана должным образом, перезагрузите
Введение в первичную обработку (выпас) и качество данных
|
55
страницу репозитория GitHub. Теперь она должна содержать ваш файл Python и
соответствующее коммит-сообщение.
Резервное копирование онлайновых файлов Python: подключение Google
Colab к GitHub. Если вся работа по выпасу данных выполняется в онлайновом режиме, то средство Google Colab можно подключить напрямую к своей учетной записи GitHub. Проверьте, что вы находитесь в своей учетной записи GitHub, а затем
в новой вкладке браузера откройте страницу https://colab.research.google.com/
github. Откроется всплывающее окно с запросом выполнить вход в свою учетную
запись GitHub, а затем другое всплывающее окно с запросом Authorize Colaboratory
(разрешить средство Colaboratory). После предоставления этого разрешения в левой
части всплывающего окна будет отображен список ваших репозиториев GitHub.
При выборе требуемого репозитория в нижней части окна отобразится список содержащихся в нем файлов блокнотов Jupyter.
В всплывающем окне Google Colab отображаются только файлы блокнотов Jupyter (т. е. файлы с расширением .ipynb). Для просмотра всех
файлов репозитория нужно перейти на веб-сайт GitHub.
Сводим все в одно целое. При работе в Google Colab, чтобы добавить новый файл
в репозиторий GitHub, нужно выполнить последовательность команд меню File |
Save a copy in GitHub. В результате откроется и закроется несколько всплывающих окон, в которых Colab выполняет вход в вашу учетную запись GitHub, а затем
в разделе Repository всплывающего окна Copy to GitHub можно будет выбрать
репозиторий, в котором сохранить ваш файл. В этом же окне в разделе File path
указывается имя сохраняемого файла, которое при желании можно изменить, а в
разделе Commit message можно ввести коммит-сообщение. Наконец, слева внизу
этого окна находится флажок Include a link to Colaboratory (Включить ссылку на
Colaboratory). Если этот флажок установить, тогда сохраненный на GitHub файл
будет содержать метку Open in Colab, щелчок по которому отроет этот файл на
веб-сайте Google Colab. Файлы блокнотов, которые явно не сохраняются на GitHub
вышеописанным способом, сохраняются на вашем диске Google Drive в папке
Colab Notebooks. Доступ к этим файлам можно получить, выбрав вкладку Google
Drive на веб-сайте Colab https://colab.research.google.com.
Папки, директории и репозитории
Возможно, вы заметили, что на всем протяжении этой главы термины папка, директория и даже репозиторий употребляются без большой разницы между ними.
В действительности, первые два термина обозначают одно и то же понятие, а последний относится исключительно к папкам, в которых по крайней мере некоторые файлы отслеживаются системой Git.
56
|
Глава 1
Иными словами, со всеми этими элементами можно взаимодействовать, используя мышь и обычные окна Finder (или проводника Windows), чтобы создавать,
удалять и открывать файлы. Единственная разница состоит в том, что в случае
репозитория необходимо использовать команды Git типа add или rm, чтобы содержать все в порядке. Мы будем рассматривать каждую из этих ситуаций более
подробно по мере их возникновения в последующих главах.
Заключение
Целью этой главы было предоставить вам общее представление о том, чему вы можете научиться в этой книге: что я имею в виду под "выпасом20 данных" и "качеством данных", а также почему я полагаю, что язык программирования Python является правильным инструментом для этой работы.
Кроме этого, мы рассмотрели все необходимые приготовления, чтобы начать (и
продолжать) использовать язык Python для выпаса данных. В частности, были предоставлены инструкции для организации предпочитаемого вами подхода программирования: использование файлов автономных сценариев Python или блокнотов
Jupyter на локальной машине или работа с блокнотами Jupyter в онлайновом режиме, используя средство Google Colab. Наконец, мы рассмотрели использование
контроля версий с любым из этих подходов для резервного копирования, документирования и предоставления общего доступа к своей работе.
В следующей главе мы пойдем намного дальше нашей программы "Hello World!",
исследуя основы языка программирования Python, и даже предпримем попытку
решить наш первый проект по выпасу данных: один день работы системы Citi Bike
города Нью-Йорк.
20
Первичной обработкой. — Примеч. ред.
ГЛАВА 2
Введение в Python
Если вы можете читать этот текст, то у меня для вас есть хорошая новость: у вас не
будет никаких проблем с обучением программированию. Почему? Поскольку уровень сложности компьютерных языков программирования в целом, а языка Python
в частности, намного ниже, чем естественных человеческих языков. Языки программирования разработаны людьми и предназначены для чтения, в большинстве
случаев, компьютерами. Поэтому у них более простая грамматика и меньшее количество "частей речи", чем у естественных языков. Поэтому, если вы чувствуете себя
достаточно уверенно при чтении текста на русском языке, который известен своим
большим словарным составом и сложной орфографией, то можете быть уверенными в том, что овладеть основами языка программирования Python вполне в пределах ваших способностей.
К концу этой главы вы будете владеть всеми навыками программирования на этом
языке, необходимыми для того, чтобы начать выполнять базовый выпас данных,
используя наиболее распространенные форматы данных, которые мы рассмотрим в
главе 4. Чтобы достичь этой цели, мы начнем с выполнения базовых упражнений
по программированию, охватывающих следующие области:
основные "части речи" языка Python — его базовую грамматику и синтаксис;
как компьютер считывает и интерпретирует код на языке Python;
как использовать "рецепты" кода, созданные другими разработчиками (и вами
самими), чтобы быстро расширить возможности своего кода.
На протяжении этой главы приводятся фрагменты кода, иллюстрирующие каждое
рассматриваемое понятие. Эти примеры также собраны в блокнотах Jupyter и в
файлах автономных сценариев Python на GitHub. Их можно загрузить и исполнять в
среде разработки Google Colab или на локальном компьютере. Так как эти файлы
позволят вам увидеть код примеров этой главы в действии, я настоятельно рекомендую создать новый блокнот Colab или Jupyter или файл автономного сценария
Python и практиковаться в написании, комментировании и исполнении этого кода
самостоятельно. (Вспомнить, как это делается, можно в разд. "Hello World!" главы 1.)
Вы можете подумать, что в этом нет большого смысла, но нет ничего более полезного для развития навыков выпаса данных и выработки уверенности в своих способностях, чем создание файла Python с нуля, с последующим обозрением, как написанный вами самими код заставляет компьютер делать то, что вы хотите, даже
58
|
Глава 2
если вы просто перепечатали его с другого файла. Да, таким образом вам придется
столкнуться с большим количеством сложностей, но в этом и заключается определенная суть: в действительности качественная обработка данных состоит не в том,
чтобы научиться делать что-то "правильно", а в том, чтобы научиться восстанавливаться после ошибок. Предоставив себе возможность делать небольшие ошибки на
начальном этапе (и научившись, как распознавать и исправлять их), вы получите
возможность по-настоящему достигнуть успеха как специалист по выпасу данных,
так и как программист. Вы никогда не сможете получить этот опыт, просто исполняя готовый код.
Поскольку эти небольшие ошибки имеют такую большую важность, в эту главу я
включила несколько врезок "Быстрая прокрутка", в которых описаны способы
продвижения предоставленных примеров на один шаг дальше, что часто связано с
преднамеренной "поломкой" кода. К концу этой главы вы будете готовы объединить рассмотренные базовые понятия в полноценную программу выпаса данных,
использующую реальные данные. Для каждого примера я также предоставляю несколько явных напоминаний о типах заметок, которые желательно включать в свой
дневник данных, о том, когда желательно выполнять резервное копирование на
GitHub, и тому подобное. Делаю я это для того, чтобы вы по-настоящему освоили
эти процессы, а также развили чувство, когда необходимо предпринимать эти шаги
в ходе работы над своими собственными проектами по выпасу данных.
Теперь, когда вы знаете, куда мы направляемся, приступим к работе!
"Части речи" языков программирования
Разные человеческие языки используют разные словарные составы и синтаксические структуры, но все они обычно имеют много общих фундаментальных понятий. Взглянем, например, на следующие два предложения
My name is Susan.
Je m'appelle Susan.
// Английский
// Французский
Оба эти предложения выражают, по сути, одно и то же: как меня зовут. И хотя в
разных языках используются разные слова и слегка разные грамматические структуры, все языки содержат такие части речи, как подлежащее, сказуемое, глаголы и
модификаторы. Они также следуют подобным грамматическим и синтаксическим
правилам в том смысле, что они структурируют идеи и слова в предложения, абзацы и так далее.
Многие языки программирования также имеют подобные ключевые структурные и
организационные элементы, которые грубо соответствуют подобным элементам
естественных языков. Чтобы получить лучшее представление о том, как все это работает, начнем изучение языков программирования таким же самим способом, какой бы мы применили при изучении нового естественного языка: исследуя "части
речи" языков программирования.
Введение в Python
|
59
Существительные ≈ переменные
В английском языке (как и в русском) существительные часто определяются как
любое слово, обозначающее "лицо, понятие или предмет". Хотя это не сверхточное
определение, оно удачно иллюстрирует, что существительным могут быть разные
сущности. В языках программирования "существительными", используемыми при
написании "предложений" программ, являются переменные. Но вместо людей, понятий и предметов они обозначают один из пяти основных типов данных:
числовой;
строчный;
список;
словарь;
булев.
Как и в случае с естественными языками, где в разных языках существительное
может быть разных типов, в различных языках программирования типы обозначаемых переменными данных также могут быть разными. Например, то, что в языке
Python называется списком (list), в языках, например, C или JavaScript называется
массивом (arrray). А объекты JavaScript в Python называются map или словарями
(dictionary)1.
Скорее всего, вы уже можете предположить, что собой представляют некоторые
типы данных из приведенного ранее списка. В отличие от существительных в естественных языках, каждый тип данных в языке Python можно с легкостью узнать по
его форматированию и пунктуации. Поэтому, при условии уделения достаточного
внимания символам вокруг данных, нам не нужно беспокоиться о смешении разных типов данных.
Чтобы получить представление об однозначной структуре пунктуации каждого типа данных Python, рассмотрим пример в листинге 2.1. При этом обязательно создайте свою копию этого кода в автономном файле или блокноте, чтобы увидеть
возможность выделения синтаксических элементов в действии. Например, числа
должны выделяться другим цветом, чем строки, так же как круглые и фигурные
скобки и комментарии (строки, начинающиеся символом #).
Листинг 2.1. Сценарий parts_of_speech.py
# Число (number) — это просто одна или несколько цифр
25
# Строка (string) — это комбинация любых символов в парных кавычках
"Hello World"
1
Многие специфичные для языка типы данных, например кортеж (tuple) в Python, не существенны
для работы по выпасу данных, поэтому мы не будем рассматривать их здесь в подробностях.
60
|
Глава 2
# Список (list) состоит из нескольких элементов, разделенных запятыми, а сам список
# берется в квадратные скобки.
# Обратите внимание на то, что в Python первый элемент списка считается
# в позиции 0, следующий — в позиции 1 и так далее.
["this","is",1,"list"]
# Словарь (dict) — это набор пар ключ:значение, разделенных запятыми,
# а весь список берется в фигурные скобки.
{"title":"Practical Python for Data Wrangling and Data Quality",
"format": "book",
"author": "Susan E. McGregor"
}
# Булев тип (boolean) имеет только два значения — истина (true) и ложь (false).
True
Конечно же, это далеко не исчерпывающий список. Точно так же, как и естественные языки поддерживают сложные существительные (например, электростанция
или воздухозаборник), языки программирования также поддерживают создание
более сложных типов данных. Но, как мы вскоре сможем увидеть, даже эти несколько простых типов данных предоставляют нам довольно обширные возможности.
В реальном мире мы также часто присваиваем имена и названия: людям, уникальным местам, предметам и понятиям, чтобы было легче ссылаться на них и обсуждать их. То же самое происходит и в программировании, и точно по этой же причине: присваивая имена переменным, мы можем ссылаться на них и модифицировать
специфичные данные таким образом, который будет понятным для компьютера.
Для примера переведем простое английское предложение в код Python:
The author is Susan E. McGregor
Прочитав это предложение, мы свяжем имя "Susan E. McGregor" с меткой "author".
Впоследствии, если у вас спросят, кто автор этой книги, вы (будем надеяться)
вспомните эту связь и ответите: "Susan E. McGregor". Эквивалентное "предложение" на языке Python показано в листинге 2.2.
Листинг 2.2. Вывод содержимого переменной в языке Python
author = "Susan E. McGregor"
Этот код дает указание компьютеру выделить область памяти, присвоить ей имя
author, а затем сохранить в ней строку "Susan E. McGregor". Если далее в программе
спросить у компьютера о переменной author, он ответит нам, что она содержит
строку "Susan E. McGregor". Соответствующий код приводится в листинге 2.3.
Введение в Python
|
61
Листинг 2.3. Печать содержимого переменной Python
# Создаем переменную с именем author и присваиваем ее содержимому
# значение "Susan E. McGregor"
author = "Susan E. McGregor"
# Проверяем, что компьютер "помнит" содержимое переменной 'author'
print(author)
Важно ли конкретное имя?
В листинге 2.3 я присвоила своей переменной имя author, но это имя не обязательно
должно быть чем-то специфичным. В принципе, переменной можно присвоить любое имя, соблюдая только следующие три ограничения — имя переменной в языке
Python не может:
начинаться с цифры;
содержать какие-либо знаки препинания, за исключением символа подчеркива-
ния (_);
быть "зарезервированным" или ключевым словом, например Number или Boolean.
Например, в листинге 2.3 я могла бы с легкостью назвать переменную nyc_resident
(житель_нью_йорка) или даже fuzzy_pink_bunny (пушистый_розовый_зайчик). Важно лишь то, чтобы программист следовал трем ранее изложенным ограничениям и
чтобы при доступе к содержимому переменной ее имя указывалось точно так же,
как оно было указано при ее создании, учитывая регистр. Например, создадим новый файл Python, вставим в него код из листинга 2.4, а затем исполним его и посмотрим, какие будут результаты исполнения.
Листинге 2.4. Сценарий noun_examples.py
# Создаем переменную с именем nyc_resident и присваиваем ее содержимому значение
# "Susan E. McGregor"
nyc_resident = "Susan E. McGregor"
# Проверяем, что компьютер "помнит" содержимое переменной nyc_resident
print(nyc_resident)
# Создаем переменную с именем fuzzyPinkBunny и присваиваем ее содержимому значение
# "Susan E. McGregor"
fuzzyPinkBunny = "Susan E. McGregor"
# Проверяем, что компьютер "помнит" содержимое переменной fuzzyPinkBunny
print(fuzzyPinkBunny)
62
|
Глава 2
# Но регистр имеет значение!
# Следующая строка кода выдаст сообщение об ошибке
print(fuzzypinkbunny)
Наилучшие практики для именования переменных
Хотя все имена переменных в листинге 2.4 соответствуют всем изложенным ранее
правилам именования переменных, не все они являются особо хорошими. Как мы
увидим на протяжении всей этой книги, подобно любому другому типу писательской деятельности, написание качественного кода состоит не просто в создании
кода, который "работает", но также и в том, чтобы этот код был как можно более
понимаемым и удобным как для компьютеров, так и для людей. Поэтому я считаю
вопрос присвоения переменным качественных имен важным аспектом хорошего
программирования. В частности, подходящими именами переменных будут такие,
которые являются:
описательными;
однозначными (в рамках данного файла или программы);
удобочитаемыми.
Поскольку удовлетворение первого условия часто требует нескольких слов, программисты часто прибегают к использованию одного из двух стилистических методов, чтобы обеспечить удобочитаемость таких имен. Оба эти метода показаны в
листинге 2.4. В первом подходе отдельные слова имени переменной разделяются
символом подчеркивания, например nyc_resident. Во втором подходе используется
так называемый верблюжий регистр (camelСase), когда все слова имени переменной
пишутся слитно, но каждое новое слово, за исключением первого, начинается с заглавной буквы, например fuzzyPinkBunny. В общем, независимо от выбранного вами
стиля, его следует использовать на всем протяжении разрабатываемого проекта,
хотя смешение этих стилей никак не повлияет на работоспособность вашего кода (и
в большой мере на его удобочитаемость также). В этой книге мы будем в основном
использовать подход с подчеркиванием, который также считается более "присущим" языку Python.
Глаголы ≈ функциям
В английском языке глаголы часто описываются как "действия" или "состояния".
В предыдущих примерах мы уже видели эквиваленты глагола в языке программирования: знак равенства (=) и функцию print(). В английском языке, чтобы описать,
чем является что-то, используется глагол "to be" (быть). В языке программирования
Python (а также во многих других языках программирования) значение переменной
указывается после знака равенства. Поэтому знак равенства также называется оператором присваивания.
Введение в Python
|
63
В программировании эквивалентом глаголов являются функции. Язык Python и
многие другие языки программирования содержат встроенные функции, которые
реализуют задачи (например, отображение вывода посредством функции print()),
которые язык "просто знает", как делать. Методы подобны функциям, но предназначены для работы с определенными типами данных, и для правильной работы их
нужно вызывать с переменной конкретного типа данных. Методы для определенного типа данных обычно отражают задачи общего характера, которые мы хотим
выполнять с его помощью. Поэтому, подобно тому, как большинство людей могут
ходить, разговаривать, есть, пить и брать в руки объекты, большинство языков программирования содержат строковые методы, которые могут выполнять такие операции, как соединение двух строк (что на профессиональном жаргоне называется
конкатенацией), разделение строки на составляющие подстроки и тому подобное.
Но поскольку операция "разделения" на части чисел, например числа 5, не имеет
смысла, то для числовых типов данных метода разделения split() не существует.
Какое же практическое различие между встроенными функциями и методами?
Не очень большое, за исключением способа использования этих "глаголов" в
"предложениях" языка Python, которые называются выражениями или инструкциями. В случае встроенных функций мы просто указываем имя функции и передаем
ей требуемые "ингредиенты", называющиеся параметры, заключая их в круглые
скобки. Например, в листинге 1.1 главы 1 мы передавали функции print() строчный
параметр "Hello World!":
print("Hello World!")
Но в случае метода split() нам нужно прикрепить этот метод к конкретной строке.
Эта строка может быть символьной константой (т. е. последовательность символов, заключенная в кавычки) или переменной, содержащей строковое значение.
Попробуйте исполнить код в листинге 2.5 в автономном сценарии или в блокноте,
и посмотрите, какой получится результат.
Листинг 2.5. Сценарий method_madness.py
# Разделяем символьную константу и отображаем результат
split_world = "Hello World!".split()
print(split_world)
# Присваиваем переменной строковое значение,
# а затем отображаем результат вызова для нее метода split()
world_msg = "Hello World!"
print(world_msg.split())
Обратите внимание на то, что если попытаться исполнить метод split() сам по себе
или с типом данных, для которого он не предназначен, то будет возвращено сообщение об ошибке. Попробуйте исполнить операторы в листинге 2.6 по отдельности
(или в двух разных ячейках блокнота) и посмотрите, какие будут результаты.
64
|
Глава 2
Листинг 2.6. Неправильные вызовы метода split()
# Следующий оператор выдаст сообщение об ошибке, поскольку метод split() должен
# вызываться со строковым объектом
split("Hello World!")
# Следующее выражение выдаст сообщение об ошибке, поскольку для
# числового типа нет метода split()
print(5.split())
Подобно типам данных, методы и функции можно распознать по стилю и пунктуации в их представлении. В редакторе кода или блокноте кода Python встроенные
функции (например, print()) отображаются определенным цветом. Например, в используемой по умолчанию в редакторе кода Atom теме One Dark имена переменных отображаются светло-серым цветом, операторы наподобие = (равно) — фиолетовым, а встроенные функции типа print() — бирюзовым. Функции можно также
определить по используемой с ними пунктуации. В частности, любой текст, сразу
же после которого следуют две парные круглые скобки (например, print()), будет
функцией. То же самое относится и к методам, с тем исключением, что они всегда
предшествуются значением соответствующего типа данных или именем переменной, отделенным от него точкой, например world_msg.split().
Язык программирования Python предоставляет довольно обширные возможности,
используя операторы, методы и встроенные функции, особенно если выполнять
такие задачи, как простые математические вычисления. Но для выпаса данных нам
нужны немного более продвинутые возможности. Точно так же, как игра на пианино или в футбол можно рассматривать как "просто" слаженную комбинацию многих более простых действий — например, движение пальцами или ногами — очень
сложные программные функции можно создавать, объединяя тщательно продуманным способом сравнительно простые операторы, методы и встроенные функции.
Вот эти определяемые пользователем функции и начинают придавать нашему коду
настоящую мощь, позволяя создавать "рецепты" кода, которые можно использовать
на многократной основе.
Например, предположим, что нам нужно распечатать одно и то же сообщение для
двух разных людей. Эту задачу можно было бы решить, используя функцию print()
тем же самым способом, как и в предыдущих примерах, как показано в листинге 2.7.
Листинг 2.7. basic_greeting.py
# Создаем переменную с именем author
author = "Susan E. McGregor"
# Создаем другую переменную с именем editor
editor = "Jeff Bleiel"
Введение в Python
|
65
# Используем встроенную функцию print() для вывода
# индивидуализированного сообщения "Hello..." для каждого пользователя
print("Hello "+author)
print("Hello "+editor)
В листинге 2.7 следует обратить внимание на два обстоятельства. Во-первых, использование функции print() справляется с поставленной задачей должным образом, что не может не радовать. По сути, наша первая цель при написании кода для
решения какой-либо задачи (включая текущую) заключается в том, чтобы он выдавал требуемый результат.
Но добившись этого, мы можем начинать думать о том, как каким-либо простым
способом сделать наш код более опрятным и более полезным. Используемые в предыдущем примере два оператора print() абсолютно идентичны, за исключением
передаваемой им переменной. Каждый раз, когда мы видим такое повторение в коде, это индикатор, что его можно заменить собственной определяемой пользователем функцией, как показано в листинге 2.8.
Листинг 2.8. Пользовательская функция greet_me.py
# Создаем функцию, отображающую на экране приветствие по любому переданному ей имени
def greet_me(a_name):
print("Hello "+a_name)
# Создаем переменную с именем author
author = "Susan E. McGregor"
# Создаем другую переменную с именем editor
editor = "Jeff Bleiel"
# Используем нашу пользовательскую функцию greet_me() для вывода
# индивидуализированного сообщения "Hello..." для каждого пользователя
greet_me(author)
greet_me(editor)
С первого взгляда может показаться, что в этом подходе нет ничего особенного,
поскольку в некоторых отношениях главная часть этого кода не особенно отличается от кода предыдущего примера, но в действительности этот пример содержит
много нового. Сейчас мы рассмотрим некоторые из новых понятий, используемых в этом примере. Но если все это не сразу будет вам понятно, то не пережи-
66
|
Глава 2
вайте, поскольку мы будем возвращаться к этим понятиям в дальнейшем материале книги.
Самой главной составляющей кода в листинге 2.8 является наша первая пользовательская функция greet_me(). Для ее создания мы использовали несколько разных
синтаксических структур и стилистических принципов, чтобы сообщить компьютеру, что он должен создать эту функцию и запомнить ее для использования в
дальнейшем. Некоторые из этих принципов, например использование описательного имени функции greet_me(), мы уже видели при именовании переменных в предыдущих примерах. Также мы придерживались правила следования имени функции парными круглыми скобками, как это делается в случае встроенных функций и
методов.
На рис. 2.1 приводится объяснение составляющих нашей пользовательской функции
greet_me(), чтобы пояснить, что происходит в каждой строке кода ее определения.
Рис. 2.1. Компоненты пользовательской функции
Как можно видеть на рис. 2.1, при создании пользовательской функции необходимо
предоставлять несколько указателей для компьютера:
ключевое слово def (сокращение от define — определить) сообщает компьютеру,
что далее следует имя функции;
парные круглые скобки, следующие сразу же за именем функции, подтвержда-
ют, что данное имя является именем функции, и служат для заключения в них
параметров (если таковые имеются);
двоеточие (:) обозначает, что все последующие строки кода с отступом являются
частью функции;
для доступа к переменной, передаваемого функции в качестве аргумента, ис-
пользуется имя локального для данной функции параметра, которое указывается
в парных круглых скобках, следующих за именем функции;
при создании пользовательской функции можно использовать уже существую-
щие функции и методы любого типа, как встроенные, так и пользовательские. В
этом состоит ключевая стратегия для создания эффективного и гибкого кода.
Пользовательские функции используются или вызываются точно так же, как и
встроенные функции. Мы просто указываем имя функции (в данном примере это
Введение в Python
|
67
greet_me()), вставляя в круглые скобки точно такое же количество аргументов, как и
количество параметров, указанное в ее определении. Поскольку при определении
нашей пользовательской функции greet_me() мы указали только один параметр
(т. е., a_name), то при ее вызове ей нужно передавать также только один аргумент. В
противном случае мы получим сообщение об ошибке.
Быстрая прокрутка
Мы рассмотрим вопрос ошибок в коде и сопутствующих сообщений более подробно в разд. "Понимание ошибок" далее в этой главе. Но вы должны смело экспериментировать с кодом в этих примерах, чтобы увидеть, что происходит в случае каких-либо проблем с ним. Экспериментировать с кодом в листинге 2.7 можно следующими способами:
вызвать функцию, не передавая ей никаких параметров, т. е. greet_me();
вызвать функцию, передавая ей избыточные параметры, т. е. greet_me(author,
editor);
вызвать функцию, передавая ей параметр, не являющийся переменной, например
greet_me("Samantha");
вызвать функцию, передавая ей параметр, не являющийся строкой, например
greet_me(14).
Обратите внимание на то, что происходит при исполнении каждой из этих вариаций кода. Если выдается сообщение об ошибке, попробуйте найти связь между содержимым этого сообщения и выполненными вами изменениями в коде.
Тогда как сообщения об ошибках программ не отличаются особым качеством
описания этих ошибок, из них обычно можно почерпнуть по крайней мере часть
полезной информации о причине недовольства компьютера. Мы рассмотрим вопрос поиска и устранения причин проблем с исполнением кода чуть далее в этой
главе, но ваши эксперименты станут хорошим началом в изучении этой области.
Применение пользовательских функций
Как вы, возможно, заметили, мне нравится рассматривать определяемые пользователем или просто пользовательские функции как своего рода программные "рецепты". Подобно кулинарным рецептам они предоставляют компьютеру переиспользуемые инструкции для преобразования одного или больше "ингредиентов" в виде
сырых данных в другой, более полезный продукт. Иногда, как в случае с нашим
"рецептом" greet_me(), такой параметр существует только один, но их может быть и
несколько, причем с разными типами данных. Не существует правильных или неправильных способов создания пользовательских функций, как не существует правильных или неправильных способов создания кулинарных рецептов. У каждого
68
|
Глава 2
будет свой собственный стиль. В то же самое время, что касается стратегии принятия решения о составляющих конкретной пользовательской функции, может быть
полезным помнить о том, как мы обычно используем (или, возможно, даже создаем) рецепты для приготовления кулинарных блюд.
Например, очевидно, что вполне возможно создать один кулинарный рецепт "День
благодарения", описывающий, как приготовить весь ужин для этого праздника с
первого до последнего блюда. В зависимости от вашего стиля отмечания этого
праздника, для "исполнения" этого рецепта может потребоваться от 2 до 72 часов.
Но он будет очень полезным один день в году, без надобности во все остальные
дни. И если бы вы захотели приготовить какое-либо отдельное блюдо из этого рецепта для другого праздника (например, картофельное пюре для Нового года), вам
бы потребовалось просмотреть весь этот рецепт, чтобы найти и извлечь соответствующие ингредиенты и инструкции только для этого блюда. Это означало бы необходимость выполнения большого объема работы, прежде чем приступать к собственно приготовлению этого блюда.
Поэтому, в то время как мы хотим, чтобы наши пользовательские функции выполняли задачи несколько более сложные, чем поддающиеся выполнению встроенными средствами языка, обычно мы не хотим, чтобы они были излишне сложными.
Подобно рецепту "День благодарения", громадные пользовательские функции (и
даже программы) ограничивают наши возможности их эффективного использования. А вот простые, сфокусированные пользовательские функции делают наш код в
общем итоге более полезным и гибким. Мы исследуем процесс создания таких
функций более пристально в главе 8.
Библиотеки: занимаем пользовательские функции
у других программистов
Если пользовательские функции можно рассматривать как программные рецепты,
тогда библиотеки являются программными поваренными книгами: большими
коллекциями пользовательских функций, созданных другими программистами,
которые можно использовать для преобразования наших ингредиентов в виде сырых данных без необходимости придумывать свои собственные "рецепты" с самого начала. Как упоминалось в главе 1, одной из причин, по которым мы вообще
используем язык Python, является наличие большого сообщества программистов,
которые создали и продолжают создавать полезные библиотеки функций на языке
Python. Как мы увидим в разд. "Отправляемся в путь с данными Citi Bike" в конце этой главы, использование этих библиотек предоставляет нам полезные и
мощные возможности.
Но прежде чем приступать к использованию этих возможностей, нам нужно рассмотреть еще две важные грамматические структуры языка Python: циклы и условные операторы.
Введение в Python
|
69
Структуры управления:
циклы и условные операторы
Как уже обсуждалось ранее, написание кода на языке Python во многом похоже на
процесс писания на естественном языке. Кроме использования нескольких основных "частей речи", код на языке Python также пишется слева направо и сверху вниз
и читается, по сути, в таком же порядке. Но путь исполнения кода программы более
похож на книгу под условным названием "Создай свое собственное приключение",
чем на обычный очерк или статью. В частности, в зависимости от предоставленных
программистом команд, некоторые части кода могут пропускаться или исполняться
многократно на основании данных или иных факторов.
Циклы
Одной из самой распространенных целей при выпасе данных будет выполнение
каких-либо одинаковых действий над каждой записью в наборе данных. Например,
предположим, что нам нужно вычислить сумму чисел в списке:
# Создаем список, содержащий количество страниц в каждой главе
# печатной версии данной книги
page_counts = [28, 32, 44, 23, 56, 32, 12, 34, 30]
Выполнить эту задачу, не прибегая к программированию, можно несколькими способами: используя программу калькулятора на компьютере, физический калькулятор или даже (о, ужас!) карандаш и бумагу. Также, если вы знаете, как работать с
электронными таблицами, можно было бы ввести каждое число в отдельную ячейку таблицы, а затем применить функцию SUM() к этим ячейкам. Для небольших
списков любой из этих подходов будет вполне удовлетворительным, но все они
плохо поддаются масштабированию: сложение 10 чисел на бумаге или с калькулятором не займет много времени, чего нельзя сказать о сложении 100 чисел. Подход
с использованием электронной таблицы несколько лучше по времени, но все равно
требует выполнения нескольких дополнительных операций: копирование и вставка
данных в таблицу и ручной выбор ячеек, подлежащих суммированию. Использование программного решения позволит избежать всех этих недостатков. Кроме этого,
независимо от количества суммируемых чисел, будь то 10 или 10 миллионов, создание соответствующей программы займет одинаковое время, а ее исполнение в
последнем случае будет лишь незначительно более длительным.
Поскольку процесс написания компьютерной программы, конечно же, относится к
более общему письменному процессу, инструкции компьютеру для решения поставленной задачи можно изложить разными способами. Один из этих способов —
считывать каждое число в списке и добавлять его в промежуточную сумму, как показано в листинге 2.9.
70
|
Глава 2
Листинг 2.9. Сценарий page_count_loop.py
# Список значений количества страниц в каждой главе книги
page_counts = [28, 32, 44, 23, 56, 32, 12, 34, 30]
# Переменная для отслеживания общего количества страниц; начальное значение равно 0
total_pages = 0
# Для каждого элемента списка выполняем определенную операцию
for a_number in page_counts:
# В данном случае добавляем текущее число к значению переменной total_pages
total_pages = total_pages + a_number
print(total_pages)
Прежде чем рассматривать другие способы программного решения этой задачи,
рассмотрим по частям программу в листинге 2.9. Очевидно, что мы начинаем ее со
списка чисел. Далее мы создаем переменную total_pages для хранения значения
общего количества страниц. Этой переменной мы присваиваем начальное значение,
равное 0 (большинство программ калькулятора выполняют эту операцию болееменее косвенным образом). Наконец, мы начинаем перебор значений в списке:
for a_number in page_counts:
Для меня лично эту строку кода легче всего понять, проговорив ее вслух, как обычное предложение: "Для каждого элемента a_number в списке page_counts выполняем
следующее". В действительности, именно это и происходит. В частности, для каждого элемента списка page_counts компьютер выполняет инструкции, содержащиеся
в блоке кода внизу под оператором for…in…:, выделенным отступами. В данном случае это суммирование текущего значения переменной total_pages и значения переменной a_number, сохраняя полученный результат в этой же переменной total_pages.
Все это довольно прямолинейно: мы уже предоставили компьютеру явным образом
значение как переменной page_counts (это наш список значений), так и переменной
total_pages. Но как насчет значения переменной a_number? Откуда оно берется и как
компьютер знает, где его найти?
Подобно оператору print() или конструкции def...имя_функции():, формат for…in…:
встроен в язык Python, и по этой причине при кодировании нам не нужно предоставлять ему много инструкций. В данном случае нам нужно предоставить оператору for…in…: всего лишь две вещи: переменную типа список (т. е. page_counts) и имя,
которое компьютер может использовать для обращения к текущему элементу списка (т. е. a_number). Назначение составляющих этого оператора показано на рис. 2.2.
В выборе имени переменной a_number, как и с именами переменных в целом, не было ничего особенного, просто оно мне показалось достаточно описательным и удобочитаемым. Важно лишь то, чтобы имя этой переменной в блоке кода (с отступом
Введение в Python
|
71
внизу под оператором) было точно таким же, как и имя соответствующей переменной в самом операторе.
\
Рис. 2.2. Структура цикла for
Быстрая прокрутка
Вот вам еще одна возможность поэкспериментировать с ошибками в коде. Модифицируйте код в листинге 2.9 следующим образом:
1. Измените имя переменной a_number только в операторе for...in:.
2. Измените имя переменной a_number как в операторе for...in:, так и в коде с
отступом внизу под этим оператором (т. е. новое имя переменной должно
быть одинаковым в обоих случаях).
Посмотрите, что происходит при попытке исполнить модифицированный таким
образом код. Если при исполнении второй модификации продолжает выводиться
сообщение об ошибке, проверьте, чтобы имена переменных были абсолютно
идентичны в обоих местах. Самый надежный способ добиться этого — использование копирования и вставки.
На профессиональном языке программистов конструкция for...in...: называется
циклом (loop) и присутствует в том или ином виде во всех широко применяемых
языках программирования. Циклом она называется по той причине, что для каждого элемента списка каждая релевантная строка кода — в случае языка Python это
все строки в блоке кода с отступом внизу под оператором for…in…: — исполняется в
повторяющемся цикле. Это немного трудно увидеть в данном примере, поскольку
наш цикл содержит только одну строку кода, поэтому давайте добавим еще несколько строк, чтобы лучше проиллюстрировать происходящее. Соответствующий
код приводится в листинге 2.10.
Листинг 2.10. Сценарий page_count_printout.py
# Список значений количества страниц в каждой главе книги
page_counts = [28, 32, 44, 23, 56, 32, 12, 34, 30]
72
|
Глава 2
# Переменная для отслеживания общего количества страниц; начальное значение равно 0
total_pages = 0
# Для каждого элемента списка выполняем определенную операцию
for a_number in page_counts:
print("Top of loop!")
# Начало цикла
print("The current item is:")
# Текущий элемент списка:
print(a_number)
total_pages = total_pages + a_number
print("The running total is:")
# Промежуточная сумма
print(total_pages)
print("Bottom of loop!")
# Конец цикла
print(total_pages)
Вы можете подумать, что для простого суммирования списка значений мы проделали слишком много работы. Конечно же, данную задачу можно решить более эффективным способом. В частности, язык Python содержит встроенную функцию
sum(), которая принимает в качестве аргумента список значений, подлежащих суммированию, и возвращает их сумму. В листинге 2.11 приводится пример использования этой функции.
Листинг 2.11. Пример использования функции sum()
# Список значений количества страниц в каждой главе книги
page_counts = [28, 32, 44, 23, 56, 32, 12, 34, 30]
# Отображаем результат исполнения функции sum()
print(sum(page_counts))
Попробуйте самостоятельно добавить этот код в свою программу.
Хотя мы могли бы использовать функцию sum() с самого начала, по разным причинам я этого не сделала, это дало возможность познакомить вас с циклом for. Вопервых, чтобы напомнить вам, что даже простые задачи программирования можно
решить более чем одним способом. Во-вторых, операторы цикла являются жизненно важной частью процесса выпаса данных (и, по сути, всего программирования), а
цикл for...in...: является одним из ключевых инструментов, который мы будем
использовать для фильтрации, оценки и переформатирования данных.
Условные операторы
Цикл for предоставляет нам простой способ обращения к каждому элементу набора
данных, но выпас данных также требует принятия решений о том, что и как делать
Введение в Python
|
73
с данными. Обычно это означает, что мы оцениваем некоторый аспект данных и
для определенного значения выполняем одно действие, а для других значений —
другое, или вообще не выполняем никаких действий. Например, предположим, что
мы хотим узнать, сколько глав в этой книге содержат более 30 страниц, а сколько
меньше 30. Для решения этой задачи нам нужно:
1. Проверить, больше ли чем 30 значение определенного элемента в списке
page_counts.
2. Если да, то инкрементируем на 1 значение переменной счетчика over_30.
3. В противном случае инкрементируем на 1 значение переменной счетчика under_30.
К счастью, язык Python содержит встроенную грамматическую структуру для выполнения точно такой оценки и принятия решения: условный оператор if...else.
Рассмотрим, как он работает. Для этого в листинге 2.11 модифицируем цикл for,
чтобы так же проверять, больше или меньше значения 30 значение переменной количества страниц главы a_number. Модифицированный код приводится в листинге 2.12.
Листинг 2.12. Сценарий page_count_conditional.py
# Список значений количества страниц в каждой главе книги
page_counts = [28, 32, 44, 23, 56, 32, 12, 34, 30]
# Создаем переменные для хранения следующих значений:
# Общего количества страниц в книге
total_pages = 0
# Количества глав, содержащих более 30 страниц
under_30 = 0
# Количества глав, содержащих менее 30 страниц
over_30 = 0
# Для каждого элемента в списке page_counts
for a_number in page_counts:
# Добавляем значение количества страниц текущей главы
# a_number к общему количеству страниц total_pages
total_pages = total_pages + a_number
# Проверяем, содержит ли текущая глава больше 30 страниц
if a_number > 30:
# Если да, то инкрементируем на 1 значение переменной счетчика over_30.
over_30 = over_30 + 1
# В противном случае...
else:
# ...инкрементируем на 1 значение переменной счетчика under_30.
under_30 = under_30 + 1
74
|
Глава 2
# Отображаем полученные результаты
print(total_pages)
print("Number of chapters over 30 pages:")
# Количество глав, содержащих свыше 30 страниц
print(over_30)
print("Number of chapters under 30 pages:")
# Количество глав, содержащих менее 30 страниц:
print(under_30)
Как и в случае с циклом for, я считаю, что легче всего понимать происходящее в
условном операторе if...else, озвучивая его вслух как предложение (рекомендую
записать это предложение как комментарий в соответствующем месте кода): "Если
(if) количество страниц текущей главы более 30, то инкрементируем на 1 значение
счетчика глав, содержащих свыше 30 страниц over_30. В противном случае (else)
инкрементируем на 1 значение счетчика глав, содержащих менее 30 страниц
under_30".
Я надеюсь, что такой подход позволит вам получить некоторое представление о
происходящем на интуитивном уровне. Тем не менее я хочу немного задержаться и
рассмотреть процесс в условном операторе if...else более подробно, поскольку мы
будем постоянно прибегать к его использованию.
Прежде всего взглянем на блок кода, выделенного отступом на одну табуляцию от
левого поля. Весь этот код входит в состав цикла for. Выделение этого кода отступом говорит компьютеру, что он находится "внутри" цикла for. Подобным образом,
код, входящий в состав каждой части условного оператора if...else, выделяется
отступом на одну или больше табуляций. В языке Python этот подход с выделением
частей кода отступами используется не только для придания коду опрятного вида.
Такое нарастающее выделение отступами требуется, чтобы код работал должным
образом. Неправильные отступы вызывают сообщения об ошибке2. Такой механизм
часто называется вложением (nesting). На рис. 2.3 приводится графическая иллюстрация этого процесса.
Вложение кода имеет несколько последствий, которые мы рассмотрим далее в этой
книге. Но на данном этапе главное, что нужно знать об этой практике, — это то,
что для того, чтобы строка кода "принадлежала" функции, циклу или условному
оператору, ее нужно сместить на одну табуляцию вправо от структуры, частью которой мы хотим ее сделать. Чтобы продемонстрировать это на практике, сведем
все, что мы сделали до сих пор, в одну программу Python, содержащую цикл, условный оператор и пользовательскую функцию. Соответствующий код приводится
в листинге 2.13.
2
Во многих других языках программирования блоки кода, входящие в состав циклов или условных
операторов, выделяются фигурными скобками. Но, как упоминалось в разд. "Удобочитаемость"
главы 1, язык Python для этой цели полагается на отступы и табуляцию.
Введение в Python
Рис. 2.3. Вложение кода
Листинг 2.13. Сценарий page_count_custom_function.py
# Список значений количества страниц в каждой главе книги
page_counts = [28, 32, 44, 23, 56, 32, 12, 34, 30]
# Определяем новую функцию count_pages(), которая принимает один аргумент:
# список чисел
def count_pages(page_count_list):
# Создаем переменные для хранения следующих значений:
# Общего количества страниц в книге
total_pages = 0
# Количества глав, содержащих более 30 страниц
under_30 = 0
# Количества глав, содержащих менее 30 страниц
over_30 = 0
# Для каждого элемента в списке page_count_list
for a_number in page_count_list:
# Добавляем значение количества страниц текущей главы a_number к общему
# количеству страниц total_pages
total_pages = total_pages + a_number
# Проверяем, содержит ли текущая глава больше 30 страниц
if a_number > 30:
# Если да, то инкрементируем на 1 значение переменной счетчика over_30
over_30 = over_30 + 1
|
75
76
|
Глава 2
# В противном случае
else:
# инкрементируем на 1 значение переменной счетчика under_30.
under_30 = under_30 + 1
# Отображаем полученные результаты
print(total_pages)
print("Number of chapters over 30 pages:")
# Количество глав, содержащих свыше 30 страниц:
print(over_30)
print("Number of chapters under 30 pages:")
# Количество глав, содержащих менее 30 страниц
print(under_30)
# Исполните этот "рецепт", вызвав его функцию, передав ей в качестве параметра
# настоящий список значений количества страниц в каждой главе
count_pages(page_counts)
Мы можем заключить наш существующий код в оболочку новой функции (которая здесь называется count_pages()), добавив строку определения функции и сместив код на одну табуляцию вправо.
Имя переменной списка значений для использования в цикле должно быть таким
же, как и имя переменной, указанное в круглых скобках в определении функции в
строке .
Функция не делает ничего до тех пор, пока не будет вызвана, т.е. исполнена. На
данном этапе нам нужно передать ей специфичный аргумент, обработку которого
мы хотим выполнять посредством этой функции.
Сравнив код в листинге 2.13 и листинге 2.12, можно увидеть, что первый код — это
тот же второй код, в который были внесены следующие три модификации:
1. Добавлен оператор определения функции def count_pages(page_count_list):.
2. Весь существующий код смещен вправо на одну дополнительную табуляцию,
сообщая таким образом компьютеру, что он принадлежит новой функции
count_pages(). В редакторе кода Atom этот отступ можно выполнить одновременно для всех требуемых строк кода, выделив соответствующие строки и нажав клавишу <Tab>. Модифицировано имя переменной списка значений, указанное в операторе for цикла, чтобы совпадало с именем переменной, указанным
в круглых скобках в определении функции.
3. В конце выполняется вызов данной функции, передавая ей в качестве аргумента
переменную page_counts. Обратите внимание на отсутствие какого бы то ни было
отступа перед оператором вызова функции count_pages(page_counts).
Введение в Python
|
77
Будем надеяться, что у вас понемногу начинает вырисовываться общая картина
выполняемого процесса при использовании всех этих грамматических структур. Но
прежде чем приступать к использованию всех этих средств для выпаса данных для
реальных ситуаций, нам нужно уделить некоторое время рассмотрению, что происходит, когда в ходе исполнения кода возникает ошибка.
Понимание ошибок
Как упоминалось в главе 1, компьютеры хорошо справляются с повторяющимися
задачами, выполняя их быстро и точно. Это позволяет нам создавать программы,
хорошо поддающиеся масштабированию. Иными словами, код для суммирования
или сортировки списка в 10 элементов (как в случае с нашим списком page_counts)
можно использовать и для списка, содержащего, скажем, 10 тысяч элементов.
Но в то же самое время компьютеры по-настоящему, существенно и неисправимо
глупы. Компьютеры не могут делать никаких заключений или создавать инновации; они могут лишь выбирать тот или иной путь исполнения кода на основе инструкций и данных, предоставляемых для них людьми. В результате процесс написания кода во многом похож на дачу указаний 2-3-летнему ребенку: они должны
быть в высшей степени буквальными и очень ясными, потому что в случае возникновения какой-либо ситуации, не упомянутой в этих инструкциях, можно ожидать
любых непредвиденных результатов их исполнения3.
Например, люди часто даже не замечают орфографические или грамматические
ошибки в письменном тексте, поскольку в большинстве случаев мы практически
автоматически сделаем умозаключение о соответствующем значении на основе
контекста предложения или абзаца. Более того, даже в случае перестановки букв во
всех словах предложения мы, как вы сейчас убедитесь, обынчо сомжем протичать
его без осбо бошьлих уислий4. В противоположность этому, если в вашем коде хотя
бы одна запятая будет не там, где она должна быть, то компьютер откажется исполнять весь код, выдав соответствующее сообщение.
Поэтому ошибки при написании кода не просто неизбежны, они ожидаемы. Независимо от вашего опыта программирования, любой фрагмент вашего кода длиной
больше, чем несколько строк, может содержать ошибки того или иного типа. Поэтому, вместо того, чтобы думать о том, как избежать ошибок в своем коде, будет
намного полезнее научиться, как интерпретировать сообщения об ошибках и исправлять их. В этой книге я буду иногда намеренно делать ошибки в коде (или рекомендовать, чтобы вы сами делали их, как я это делаю в этой главе во врезках
"Быстрая прокрутка"), чтобы они стали для вас привычными, и вы могли начать
разрабатывать свой собственный процесс поиска их причин и последующего уст3
Конечно же, в отличие от компьютеров, малыши способны проявлять настоящее оригинальное
мышление и способны к самостоятельному обучению.
4
Дополнительную информацию на эту тему можно найти в посте Wordyard (http://wordyard.com/
003/09/15/if-u-cn-rd-ths-msg-u-r-jst-lke-vryne-lse) блога Scott Rosenberg.
78
|
Глава 2
ранения. Для начала мы рассмотрим три главных типа ошибок при программировании: синтаксические ошибки, ошибки времени исполнения и логические ошибки.
Синтаксические ошибки
Грамматические или синтаксические ошибки программирования могут быть одновременно самыми простыми и при этом самыми изводящими ошибками, отчасти
потому, что они происходят очень часто и компьютер постоянно закатывает скандалы из-за них. Упоминаемая ранее неправильно поставленная запятая является
примером синтаксической ошибки: так или иначе, в вашем коде было допущено
нарушение грамматических правил языка программирования.
Я называю такие ошибки простыми потому, что они почти всегда являются таковыми. Исходя из моего опыта, большинство синтаксических ошибок (и в более широком смысле, ошибки программирования) являются обычными опечатками: пропущенная запятая или кавычка или избыточный или недостаточный отступ строки
кода. К сожалению, многие современные программисты, с которыми мне приходилось работать, находят такие ошибки особенно изводящими, как раз из-за их простоты, и потому, что они чувствуют себя дураками из-за того, что допустили их.
По правде говоря, опытные программисты допускают синтаксические ошибки все
время. Самое главное, чему начинающие программисты могут научиться на своих
синтаксических ошибках, — это как не допускать, чтобы они сбивали их с курса.
Более того, одной из причин, по которой я включила ранее в эту главу врезки "Быстрая прокрутка", в которых даются указания, как преднамеренно внести ошибки
в свой код, является желание продемонстрировать, насколько легко можно допустить ошибку в коде, а также насколько легко ее можно исправить. В некоторых
отношениях одним из самых ценных навыков, которым вы научитесь в процессе
программирования, — это как допускать множество ошибок, но не дать этому
обескуражить себя.
Программирование как игра
Чтобы программные ошибки не вызывали особо большого расстройства, особенно на начальных стадиях обучения, к процессу программирования можно
попробовать подходить как к своего рода видеоигре, где для прохождения
трудного уровня часто требуются многократные (как минимум, десятки) попытки. Иногда повторная попытка обуславливается по-настоящему трудным
противником (или задачей), но иногда вы просто совершаете ошибку и попадаете в ловушку (или просто забываете поставить запятую). В каждом из этих
случаев ключевой подход к успешному решению — пробовать снова, но только до определенной степени. Тогда как определенно не стоит отказываться от
попытки решить программную задачу при первой же ошибке, в многочасовом
сеансе программирования, несомненно, можно перегореть точно так же, как и
при попытках пройти трудный уровень видеоигры.
Введение в Python
|
79
Когда такое происходит, то возьмите перерыв по крайней мере на 20-30 минут,
прежде чем снова пытаться решить задачу. Со временем у вас начнет вырабатываться чувство, когда нужно сделать такой перерыв. Это еще один навык
программирования, который, в конечном счете, будет для вас бесценным.
При возникновении синтаксической ошибки мы будем знать, что это именно синтаксическая ошибка, поскольку в последней строке (обычно многострочного) сообщения об ошибке будет указано SyntaxError: описание ошибки. Но более полезной
для устранения ошибки будет та часть сообщения об ошибке, в которой указывается файл, содержащий ошибку, и строка кода этого файла, в которой она находится.
Со временем просто посмотреть на эту строку кода и поискать в ней возможные
проблемы (обычно это пропущенные знаки пунктуации: запятая, скобка, точка с
запятой, кавычки) будет достаточным для вас, чтобы определить причину ошибки.
Обычно сообщение об ошибке содержит проблемную (предположительно) строку
кода, в которой возможный пропущенный символ обозначается знаком вставки (^).
Но иногда бывают случаи, когда место проблемы указывается неверно. Рассмотрим, например, код в листинге 2.14, где в одной строке данных типа dict пропущена запятая.
Листинг 2.14. Код с преднамеренной ошибкой
1
2
3
4
5
6
# Хотя ошибка в действительности находится в строке 4 (пропущена запятая),
# в сообщении об ошибке указывается строка 5
book = {"title":"Practical Python for Data Wrangling and Data Quality",
"format": "book"
"author": "Susan E. McGregor"
}
Само сообщение об ошибке выглядит следующим образом:
File "ObjectError.py", line 5
"author": "Susan E. McGregor"
^
SyntaxError: invalid syntax
Как видите, хотя в коде в листинге 2.14 запятая пропущена в строке 4 после значения "book", в сообщение об ошибке указывается, что ошибка находится в строке 5.
Это объясняется тем, что транслятор понял, что имеется проблема, только в этой
точке. Но, как правило, синтаксические ошибки будут находиться в строке, указанной транслятором (или в строке под ней).
Ошибки времени исполнения
В категорию ошибок времени исполнения входят проблемы любого типа, которые
возникают в процессе исполнения кода. Подобно синтаксическим ошибкам, боль-
80
|
Глава 2
шая часть ошибок времени исполнения, по сути, те же опечатки, такие как, например, неправильно указанные имена переменных. В частности, когда сообщение об
ошибке содержит фразу типа: некая_переменная не была определена (variable was not
defined), можно быть почти уверенным, что ваш код содержит неправильно написанную переменную. Следует иметь в виду, что лишние или недостающие пробелы
также учитываются. Изучение всего сообщения об ошибке, чтобы отследить ее источник может быть несколько затруднительным, поскольку такие сообщения обычно ссылаются на подробности внутренней работы языка Python в объеме, который я
лично не нахожу особо полезным. Поэтому я рекомендую скопировать имя проблемной переменной непосредственно из сообщения об ошибке, а затем выполнить
поиск этой переменной без учета регистра в исходном коде. (Редактор кода Atom
осуществляет такой поиск по умолчанию.) В результате будут выделены все подобные (но не идентичные) варианты написания данной переменной, что ускорит
поиск несовпадающего имени.
Например, в листинге 2.15 имя переменной в определении функции greet_me(a_name)
не совпадает с именем переменной в теле определения.
Листинг 2.15. Несовпадающие имена переменной вызовут ошибку времени исполнения
# Создаем функцию, отображающую на экране приветствие по любому переданному ей имени
def greet_me(a_name):
print("Hello "+A_name)
# Создаем переменную с именем author
author = "Susan E. McGregor"
# Передаем функции greet_me() аргумент author
greet_me(author)
Поскольку имя параметра в круглых скобках в определении функции всегда превалирует над именами в остальных местах, исполнение кода в листинге 2.15 создает
сообщение об ошибке, показанное в листинге 2.16.
Листинг 2.16. Сообщение об ошибке времени исполнения,
вызванное несовпадающими именами переменной
File "greet_me_parameter_mismatch.py", line 10, in <module>
greet_me(author)
File "greet_me_parameter_mismatch.py", line 4, in greet_me
print("Hello "+A_name)
NameError: global name 'A_name' is not defined
Обратите внимание на то, что как обычно бывает, последние несколько строк сообщения содержат наиболее полезную информацию. В частности, в последней
строке сообщения говорится, что мы пытаемся использовать имя переменной
Введение в Python
|
81
A_name, предварительно не объявив его, а предшествующая строка содержит сам
код, где находится эта переменная. Имея эти два элемента информации плюс нашу
стратегию поиска, определить причину данного сообщения об ошибке, скорее всего, не займет слишком много времени.
Другой очень распространенный тип ошибки времени исполнения происходит при
попытке выполнить с определенным типом данных операцию, которую он не поддерживает. Во врезке "Быстрая прокрутка" в конце разд. "Глаголы ≈ функциям"
было предложено исполнить заведомо неправильную строку кода greet_me(14). Попытка исполнения этого кода вызвала бы сообщение об ошибке, содержащее слово
TypeError, означающее, что какая-то часть нашего кода обратилась к типу данных,
не соответствующему ожидаемому. В данном случае проблема состоит в том, что
функция greet_me() ожидает в качестве параметра строковое значение, но ей было
передано числовое значение 14.
Трудность с ошибками этого типа состоит в сложности определения точного местонахождения причины ошибки вследствие разнесенности мест в коде, где переменной присваивается значение и где эта переменная (и, следовательно, ее значение) используется. Эти два процесса могут находиться в коде на значительном расстоянии друг от друга, особенно с повышением уровня сложности разрабатываемой
программы. Например, в сообщении об ошибке в листинге 2.16, вызванной ошибкой в коде в листинге 2.15, указываются два места в коде. Первым является строка,
в которой переменная передается функции, и, следовательно, в которой ей присваивается значение:
File "greet_me_parameter_mismatch.py", line 10, in <module>
А второе — строка кода, в которой это значение используется:
File "greet_me_parameter_mismatch.py", line 4, in greet_me
Как уже упоминалось ранее, для определения причины проблемы будет полезным
начать процесс ее поиска с места использования проблемной переменной или значения, а затем, перемещаясь по коду, пытаться найти строку кода, где этой переменной присваивается ее значение. Но при этом следует иметь в виду, что сообщение
об ошибке не обязательно может содержать такую строку. Это часть тех обстоятельств, которые делают задачу определения причин ошибок времени исполнения
такого типа более трудной, чем для обычных синтаксических ошибок.
То, что ошибки времени исполнения являются одними из самых трудных для диагностирования, и есть ключевая причина, почему я рекомендую часто и регулярно
тестировать и сохранять разрабатываемый код. Причину новой ошибки времени
исполнения намного легче определить, если после последнего тестирования было
добавлено небольшое количество строк нового кода, поскольку проблема должна
находиться в этих нескольких строках. Использование подхода типа "пишем, исполняем, повторяем" значительно сокращает область поиска причин новых ошибок, что, в свою очередь, позволит их обнаружить сравнительно быстро.
82
|
Глава 2
За исключением…
Одна из реальностей выпаса данных — нашему коду часто приходится сталкиваться с неподходящими или непредсказуемыми данными, поскольку они могут содержать неправильные или вообще отсутствующие значения. Например,
некоторые ячейки столбца числовых значений могут быть пустыми. Как мы
увидим, в частности, в главе 6, данные пустые ячейки не будут автоматически
преобразованы в какое-либо число, например ноль5. Это означает, что пустые
значения могут вызывать ошибки, в случае если ваш код может обрабатывать
только числовые значения.
В языке Python этот тип досадных, но предсказуемых ошибок называется исключением (exception), поскольку они происходят, когда программа сталкивается с исключительной или неожидаемой ситуацией, например, с неподходящим типом данных. В программах по выпасу данных исключения происходят
особенно часто, их можно обеспечить возможностями обработки исключений,
т. е. дать нашему коду указание вместо выдачи сообщения об ошибке и прекращения исполнения делать что-то другое.
И хотя обработка исключений может быть полезной, в рамках этой книги мы не
будем уделять этому вопросу много внимания. Это объясняется тем, что наша
цель здесь состоит в том, чтобы не просто создавать программы, которые могут
работать с определенными наборами данных, но и обеспечивать качество этих
данных. В результате бóльшую часть времени мы будем пытаться узнать, когда и
где наши данные не соответствуют нашим ожиданиям, а не заниматься написанием кода, чтобы пройти мимо этих проблем, не останавливаясь. Поэтому будет более разумным решать вопрос с ошибками времени исполнения, когда они возникают, а не пытаться предвидеть все возможные ошибки (и обрабатывать их)6.
Конечно же, можно написать обработчики исключений, которые помогут нам
конструктивно оценить качество данных. Но в большинстве случаев нам просто не нужно будет делать это напрямую, поскольку библиотеки функций языка Python, которые мы будем использовать (опять же, особенно в главе 6), будут выполнять бóльшую часть этой работы для нас. Мы также минимизируем
вероятность необходимости использования обработчиков исключений, используя инкрементальный подход к написанию наших программ по выпасу
данных. В частности, мы будем начинать разработку и тестирование нашего
кода с подмножеством целевого набора данных, а затем будем исполнять полученную программу с полным набором данных (или его версиями), чтобы
оценить ее работу. Это поможет нам сбалансировать скорость и масштаб, позволяя быстро оценить качество данного набора данных, и в то же самое время
создавать код, способный работать с полным набором данных, если все пойдет
хорошо.
5
Но, как мы увидим в главе 3, это в действительности полезное поведение.
Информация по наилучшим практикам по покрытию кода предоставляется в статье Code Coverage
Best Practices в блоге Google Testing Blog (https://testing.googleblog.com/2020/08/code-coverage-bestpractices.html).
6
Введение в Python
|
83
Логические ошибки
Самыми сложными программными ошибками являются логические ошибки, под
которыми подразумеваются ситуации, когда программа в целом работает, но только не так, как планировалось. Эти ошибки особо коварны, поскольку с точки зрения
транслятора с ними все в порядке, т. е. ваш код не содержит никаких инструкций,
которые были бы для него непонятными или неправильными. Но не забывайте, что
компьютеры глупы, и поэтому они в блаженном неведении позволят вашей программе выполнять инструкции, создающие бессмысленные, абсурдные и даже вводящие в заблуждение результаты. К примеру, можно выполнять правильные математические вычисления с некачественными данными (как рассматривается в главе 3)
или можно создать полностью функциональный код, делающий что-либо неуместное или неправильное. Более того, мы уже создавали такой код!
В частности, в листинге 2.12 можно увидеть, что наши комментарии с описанием
желаемых действий кода не совсем соответствуют действиям, которые этот код
выполняет в действительности. Результаты исполнения этого кода выглядят следующим образом:
291
Number of chapters over 30 pages:
(Количество глав, содержащих свыше 30 страниц)
5
Number of chapters under 30 pages:
(Количество глав, содержащих менее 30 страниц)
4
Но наши входные данные выглядят так:
page_counts = [28, 32, 44, 23, 56, 32, 12, 34, 30]
Видите проблему? Тогда как у нас действительно есть пять глав, содержащих свыше 30 страниц, количество глав, содержащих менее 30 страниц, на самом деле равно трем, поскольку одна глава имеет ровно 30 страниц.
На данном этапе это может казаться не особенно значительной ошибкой, ведь, в
конце концов, насколько важными будут последствия, если одна глава с 30 страницами будет отнесена к главам, содержащим менее 30 страниц? Но предположим,
что вместо подсчета глав этот код использовался для определения права на участие
в голосовании. Если бы этот код подсчитывал только избирателей старше 18 лет, то
сотни тысяч людей были бы лишены избирательских прав.
Исправить эту логическую ошибку не представляет ни больших трудностей, ни
сложностей в аспекте кода, который нужно подкорректировать. Нам нужно лишь
заменить этот код:
# Проверяем, содержит ли текущая глава больше 30 страниц
if a_number > 30:
84
|
Глава 2
следующим кодом:
# Проверяем, содержит ли текущая глава 30 или больше страниц
if a_number >= 30:
Вызов, брошенный ошибками данного типа, состоит в том, что их недопущение
полагается полностью на ваше усердие как программиста, чтобы в процессе разработки программы не пропустить мимо своего внимания какое-либо возможное значение или несоответствие данных, которое может приводить к неправильным измерениям или результатам. Поскольку транслятор не может сообщить нам о наличии
логических ошибок в разрабатываемой программе, то единственный надежный
способ не допустить их — изначально тщательно планировать свои программы, а
также тщательно выполнять проверку корректности выдаваемых ими результатов.
В разд. "Отправляемся в путь с данными Citi Bike" в конце этой главы мы увидим,
как выглядит этот процесс, когда будем заниматься разработкой программы для
работы с набором реальных данных.
Не забываем о Git!
Наличие ошибок в разрабатываемом коде может досадно срывать ваши планы,
но их отрицательное воздействие можно уменьшить, если добиться, чтобы новые ошибки не сводили на нет достигнутый ранее прогресс в достижении работоспособности кода. Именно поэтому я рекомендую, чтобы всякий раз, когда у вас есть новый фрагмент кода, работающий должным образом, его нужно
было не только сохранить, но также выполнить для него коммит в своем репозитории Git.
Это не должно быть сложным процессом ни при работе с блокнотами, ни при
использовании автономных сценариев. Если вы работаете в папке
data_wrangling, которую мы создали в разд. "Сводим все в одно целое" главы 1,
тогда ваш репозиторий уже создан и подключен к GitHub. Вам нужно только
добавить в локальный репозиторий все новые файлы (команда add), выполнить
для них коммит с сообщениями (команда commit), а затем сохранить их на
GitHub (команда push). Это хоть и выглядит сложно, но в действительности
требует выполнения всего лишь нескольких команд в терминале, которые после нескольких таких операций станут для вас обыденной процедурой.
Лично я всегда предпочитаю начинать любой процесс коммита проверкой статуса своего репозитория, чтобы знать, какие файлы нужно добавить, а для каких нужно только выполнить коммит. В результате мой типичный процесс сохранения своей работы в системе Git выглядит следующим образом:
git status
git add имя_файла
git commit -m "Описательное сообщение для коммита!" имя_файла
git push
Введение в Python
|
85
Вот и все! Конечно же, операцию push не обязательно выполнять для каждого
коммита, поскольку все выполненные ранее коммиты можно сохранить на
GitHub одновременно одной командой push. В любом случае, регулярная проверка разрабатываемого кода таким образом означает, что если что-то пойдет
не так или если вам нужно взять перерыв, у вас всегда будет рабочий код, чтобы возобновить работу.
Теперь, когда мы рассмотрели все основы программирования на языке Python,
можно переходить от работы с "игрушечными" примерами с использованием
данных на основе этой книги к работе с реальными данными. Чтоб получить
первое представление о выпасе данных с использованием реального набора
данных, воспользуемся информацией, полученной из системы общего пользования велосипедами города Нью-Йорк, называющейся Citi Bike.
Отправляемся в путь с данными Citi Bike
Каждый месяц миллионы людей по всему миру перемещаются по городам и другим
населенным пунктам на велосипедах, получаемых при помощи систем совместного
пользования велосипедами (также называются велопрокат или велошеринг). Программа Citi Bike города Нью-Йорк была запущена в 2013 году с шестью тысячами
велосипедов, а в 2020 году отметила свою стомиллионную поездку.
Программа Citi Bike предоставляет в свободном доступе как архивные, так и оперативные данные о работе своей системы. Чтобы получить представление о том, что
нужно для выпаса реальных данных, мы используем данные системы Citi Bike, чтобы получить ответ на простой вопрос, сколько поездок осуществляется каждый
день разными типами пользователей системы.
Чтобы ответить на этот вопрос, нам потребуются наши навыки работы с языком
Python, поскольку каждый день пользователи системы Citi Bike осуществляют сотни тысяч поездок. Это означает, что данные только за один день занимают слишком много строк, чтобы их можно было бы обработать посредством электронной
таблицы Excel или Google Sheets. Но даже на таком маломощном устройстве, как
Chromebook, этот объем данных можно обработать при помощи программы Python
без каких бы то ни было проблем.
Чтобы продумать подход к решению этой задачи, пересмотрим шаги процесса выпаса данных, изложенные в разд. "Что такое выпас данных?" главы 1.
1. Обнаружение или сбор данных.
2. Оценка качества данных.
3. Очистка, стандартизация и/или преобразование данных.
4. Анализ данных.
5. Визуализация данных.
6. Предоставление данных.
86
|
Глава 2
Для этого упражнения мы будем концентрироваться на шагах с 1 по 4, хотя, как мы
увидим, я выполнила некоторую предварительную подготовку, что позволит нам
сократить время работы на определенных шагах. Например, я уже нашла данные
системы Citi Bike (https://citibikenyc.com/system-data) и загрузила архивные данные поездок за сентябрь 2020 г. (https://s3.amazonaws.com/tripdata/index.html),
проверила, что столбец User Type содержит только значения Customer и Subscriber, а
также урезала полный набор данных за сентябрь 2020 г., оставив только данные для
поездок, которые начались 1 сентября 2020 г. Мы рассмотрим выполнение всех
этих процессов в последующих главах, но на данном этапе я хочу сфокусировать
внимание на том, как применять уроки этой главы к данным иным, чем те, которые
мы создали сами7.
Начинаем с создания псевдокода
Один из наилучших способов начать работу над проектом выпаса данных любого
размера — заблаговременно спланировать свой подход к нему и включить эти основные принципы в файл программы Python посредством метода, называющегося
псевдокодом. Псевдокод, по сути, означает пошаговое изложение на обычном русском (или любом другом предпочитаемом вами) языке основных аспектов своей
программы. Кроме предоставления возможности продумать, каких результатов
должна достичь ваша программа, не беспокоясь о том, как все это реализовать в
коде, псевдокод даст вам ценный ориентир, по которому можно будет возобновить
работу над проектом после перерыва. Хотя вам, несомненно, повстречается много
профессиональных программистов, которые не выполняют эту часть процесса на
регулярной основе, я могу вам гарантировать, что это поможет вам завершать работу над своими проектами по выпасу данных более быстро и что привычка этого
типа приветствуется в любой среде профессионального программирования или интеллектуальной обработки данных.
Я предпочитаю вставлять общую обрисовку программы и соответствующий псевдокод вверху файла программы в больших блоках комментариев. Для начала я делаю следующие три вещи:
1. Формулирую мой вопрос.
2. Описываю, как я буду "отвечать" на этот вопрос.
3. Излагаю разговорным языком шаги, которые будет выполнять моя программа.
Это означает, что сначала мне придется написать в файле программы большое количество комментариев, как показано в листинге 2.17.
7
Весь код для этого упражнения находится в файлах hitting_the_road_with_citibike.py и
hitting_the_road_with_citibike.ipynb. Но я настоятельно рекомендую вам создать соответствующие
файлы и ввести в них предоставленный код самостоятельно.
Введение в Python
|
87
Листинг 2.17. Программа hitting_the_road_with_citibike.py
# Вопрос: Сколько поездок делают каждый день клиенты системы Citi Bike
# отдельно по категориям subscribers и customers?
#
#
#
#
Ответ: Выберите один день для исследования.
Набор данных для этого упражнения был сгенерирован с оригинальных данных системы Citi
Bike, находящихся по этому URL-адресу https://s3.amazonaws.com/tripdata/index.html
Имя файла: 202009-citibike-tripdata.csv.zip
#
#
#
#
#
#
#
#
#
#
Обрисовка программы:
1. Считываем файл данных: 202009CitibikeTripdataExample.csv
2. Создаем переменные для подсчета значений subscribers, customers, и other
3. Для каждой строки в файле
a. Если значение "User Type" равно "Subscriber,", инкрементируем на 1 значение
"subscriber_count"
b. Если значение "User Type" равно "Customer,", инкрементируем на 1 значение
"subscriber_count"
c. В противном случае инкрементируем на 1 значение переменной other
4. Отображаем полученные результаты
Выполнив обрисовку программы, можно приступать к реализации ее первой части:
считывания данных.
При каждом создании подобного файла помните, что даже если его сохранить в папке локального репозитория Git, все равно необходимо выполнить команду git add, чтобы система Git сохранила все выполненные
изменения. Не забывайте, что для файла нельзя выполнить коммит (команда git commit), пока он не будет добавлен. Если вам требуется освежить свои знания в этой области, то все эти шаги изложены в разд. "Не
забываем о Git!" ранее в этой главе. Тогда как частота выполнения коммитов предоставляется полностью на ваше усмотрение, для этого упражнения я рекомендую выполнять коммиты (не забывая о сопровождающем
описательном сообщении) после завершения каждого блока кода в этом
разделе. Конечно же, по мере того, как вы чувствуете себя все более уверенными с процессами кодирования и коммитами Git, вы сами определите наиболее подходящие для вас частоту и ритм резервного копирования
своего кода.
Загрузка данных разных форматов может быть одним из наиболее сложных процессов выпаса данных (как мы увидим более подробно в главе 4). К счастью, существует большое количество библиотек функций Python, которые могут помочь нам
с этой задачей, и мы воспользуемся одной из них прямо сейчас. Я уже вкратце упоминала библиотеки в разд. "Библиотеки: занимаем пользовательские функции у
88
|
Глава 2
других программистов" ранее в этой главе; это, по сути, просто сборники кодовых
рецептов. Для этого проекта мы используем кодовые "рецепты" из библиотеки csv,
которая в основном предназначена для работы, как и следовало ожидать, с файлами
с расширением .csv. Расширение файла .csv означает comma-separated values (значения, разделенные запятыми). Если вам раньше не приходилось иметь дело с такими файлами, не волнуйтесь. Мы подробно рассмотрим разные типы файлов в
главе 4. Но в данный момент наличие библиотеки csv означает, что нам нет надобности много знать о файлах этого типа для того, чтобы работать с ними, поскольку
кодовые рецепты библиотеки будут выполнять большую часть работы для нас.
Если вы следуете этому примеру в своем файле, добавьте в него код в листинге 2.18.
Листинг 2.18. Сценарий hitting_the_road_with_citibike.py (продолжение)
# Импортируем библиотеку csv
import csv
# Открываем файл 202009CitibikeTripdataExample.csv в режиме чтения (r)
# Этот файл должен находиться в той же самой папке, что и файл автономного сценария
# или блокнота
source_file = open("202009CitibikeTripdataExample.csv","r")
# Передаем наш исходный файл source_file как аргумент (ингредиент) методу (рецепту)
# DictReader библиотеки csv.
# Сохраняем результат в переменной citibike_reader
citibike_reader = csv.DictReader(source_file)
# Метод DictReader добавил в наши данные некоторую полезную информацию. Например,
# свойство fieldnames позволяет нам обращаться ко всем значениям в первой строке
# (или заголовке)
print(citibike_reader.fieldnames)
Библиотека csv содержит ряд полезных кодовых рецептов для работы с файлами
данных.
Встроенная функция open() принимает в качестве параметров имя файла и режим
работы с ним. В данном примере целевой файл (202009CitibikeTripdataExample.csv)
должен находиться в той же самой папке, что и файл автономного сценария или
блокнота. Значение параметра режима может быть r (read — чтение) или w (write —
запись).
Отображая значения cltlbike_reader.fieldnames, мы можем видеть, что точным
именем столбца User Type (тип пользователя) является usertype.
На данном этапе этот сценарий уже можно исполнять, и он должен выдать результат наподобие показанного в листинге 2.19.
Введение в Python
|
89
Листинг 2.19. Результаты исполнения сценария hitting_the_road_with_citibike.py
в его текущем виде
['tripduration', 'starttime', 'stoptime', 'start station id', 'start station
name', 'start station latitude', 'start station longitude', 'end station id',
'end station name', 'end station latitude', 'end station longitude', 'bikeid',
'usertype', 'birth year', 'gender']
На данном этапе мы успешно завершили первый шаг нашей обрисовки: прочитали
данные, с которыми будем работать. Также мы использовали библиотеку csv для
преобразования этих данных и даже для создания некоторых метаданных. Что важно, теперь мы знаем, что точное имя столбца, содержащего информацию о типе
пользователя User Type, в действительности является usertype. Эта информация будет полезной, когда настанет время создавать условные операторы if...else. Чтобы
проверить, что наш сценарий работает должным образом, сохраните и исполните
его. Если все работает, как и должно (т. е. сценарий выводит на экран список заглавий столбцов), то теперь будет подходящее время выполнить процесс резервного
копирования в систему Git:
git status
git commit -m "Описательное сообщение для коммита" имя_файла_с_расширением
git push
Помните, что при работе в Google Colab резервное копирование можно выполнить
непосредственно на веб-сайте GitHub, выполнив последовательность команд меню
File | Save a copy in GitHub, не забывая при этом ввести соответствующее коммитсообщение в открывшемся всплывающем окне.
Успешно выполнив первый шаг нашего плана, можно переходить к выполнению
второго, как показано в листинге 2.208.
Листинг 2.20. Сценарий hitting_the_road_with_citibike.py (продолжение)
# Создаем переменные для хранения количества пользователей каждого типа системы
# Citi Bike Инициируем каждую переменную, присваивая ей значение 0.
subscriber_count = 0
customer_count = 0
other_user_count = 0
Довольно просто, не так ли? Переходим к третьему шагу. Чтобы проверить каждую
строку данных в файле, нам нужно создать цикл for…in, который должен содержать
условный оператор if...else для проверки наличия определенных значений в
столбце usertype. Чтобы отслеживать, что делает каждая строка кода, я вставила в
8
Добавьте этот код в свой файл под последней строкой предыдущего блока. Делайте это со всеми
последующими блоками кода этого сценария.
90
|
Глава 2
код большое количество комментариев, объясняющих действия этих строк. Соответствующий код приводится в листинге 2.21.
Листинг 2.21. Сценарий hitting_the_road_with_citibike.py (еще одно продолжение)
# Шаг 3: Обрабатываем в цикле каждую строку данных
for a_row in citibike_reader:
# Шаг 3a: Если значение в столбце usertype
# текущей строки равно "Subscriber"
if a_row["usertype"] == "Subscriber":
# инкрементируем на 1 значение переменной subscriber_count
subscriber_count = subscriber_count +1
# Шаг 3b: В противном случае (else), если значение в столбце usertype
# текущей строки равно "Customer"
elif a_row["usertype"] == "Customer":
# инкрементируем на 1 значение переменной customer_count`
customer_count = customer_count + 1
# Шаг 3c: Значение столбца usertype текущей строки не равно ни Subscriber,
# ни Customer. Поэтому инкрементируем на 1 значение общей переменной
# ohter_user_count
else:
other_user_count = other_user_count + 1
Нам необходимо обеспечить, чтобы данные для цикла for были уже преобразованы методом DictReader(), поэтому здесь нужно обращаться к переменной
citibike_reader.
Чтобы строки кода условного оператора if были внутри цикла for, их нужно
сдвинуть вправо на дополнительный отступ.
Поскольку здесь, кроме "в противном случае" (else), нам также требуется и "если" (if), мы используем составное ключевое слово elif, которое является сокращением от else if.
Это последнее else улавливает все строки данных, в которых значение столбца
usertype не равно одному из значений, на которые выполняется явная проверка (в
данном случае это значения Subscriber и Customer). Таким образом реализуется базовая проверка качества данных: если этот столбец данных содержит какие-либо неожидаемые значения, значение переменной other_user_count будет больше нуля (0),
и нам нужно будет исследовать исходные данные более внимательно.
С первого взгляда может показаться, что код в листинге 2.21 выполняет большой
объем работы. Но в действительности в нем выполняется только проверка значения
Введение в Python
|
91
столбца usertype каждой строки данных на равенство значению Subscriber или
Customer и при положительном результате проверки инкрементируется (увеличивается) на 1 значение соответствующей переменной счетчика. Если же значение
usertype не равно ни одному из этих значений, то инкрементируется значение переменной счетчика other_user_count.
Хотя может выглядеть странным, что данный фрагмент кода содержит намного
больше строк комментариев, чем собственно кода, это, в действительности, вполне
нормально и даже хорошо. Потому что компьютер никогда не забудет, как читать
код на языке Python, но вы же с течением времени полностью забудете, что и почему этот код делает. Вот эти комментарии и помогут вам освежить свою память. И в
этом нет ничего плохого! В конце концов, запоминание всего кода сделало бы процесс программирования довольно неэффективным. А излагая письменно подробную информацию о коде в виде комментариев, в будущем мы сможем с легкостью
понять, что он делает, без необходимости перевода операторы языка Python на
обычный язык.
Прежде чем продолжать, в обязательном порядке проверьте правильность своего
кода, исполнив его. Поскольку для большинства наиболее распространенных ошибок отсутствие новостей означает хорошие новости, то при отсутствии сообщений
об ошибках можно выполнить резервное копирование кода в систему Git. В случае
же наличия каких-либо ошибок, конечно же, их нужно устранить. Теперь остался
только один очень простой шаг: вывод результатов на экран. Реализуем его самым
прямолинейным путем, используя встроенный оператор print(), как показано в
листинге 2.22.
Листинг 2.22. Сценарий hitting_the_road_with_citibike.py (последняя часть)
# Шаг 4: Отображаем результаты, не забывая включить заголовки:
print("Number of subscribers:")
# Количество подписчиков
print(subscriber_count)
print("Number of customers:")
# Количество клиентов
print(customer_count)
print("Number of 'other' users:")
# Количество прочих пользователей
print(other_user_count)
Обратите внимание на то, что все эти операторы print() выровнены по левому
краю, потому что мы хотим отобразить значения только после того, как цикл for
обработает весь набор данных.
Добавьте код в листинге 2.22 в свой файл сценария, сохраните его и запустите на
исполнение. Результаты исполнения должны выглядеть наподобие показанных в
листинге 2.23.
92
|
Глава 2
Листинг 2.23. Результаты исполнения полного сценария
hitting_the_road_with_citibike.py
['tripduration', 'starttime', 'stoptime', 'start station id', 'start station
name', 'start station latitude', 'start station longitude', 'end station id',
'end station name', 'end station latitude', 'end station longitude', 'bikeid',
'usertype', 'birth year', 'gender']
Number of subscribers:
58961
Number of customers:
17713
Number of 'other' users:
0
Если ваши результаты похожи на эти, тогда примите наши поздравления. Вы успешно создали свою первую программу для выпаса реальных данных. Не забудьте
выполнить цикл резервного копирования своей работы в системе Git.
Масштабирование
Посредством данного сценария мы решили следующие задачи:
1. Успешно подсчитали количество пользователей типа Subscriber и Customer, которые использовали услуги системы Citi Bike в один день в сентябре 2020 г.
2. Удостоверились, что столбец usertype не содержит никаких других типов значений, кроме этих двух (поскольку значение переменной other_user_count равно 0).
Если ранее вам приходилось выполнять выпас данных, используя только электронные таблицы или программы баз данных, и это первый раз использования для этой
цели средства на языке Python, есть вероятность того, что, учитывая все обстоятельства, этот процесс занял больше времени, чем ваш предыдущий метод. Но, как
я уже упоминала несколько раз ранее, ключевое преимущество подхода с использованием кодирования над многими другими методами состоит в возможности
почти незаметного для пользователя масштабирования полученного решения.
Масштабирование работает двумя способами. Прежде всего, программное решение
обрабатывает крупные наборы данных почти с такой же самой скоростью, как и
наборы данных малого размера. Например, на моем устройстве Chromebook сценарий hitting_the_road_with_citibike.py обрабатывает данные системы Citi Bike за один
день приблизительно за полсекунды. Обработка этим же сценарием данных за весь
сентябрь занимает приблизительно 12 секунд. Большинству прикладных программ,
которым обработка данных за весь месяц была бы хотя бы под силу, скорее всего,
потребовалось бы столько времени только для того, чтобы открыть файл данных,
не говоря уже о какой-либо его обработке. Таким образом, использование подхода
с программным решением на языке Python помогает масштабированию по той причине, что мы можем обрабатывать крупные наборы данных более быстро, нежели
Введение в Python
|
93
посредством прикладных программ. Вы можете сами удостовериться в этом, заменив следующую строку сценария:
source_file = open("202009CitibikeTripdataExample.csv","r")
этой:
source_file = open("202009-citibike-tripdata.csv","r")
Для автономного сценария Python можно даже измерить время обработки этого нового файла данных, добавив ключевое слово time перед командой запуска сценария:
time python _имя_файла_сцерания_.py
Это иллюстрирует второй тип масштабирования, позволяемого программными решениями на языке Python. В частности, после того как мы создали программу для
обработки набора данных определенного размера, для той же самой обработки набора данных бóльшего размера (но с той же самой структурой) требуется лишь незначительное усилие. Вот в этом отношении решения на языке Python (и подход с
использованием программирования в целом) проявляют себя наилучшим образом.
В данном случае для обработки нашим сценарием данных за весь сентябрь 2020 г.
потребовалось лишь загрузить другой файл данных. Изменив имя файла, загружаемого оператором source_file = open(), мы смогли обработать данные за весь сентябрь, а не всего лишь за один его день. Иными словами, дополнительное усилие,
требуемое для обработки сотен тысяч дополнительных строк данных, равно в точности усилию, требуемому, чтобы скопировать и вставить новое имя файла данных.
Но основании этого примера мы могли бы обработать данные за весь год всего
лишь за несколько минут (или меньше, как мы увидим в главе 8). Это почти невозможно реализовать посредством любого метода выпаса данных, не использующего
программирования.
Весь сценарий, который мы создали в этом разделе, представляет собой пример, как,
используя лишь базовые структуры языка, рассмотренные в этой главе, можно организовать по-настоящему полезный и эффективный выпас данных средствами языка
Python. Хотя в последующих главах нас ожидают новые форматы данных и задачи, я
надеюсь, что рассмотренный на данном этапе материал позволил вам получить представление о том, сколько можно сделать посредством даже базовых средств Python
приложив лишь небольшие усилия и внимание к мелочам. Представьте, какие возможности отроются перед вами, если вы продолжите изучать эту тему!
Заключение
Хотите верьте, хотите нет, но в этой главе мы рассмотрели все основные средства
языка Python, которые вам потребуются для работы по выпасу данных, а также для
решения почти любых других задач посредством этого языка. Вкратце повторим,
что мы изучили:
Типы данных
Это "существительные" языка программирования: числа, строки, списки, словари и булевы значения.
94
|
Глава 2
Функции
Это "глаголы" языка программирования: операторы, встроенные функция и
пользовательские функции.
Оператор цикла for…in…
Позволяет исполнять определенный блок кода для каждого элемента списка.
Оператор условия if...else...
Позволяет принимать решения, какие фрагменты кода исполнять, на основе, как
правило, атрибутов данных.
Ошибки
Мы рассмотрели разные типы ошибок, с которыми вам, скорее всего, придется
столкнуться при написании программ, наилучшие методы для диагностики их
причин и их предотвращения.
Мы также на практике объединили эти понятия для создания простой программы
для обработки выборочных данных системы Citi Bike города Нью-Йорк. В последующих главах мы рассмотрим этот пример более подробно, а также исследуем
новые примеры. Наша следующая задача заключается в том, чтобы расширить понимание процесса оценки самих данных как части работы по выпасу данных.
ГЛАВА 3
Понимание качества данных
Данные находятся повсюду вокруг нас. Они создаются автоматически нашими мобильными устройствами, покупками и физическими перемещениями. Они собираются электрическими счетчиками, системами общественного транспорта и инфраструктурой связи. Они используются для оценки нашего здоровья, зарплатного потенциала и кредитоспособности1. Экономисты даже объявили, что данные — это
"новая нефть"2, в свете их потенциальных возможностей для преобразования многих аспектов нашей жизни.
И хотя данные могут быть в изобилии, по правде говоря, качественные данные остаются в дефиците. Поборники концепции "революции данных" утверждают, что
при наличии достаточного количества данных мы можем лучше понимать настоящее и улучшить, и даже предсказать, будущее. Но чтобы все это было бы даже в
области возможного, вся эта аналитика должна быть основана на данных высокого
качества. При отсутствии данных высокого качества все наши усилия по выпасу,
анализу, визуализации и предоставлению данных дадут нам, в наилучшем случае,
не больше знаний об окружающем нас мире, чем у нас бы было с самого начала.
Тогда как это было бы пустой тратой усилий, последствия неспособности распознать низкокачественные данные будут еще хуже, поскольку результатом может
быть создание рационального на вид, но искаженного в действительности представления о реальности. Более того, поскольку управляемые данными системы используются для принятия широкомасштабных решений, даже небольшой объем
некачественных данных может причинить значительный вред и таить в себе большую опасность. Конечно же, для "тренировки" модели машинного обучения можно
использовать данные о сотнях или даже тысячах людей. Но если эти данные не
представляют население, к которому эта модель будет применяться, количество
людей, на которых могут распространяться последствия решений этой системы,
может быть в сотни или тысячи раз большим, чем количество в исходном обучающем наборе данных. Поскольку ставки такие высокие, обеспечение качества данных является неотъемлемой частью процесса выпаса данных. Но что означает тре1
См., например, книгу The Secret Bias Hidden in Mortgage-Approval Algorithms, авторы Emmanuel
Martinez и Lauren Kirschner (издательство The Markup) (https://themarkup.org/denied/2021/08/25/
the-secret-bias-hidden-in-mortgage-approval-algorithms).
2
См. статью The World's Most Valuable Resourse Is No Longer Oil, but Data в журнале The Economist.
(https://economist.com/leaders/2017/05/06/the-worlds-most-valuable-resource-is-no-longer-oil-but-data).
96
|
Глава 3
бование, чтобы данные были "высокого качества"? По моему личному мнению,
данные являются высокого качества только в том случае, если они как соответствуют цели их применения, так и обладают высокой внутренней целостностью.
Но что именно означают эти два термина? Это как раз то, что мы подробно исследуем в этой главе. Мы начнем с обсуждения понятия соответствия данных, которое связано с пригодностью данных для использования в конкретном контексте,
или предоставления ответа на конкретный вопрос. Затем мы разложим на составляющие аспекты целостности данных: характеристики набора данных, которые
влияют как на его соответствие данному назначению, так и на типы анализов, для
которых мы можем ответственно использовать эти данные. Наконец, мы рассмотрим некоторые инструменты и стратегии для поиска данных и работы с ними, которые помогут нам максимально повысить их общее качество, позволяя нам быть
уверенными в надежности результатов, полученных на их основе.
В случае если все это начинает казаться вам скучным, я хочу повторить призыв,
который я написала в разд. "Что такое выпас данных?" главы 1: попытка пропустить работу по оценке качества данных может нанести ущерб вашим усилиям по
выпасу данных. В лучшем случае вы предоставите свою работу для общего использования и столкнетесь с вопросами об использованном вами процессе, на которые у
вас не будет ответов. А в худшем случае вы в итоге будете продвигать "знания",
которые являются ошибочными и могут нанести серьезный вред. Вы также лишите
себя хороших технических навыков, поскольку как раз решение задач качества
данных предоставляет наилучшую возможность нарастить свои знания в области
программирования. Если вы действительно хотите преуспеть в области выпаса
данных, то оценка качества данных должна быть частью установившегося порядка
вашей работы.
Не забываем о документировании
Как упоминалось ранее, подробное комментирование разрабатываемого кода
будет разумной тратой времени, если вы хотите улучшить свои навыки кодирования и извлечь бóльшую пользу из своего кода в будущем. Но, в случае надобности, приложив достаточно усилий и времени, разобраться, что и как делает программа, можно всегда по ее коду и без комментариев.
Однако то же самое нельзя сказать о работе по обеспечению качества данных:
документация в вашем дневнике данных в действительности является незаменимой. Выводы касательно репрезентативности или действительности набора
данных будут в большинстве случаев основаны на информации, полученной
вами из широкого круга источников: от вашего собственного чтения и исследований до разговоров с экспертами до дополнительных наборов данных. Но
без наличия качественной документации, указывающей, кто сказал, что или
каким образом вы получили данную информацию, любая попытка повторить
или подтвердить вашу предыдущую работу будет почти однозначно обречена
на неудачу.
Понимание качества данных
|
97
Почему? Потому что источники информации, особенно в Интернете3, имеют свойство перемещаться, изменяться и исчезать. Например, ссылки, которые я изначально включила в эту книгу, переместились или прекратили работать в течение всего
лишь нескольких месяцев после этого. Вы можете потерять связь с экспертом, с
которым вы разговаривали несколько месяцев тому назад, или же повторный поиск
может возвратить результаты, отличающиеся от предыдущих. Тогда как вопрос
выбора конкретного подхода решать только вам, я не могу сказать достаточно уверенно, насколько важным является документирование своей работы по выпасу данных, особенно что касается вопроса обеспечения качества данных (который составляет практически всю эту область). Без подробного описания выполняемого процесса, когда настанет время предоставить результаты своей работы для общего
использования, вы можете обнаружить, что находитесь на зыбкой почве, что вынудит вас начать все повторно, причем с самого начала.
Оценка соответствия данных
Наверное, одним из самых распространенных заблуждений о выпасе данных является представление, что это в основном количественный процесс, т. е. заключается
в основном в работе с числами, формулами и кодом. В действительности же, независимо от типа обрабатываемых данных, будь то показатели температуры или сообщения социальных сетей, основная работа по выпасу данных заключается в принятии широкого диапазона субъективных решений. Например, от таких, как точно
ли ваши данные представляют исследуемый феномен, до таких, как что делать насчет недостающих точек данных и имеется ли достаточно данных, чтобы получить
какое-либо значащее понимание. Первое понятие — насколько точно данный набор
данных представляет исследуемое явление — в общих чертах выражает то, что я
имею в виду под соответствием данных. И субъективная оценка соответствия набора данных его предполагаемому назначению играет намного бóльшую роль, чем
применение математических формул. Этому есть довольно простое объяснение:
наш мир довольно беспорядочное место, и даже то, что может выглядеть как простейший набор данных о некотором его аспекте, всегда фильтруется через тот или
иной человеческий фильтр. Возьмем нечто такое прямолинейное, как измерение
температуры на вашем рабочем месте в течение одной недели. Теоретически, нужно просто взять термометр, установить его и каждый день записывать показатели.
Что может быть проще?
Или же не все так просто? Начнем с оборудования. Какой термометр вы использовали, цифровой или ртутный? Где именно вы его расположили? Возле двери, окна,
источника обогрева или охлаждения? Снимали ли показатели в одно и то же время
3
Например, см. статью Raiders of the Lost Web, автор Adrienne LaFrance, в журнале The Atlantic
(https://theatlantic.com/technology/archive/2015/10/raiders-of-the-lost-web/409210).
98
|
Глава 3
каждый день? Подвергался ли термометр когда-либо воздействию прямых солнечных лучей? Какой в вашем помещении типичный уровень влажности?
Вы можете думать, что все это надуманные факторы сложности, но если вам когдалибо приходилось жить в общем здании (например, в многоквартирном доме), то
вам, наверное, пришлось испытать чувство, что окружающая температура была
намного выше или ниже, чем показания какого-то термометра. Подобным образом,
если вам когда-либо приходилось ухаживать за больным ребенком, вы, скорее всего, слишком хорошо знаете, как разные типы термометров (или даже один и тот же
термометр с интервалом измерения всего лишь в несколько минут) дают разные
показания температуры тела
Иными словами, снимаемое вами двух- или трехзначное значение температуры
может подвергаться воздействию большого количества факторов, а само значение
не предоставляет никакой информации ни об одном из этих факторов. Вот поэтому,
когда мы пытаемся получить ответ на вопрос при помощи данных, знать только о
содержимом набора данных будет недостаточно; нам нужно также знать о процессах и механизмах, которые использовались для сбора этих данных. Затем, имея в
своем распоряжении все, что мы знаем о том, как собирались наши данные, нам
нужно определить, действительно ли их можно использовать, чтобы дать значащий
ответ на наш конкретный вопрос.
Конечно же, данная задача не является ни новой, ни уникальной. С точно такой же
задачей сталкиваются ученые во всех научных областях в попытках обнаружить
новую информацию об окружающем нас мире. Изучение рака вряд ли могло развиваться, если бы каждому отдельному ученому нужно было бы самому проводить
конкретные исследования. Без возможности основывать свои исследования на базе
работ других ученых научный и технический процесс постепенно бы остановился,
если бы вообще не сошел с рельс. Поэтому в течение времени научное сообщество
разработало три ключевые метрики для определения пригодности или соответствия
набора данных получению ответа на данный вопрос: достоверность, надежность
и репрезентативность.
Достоверность данных
В своем простейшем варианте достоверность данных описывает, в какой мере измерительный прибор измеряет то, что он должен измерять. В нашем примере с измерением комнатной температуры это означает обеспечение того, чтобы выбранный нами термометр в действительности измерял температуру воздуха в комнате, а
не чего-либо другого. Например, тогда как традиционные спиртовые термометры,
скорее всего, будут правильно измерять температуру именно воздуха, инфракрасные термометры имеют склонность измерять температуру поверхности, на которую
они направлены. Поэтому, даже в случае такого простого на вид измерения, как
измерение температуры воздуха, нам нужно учитывать инструменты и методы,
применяемые для снятия показаний данных, чтобы обеспечить их достоверность
относительно нашего вопроса.
Понимание качества данных
|
99
Неудивительно, что при сборе данных о явлениях более сложных, чем простые физические явления, процесс становится еще более сложным. Конструктивная достоверность описывает степень эффективности фиксации нашими данными конструкции (обычно абстрактной) или идеи, явления, которое мы пытаемся понять. Например, скажем, что мы хотим узнать, какие школы в нашем районе "наилучшие". Какие
данные могут помочь нам получить ответ на этот вопрос? Прежде всего, нам нужно
осознать, что термин "наилучшие" является неточным. Наилучшие в каком отношении? Нас интересуют школы с наивысшим процентом успешных выпускников? С
наилучшими результатами стандартизованных тестов? Школы с наилучшими оценками учащихся? С наилучшими оценками работы преподавателей? Уровнем удовлетворения учеников своей школой? Уровнем участия во внеклассных активностях?
Чтобы использовать данные для того, чтобы начать отвечать на этот вопрос, сначала нужно четко сформулировать две цели. Первая — выяснить, "наилучшие" для кого? Вы пытаетесь получить ответ на этот вопрос для своего ребенка? Или для ребенка своего друга? Ответив на этот вопрос, вы сможете лучше выполнить вторую
цель, которая заключается в том, чтобы воплотить в жизнь ваше конкретное представление о "наилучшем". Если ребенку вашего друга нравится заниматься, например, спортом, тогда внеклассные занятия могут быть более важными, чем учебные.
В области анализа данных этот процесс выбора индикаторов называется операционализацией конструкции и неизбежно требует выбора и балансирования заменителей идеи или концепта, который мы пытаемся понять. Эти заменители (такие как
процент успешных выпускников, результаты тестов, внеклассные активности и т. п.)
представляют собой те элементы, о которых мы можем собрать данные, избранные нами для представления абстрактного концепта ("наилучшая" школа), который
нельзя измерить непосредственно. Высококачественные данные по меньшей мере
должны обладать хорошей конструктивной достоверностью касательно нашего
вопроса, ибо в противном случае наши результаты выпаса данных будут бессмысленными.
Как? И для кого?
Начав думать о конструктивной достоверности для своих вопросов по выпасу
данных, вы, скорее всего, начнете гораздо больше любопытствовать об этом
понятии повсюду, где вам придется сталкиваться с использованием данных
для принятия решений или делания "предсказаний" об окружающем нас мире.
Более чем вероятно, что вы обнаружите множество случаев неясных утверждений (типа "наилучший") без предоставления достаточного или вообще какого-либо объяснения о том, каким образом это понятие "наилучший" было
определено. Подобным образом, разработчики систем, управляемых данными,
на вопрос "Для кого?" могут изначально ответить с энтузиазмом "Для всех!"
Но настоящий ответ лежит в плоскости их выбора заменителей, которые они
могли выбрать эвристическим образом на основе лишь своих собственных
вкусов и предпочтений. В этих случаях настоящим ответом на вопрос "Для кого?" является "Для таких людей, как я".
100
|
Глава 3
Конечно же, иногда вам могут сказать, что просто невозможно знать, как определяется "наилучший", поскольку для принятия решений или предсказания
используется так называемый "черный ящик", как часто описывают неконтролируемые системы машинного обучения. Но, как я упоминала в разд. "Что
такое качество данных" главы 1, такой ответ несколько лукавый. В настоящее
время мы действительно не можем с уверенностью сказать, каким именно образом система машинного обучения присвоила веса или приоритеты определенным факторам при составлении предсказаний. Но при этом мы знаем, что
она всегда будет повторять и усиливать образцы, существующие в данных, использованных для ее "обучения". И в таких случаях ответы на оба вопроса
"Как?" и "Для кого?" будут находиться в составе обучающих данных, если
разработчики системы достаточно уверены в ней, чтобы предоставить их вам.
Другим типом достоверности, имеющей важность для обеспечения соответствия
данных, является достоверность содержимого. Этот тип достоверности связан с
тем, насколько полны ваши данные для данного измерения заменителя. Предположим, что в нашем примере с "наилучшей" школой мы установили, что индикатором
"наилучшей" являются оценки учеников, но в нашем распоряжении есть оценки
только по таким предметам, как история и физкультура. Хотя для многих людей
данные по оценкам могут, в принципе, удовлетворять требованию конструктивной
достоверности, для определения наилучшей школы наличие этих данных только
по двум предметам будет недостаточным для удовлетворения требования достоверности содержимого, а для высококачественных данных нам нужны обе эти достоверности.
Посмотрим, что скажут данные
Принятие решений на основе данных может быть мощным инструментом для
понимания происходящего в окружающем нас мире. Иногда этот подход может быть привлекательным, потому что кажется, что он выходит за рамки
(или, по крайней мере, избегает их) необходимости продираться сквозь трудные дискуссии о нормах, ценностях и этических принципах.
Но в действительности такое доверие "данным" означает, что мы полагаемся
на ценности, предположения и предвзятости создателей этих данных, тех, кто
разработал и реализовал сбор данных. Вот поэтому данные сами по себе никогда не могут быть объективными, такими могут быть только процессы выпаса
данных, используемые для их обработки.
Надежность данных
В рамках набора данных под надежностью данного индикатора имеются в виду его
точность и стабильность. Взятые вместе, они помогают нам оценить, дадут ли из-
Понимание качества данных
|
101
мерения одного и того же индикатора, выполненные дважды при одинаковых условиях, одинаковые — или, по крайней мере, очень похожие — результаты. Воспользуемся снова нашим примером с измерением температуры: измерение температуры
ребенка при помощи орального термометра вряд ли даст очень надежные результаты, поскольку для этого требуется, чтобы ребенок держал рот закрытым в течение
сравнительно длительного времени, с чем, по моему опыту, дети справляются не
очень хорошо. В противоположность этому, установка термометра под мышку может дать более надежные показания, поскольку ребенка можно обнять, чтобы
удерживать термометр на месте. Но в этом случае показания термометра могут не
совсем точно отображать настоящую внутреннюю температуру тела ребенка, как
могут другие методы. Вот почему большинство медицинских справочников указывают разные предельные температуры лихорадки для детей в зависимости от метода, используемого для измерения температуры.
В случае абстрактных концептов и реальных данных определение надежности индикатора данных особенно сложно, поскольку данные никогда, по сути, нельзя собрать более одного раза, будь то по причине непомерно высокой стоимости или
невозможности повторить с точностью все обстоятельства. В некоторых случаях
надежность оценивается примерно, сравнивая две похожие группы и используя или
новые, или ранее собранные данные. Поэтому, хотя драматические колебания результатов стандартизированных тестов от одного года к следующему указывают,
что эти результаты не могут быть надежным индикатором качества данной школы,
эта непостоянность сама по себе всего лишь часть проблемы. Ведь эти результаты
могут отражать качество обучения, но также могут отражать и изменения в самом
тесте или способе его оценки или какое-либо другое нарушение в среде обучения
или прохождения теста. Чтобы определить, достаточно ли надежные данные результатов стандартизованного теста для включения их в вашу оценку "наилучшей"
школы, нужно будет исследовать сравнительные данные за другие годы или из других школ, а также собрать дополнительную информацию о более широких обстоятельствах, которые могли бы вызвать это колебание. Конечным заключением может быть, что бóльшая часть данных результатов теста достаточно надежная, но
нужно удалить несколько определенных точек данных, или же что данные недостаточно надежные, чтобы быть составляющей высококачественного процесса обработки данных.
Репрезентативность данных
Ключевая ценность систем, управляемых данным, состоит в том, что они позволяют нам создавать знания или даже делать предсказания о людях и явлениях, слишком масштабных или сложных для людей, чтобы эффективно рассуждать о них.
Как следует из этого, посредством первичной обработки и анализа данных мы можем принимать решения более быстро и объективно. С учетом мощных вычислительных средств, доступных в настоящее время частным лицам, не говоря уже об
организациях, эти управляемые данными системы, несомненно, могут генерировать
решения более быстро, чем это могут делать люди. Но правильно ли эти знания
102
|
Глава 3
отображают определенную совокупность лиц или ситуацию, зависит напрямую от
репрезентативности используемых данных.
Является ли набор данных достаточно репрезентативным, зависит от нескольких
обстоятельств, наиболее важное из которых восходит к вопросу "Для кого?", который мы обсуждали в разд. "Достоверность данных" ранее в этой главе. При разработке расписания занятий для начальной школы возможно собрать данные для всей
совокупности учеников. Если все остальные критерии для соответствия данных
будут отвечать требованиям, тогда мы сразу же будем знать, что наши данные являются репрезентативными, поскольку мы собрали их непосредственно со всей совокупности (или о ней) лиц, к которой они будет применяться.
Но что если нам нужно решить такую же задачу для всех школ города? Крайне маловероятно, что вы сможете успешно собрать данные о каждом отдельном учащемся каждой отдельной школы; это означает, что при разработке расписания вы будете полагаться на входные данные только для подмножества учащихся.
Таким образом, при работе с подмножеством (или выборкой) всегда важно обеспечить его репрезентативность более широкой совокупности, к которой вы планируете применять свои заключения. Подробное рассмотрение методологии формирования выборки из совокупности выходит за рамки этой книги4, но базовая идея
состоит в том, что для того, чтобы полученная нами информация точно распространялась на определенное сообщество, используемая нами выборка данных
должна пропорционально отражать состав этого сообщества. Это означает, что нам
нужно вложить время и ресурсы в понимание нескольких аспектов этого сообщества как целого, прежде чем мы сможем понять, является ли наша выборка репрезентативной.
Здесь вы, наверное, думаете: подождите, если бы мы уже смогли получить информацию о всей совокупности, то нам вообще не нужно было бы делать из нее выборку. В некотором смысле это верно. Во многих случаях возможно получить некоторую информацию обо всем сообществе, вот только не совсем ту информацию, которая нам требуется. Например, в нашем сценарии с составлением расписания
занятий мы бы, в идеале, собрали информацию о том, как учащиеся каждый день
добираются до школы и возвращаются из нее домой, а также сколько времени это
занимает. Кроме этого, мы бы получили некоторую информацию о расписании работы технических работников школы. Но для всей совокупности школы мы бы
могли иметь (если мы работаем совместно со школьной системой), скорее всего,
только такую информацию, как домашние адреса, адрес школы и, возможно, тип
транспортной поддержки (https://schools.nyc.gov/school-life/transportation/buseligibility). Используя эту информацию, скорее всего, совместно с некоторой дополнительной административной информацией, мы могли бы начать создавать
прикидочные оценки доли от всей совокупности определенных типов учеников,
ездящих в школу, а затем попытаться воспроизвести эти доли при взятии репрезен4
Amitav Banerjee и Suprakash Chaudhury предоставляют легкочитаемый обзор этого предмета в своей
статье Statistics Without Tears (https://ncbi.nlm.nih.gov/pmc/articles/PMC3105563).
Понимание качества данных
|
103
тативной выборки из результатов нашего опроса. Только на этом этапе мы будем
готовы перейти к следующему шагу процесса выпаса данных.
Как мы видим, обеспечение репрезентативности требует внимательного рассмотрения, какие характеристики совокупности уместны для нашего вопроса по выпасу данных. Также необходимо попытаться найти достаточный объем дополнительной информации, чтобы обеспечить пропорциональное представление этих
характеристик нашим набором данных. Этот тест на соответствие данных многие
управляемые данными службы постоянно проваливают, что, наверное, и неудивительно. Тогда как компании и исследователи могут расхваливать количество данных, использованных в разработке своих систем, реальность такова, что большинство легкодоступных для них наборов данных обычно не являются репрезентативными, скажем, для совокупности населения США или мирового населения.
Например, крайне маловероятно, что данные, например, о трендах поисковых систем, активностях в социальных сетях, использовании общественного транспорта
или владении смартфоном будут репрезентативными для более широкой совокупности, поскольку они неизбежно подвержены воздействию таких факторов, как
доступ к Интернету и уровень дохода. Это означает, что в этих наборах данных сообщества имеют избыточную репрезентацию, тогда как репрезентация других
сущностей недостаточна (иногда весьма значительно). В результате имеем системы, неспособные к обобщению, например системы распознавания лиц, которые
не "видят" лиц афроамериканцев (http://proceedings.mlr.press/v81/buolamwini18a/
buolamwini18a.pdf).
Если вы поставлены перед проблемой нерепрезентативных данных, как ее можно
решить? Как минимум, вам нужно откорректировать (и четко изложить) свою
оценку аспекта "Для кого?", чтобы он отражал совокупность, которую он должен
представлять. Это сообщество — единственное, для которого будут действительны
ваши знания, полученные в результате выпаса данных. Также имейте в виду, что
репрезентативность может обеспечить только точное отображение реальности результатами ваших усилий по выпасу данных. Она не является ценностным суждением касательно того, следует ли продолжать сохранять эту реальность. Если результаты вашей работы по выпасу данных будут использованы для осуществлений
изменений в системе или организации, завершение вашего процесса выпаса данных
в действительности является только начальной точкой в процессе обдумывания,
каким образом применить полученные знания, особенно касательно таких сложных
вопросов, как справедливость (https://youtube.com/watch?v=jIXIuYdnyyk).
Это верно даже тогда, когда у нас есть данные обо всей совокупности. Например,
если нужно узнать, какие организации получили грант определенного типа, тогда
представляющей для нас интерес "совокупностью" или сообществом будут только
те организации, которых получили эти гранты. В то же самое время проверка данных на репрезентативность по отношению ко всей совокупности в целом попрежнему имеет значение: в случае если одно или больше сообществ в вашей совокупности получателей гранта представлены чрезмерно или недостаточно, это может быть признаком скрытых факторов, влияющих на выбор получателей.
104
|
Глава 3
Проблема коллапса контекста
Огромный источник "плохих" данных и анализов данных можно проследить до
явления, называющегося коллапсом контекста (https://zephoria.org/thoughts/
archives/2013/12/08/coining-context-collapse.html), где данные, созданные для
одной цели, используются, обычно без особого критического осмысления, для
другой. Например, онлайновые рекламные сети часто используют данные нашей истории покупок или просмотра веб-страниц для принятия решений, какую рекламу нам показывать или какие продукты рекомендовать. Но при этом
эти системы часто смешивают то, что мы купили, с тем, что мы будем покупать, и результаты часто поразительно лишены понимания темы, что наглядно
иллюстрируется в следующем твите (https://twitter.com/GirlFromBlupo/status/
982156453396996096):
Когда данные, созданные в одном контексте (просмотр веб-страниц), используются для принятия решений в другом контексте (реклама), результаты могут
дойти до абсурда. Тогда как ситуации, подобные этой, принадлежат к разряду
обычных странностей онлайнового сообщества, некоторые случаи коллапса
контекста могут нанести серьезный вред, как я описывала во врезке "Распаковка системы COMPAS" главы 1.
Оценка целостности данных
Понятие соответствия данных означает, являются ли наши данные правильными
для использования их в целях получения ответа на наш вопрос по выпасу данных.
С другой стороны, понятие целостности данных главным образом означает, могут
ли имеющиеся данные поддерживать анализы, которые нужно выполнить для того,
чтобы ответить на этот вопрос. Как мы увидим в этом разделе, в процессе оценки
целостности данных необходимо рассмотреть много разных характеристик данных.
Но обязательно ли определенный набор данных должен обладать всеми этими характеристиками, чтобы иметь высокую целостность и, следовательно, высокое качество? Нет, не обязательно. Тогда как некоторые из этих характеристик являются
необходимыми, важность других зависит от нашего специфичного вопроса и методов, необходимых для получения ответа на него. Кроме этого, за редкими исклю-
Понимание качества данных
|
105
чениями, многие из этих характеристик усовершенствуются и улучшаются в процессе выпаса данных.
Иными словами, тогда как обеспечение соответствия данных является обязательным, типы и степень целостности, требуемые для конкретного проекта по выпасу
данных, будут варьироваться. Конечно же, чем больше ваши данные будут соответствовать этим требованиям, тем более полезными они будут как для вас, так и
для других.
В общих чертах, набор данных высокой целостности в той или иной степени будет
обладать следующими характеристиками5:
Необходимые, но недостаточные
Известного происхождения
Тщательно аннотированные
Важные
Уместные
Полные
Большого объема
Многомерные
Атомарные
Достижимые
Непротиворечивые
Ясные
Размерностно структурированные
Но, как я уже упоминала, не все эти характеристики целостности данных имеют
одинаковую важность. Некоторые из них являются обязательными, тогда как другие почти всегда являются результатом определенных шагов процесса выпаса данных, а не его предшественниками. Хотя мы стремимся к тому, чтобы к началу анализов наши данные обладали как можно большим количеством этих характеристик,
нам всегда нужно достичь определенного баланса между более полным усовершенствованием наших данных и завершением нашей работы вовремя, чтобы полученные в результате знания были полезными и своевременными.
В этом смысле оценка целостности данных может быть полезным методом для назначения приоритетов нашим усилиям по выпасу данных. Например, при отсутствии для набора данных любой характеристики из категории "Необходимые, но недостаточные", можно полностью забыть об его использовании. Если набору данных
не хватает одной или двух характеристик из категории "Важные", его данные, мо5
Этот список взят из замечательной книги Now You See It: Simple Visualization Techniques for
Quantitative Analysis, автор Stephen Few (Издательство Analytics Press), с адаптациями на основе моего
опыта по выпасу данных.
106
|
Глава 3
жет быть, можно будет еще спасти, объединив этот набор с другими или же ограничив область анализа, и, соответственно, утверждений, которые можно выдвигать
на основе его обработки. А вот получение характеристик из категории "Достижимые" часто является целью шага очистки процесса выпаса данных, а не чем-то, что
можно ожидать от большинства данных реального мира при первом столкновении
с ними.
В конечном итоге, степень соответствия наборов данных этим характеристикам,
которые рассматриваются более подробно далее, будет зависеть от количества времени, которое вы можете позволить уделить своему проекту выпаса данных, но в
случае отсутствия хорошего солидного большинства из них вы получите ограниченные знания в результате его обработки. Как подробно рассматривается в главе 6,
именно это непостоянство является причиной такого тесного переплетения областей выпаса данных и качества данных.
Необходимые, но недостаточные
Большинство времени обрабатываемые нами данные были собраны кем-то другим
или большим количеством людей и процессов, к которым у нас нет прямого доступа. В то же самое время мы должны быть в состоянии поддерживать как наш процесс выпаса данных, так и любые полученные из него знания. Это означает, что
успешный процесс выпаса данных должен обладать парой по-настоящему важных
характеристик.
Известного происхождения
Как обсуждалось в разд. "Что такое качество данных?" главы 1, данные — это
результат человеческих решений о том, что и как измерять. Это означает, что использование набора данных, собранных другими лицами, требует значительного
доверия к ним, особенно потому, что верифицирование каждой отдельной точки
данных возможно лишь в очень редких случаях. Если бы это было возможно, то
мы, наверное, просто собирали бы данные самостоятельно. Вот почему так важно
знать происхождение используемого набора данных: если мы не знаем, кто собрал
их, какими методами и/или с какой целью, нам будет очень трудно дать оценку,
подходят ли они для нашей цели выпаса данных или насколько правильно мы интерпретируем их.
Конечно же, это не означает, что нам, к примеру, необходимо знать дни рождения и
любимые цвета всех, кто участвовал в создании используемого набора данных. Но
вы должны попытаться узнать достаточно об их профессиональном опыте, мотивации для сбора этих данных (например, является ли это требованием закона), а также использованные ими методы, чтобы получить некоторое понимание о том, какие индикаторы может потребоваться подтвердить, а какие можно использовать
как есть. В идеале как информация о создателях данных, так и достаточная документация об этих процессах должна быть доступна, чтобы вы могли довольно быстро получить ответы на все эти вопросы о происхождении данных. Но если эту
Понимание качества данных
|
107
информацию будет трудно найти, возможно, лучше оставить этот набор данных и
найти другой. Поскольку эта информация вам нужна для того, чтобы оценить соответствие данных, может быть лучше потратить свое время на поиски другого набора данных или на сбор данных самому.
Тщательно аннотированные
Тщательно аннотированный набор данных содержит достаточно сопутствующей
информации, или метаданных, чтобы его можно было интерпретировать. Эта информация должна содержать от высокоуровневых объяснений методологии сбора
данных до "словарей данных", описывающих каждый индикатор данных вплоть до
его единиц. Это может казаться прямолинейной задачей, но для методов предоставления такой аннотационной информации не всегда есть общепринятые стандарты. Иногда она может находиться в файлах данных или в самих элементах данных,
а иногда может содержаться в отдельных файлах или документах, хранящихся в
полностью отдельном месте, чем то, из которого были получены данные.
Но независимо от их структуры или метода предоставления, надежные документы
аннотации данных являются неотъемлемым компонентом данных высокой целостности, поскольку без них невозможно выполнять какие бы то ни было анализы
данных или делать какие-либо заключения на их основе. Например, представьте
себе попытку интерпретировать финансовый план, не зная разрядности его данных:
то ли это доллары, то ли тысячи долларов, то ли миллионы. Очевидно, что это будет невозможным. А чтобы понять истинно американское отношение к важности
аннотирующих документов, наподобие словарей данных, знайте, что для того, чтобы их получить, иногда требуется подать судебный иск (https://trac.syr.edu/foia/
ice/20210805).
Выведывание данных USDA6
Служба сельскохозяйственного маркетинга министерства сельского хозяйства
США (USDA) предоставляет ежедневные отчеты (https://www.ams.usda.gov/
market-news/custom-reports) о перемещениях продуктов питания через 16 центральных сельскохозяйственных рынков по всему миру. Каждый день специалисты из министерства USDA обходят эти рынки, записывая размеры, цены, состояние, происхождение и другие аспекты предлагаемых на продажу сельскохозяйственных продуктов, таких как, например, яблоки (https://oreil.ly/DzjBP).
И хотя эти данные обладают многими признаками высокой целостности — они
чистые, подробно сегментированы и большого объема — значения некоторых
элементов данных неясны. Например, что означают термины "FINEAPPEAR"
и "FRAPPEAR"? К счастью, ответ на этот вопрос значительно облегчается знанием, что эти данные собраны министерством USDA о центральных сельскохозяйственных рынках.
6
Unites States Department of Agriculture — Министерство сельского хозяйства США.
108
|
Глава 3
Выполняем поиск в Интернете по фразе "USDA Terminal Market", прокручиваем
результаты поиска и где-то посредине страницы обнаруживаем ссылку с этими
терминами, которая ведет на отчет из центрального сельскохозяйственного рынка города Лос-Анжелес (https://www.ams.usda.gov/mnreports/hc_fv020.txt).
Этот отчет содержит телефонный номер, один быстрый звонок по которому
раскрывает эту тайну.
Важные
Добиться какого-либо успеха в выпасе данных, не имея достаточной информации о
происхождении набора данных и о его метаданных, — задача довольно трудная. Но
даже при их наличии мы далеко не уедем, не имея достаточного объема данных, за
правильный период или периоды времени и с правильной степенью детализации.
Вместе с этим, следующие характеристики данных часто можно оценить, а иногда
и улучшить, посредством нашей собственной работы по выпасу данных. Таким образом, тогда как работать с наборами данных, которые уже обладают такими характеристиками, будет, несомненно, легче, может заслуживать внимания исследование
наборов данных и без некоторых характеристик.
Своевременные
Насколько актуально состояние используемых данных? Если только вы не изучаете
какой-либо исторический период, важно обеспечить, чтобы ваши данные были достаточно свежими, чтобы осмысленно отображать текущее состояние в мире. Хотя,
насколько устаревшими должны быть данные, чтобы считаться слишком устаревшими, будет зависеть как от исследуемого явления, так и от периодичности сбора и
предоставления данных.
Например, если вас интересует демография вашего района, но ваши самые свежие
данные были собраны несколько лет тому назад, существует высокая вероятность,
что с тех пор в этой области произошли значительные изменения. Данные по безработице будут устаревшими, если им свыше одного месяца, тогда как данные
биржевых котировок, полученные всего лишь несколько секунд тому назад, уже
будут слишком устаревшими, чтобы их информацию можно было использовать для
принятия решений по сделкам. Если только вы не знакомы с данной областью,
оценка своевременности данных будет, скорее всего, требовать проведения консультаций с экспертами, а также с создателем этих данных.
Полные
Содержит ли набор данных все значения данных, которые он должен содержать?
Например, в данных по яблокам во врезке "Выведывание данных USDA" ранее в
этой главе только несколько строк содержат описание внешнего вида, тогда как
большинство строк оставлено пустыми. Возможно ли все-таки получить полезные
Понимание качества данных
|
109
знания из таких неполных данных? Чтобы ответить на этот вопрос, сначала нужно
ответить на два других. Первый: почему эти данные пропущены? Второй: необходим ли этот индикатор данных для выполнения конкретного анализа, требуемого
для нашего процесса выпаса данных?
Например, данные могут быть неполными по причине пропущенных отдельных значений или по причине нерегулярного сбора данных. Например, данные, которые
должны собираться ежемесячно, содержат разрыв между сборами в шесть месяцев.
Набор данных также может быть обрезан, что является распространенной проблемой
при попытке открыть набор данных большого размера в электронной таблице. Независимо от причины нехватки некоторых данных, ее необходимо определить, чтобы
знать, что делать дальше. В случае с данными по яблокам (см. врезку "Выведывание
данных USDA" ранее в этой главе) мы потенциально могли бы игнорировать категорию Appearance (внешний вид), если наш основной интерес состоит в определении
метода установки цен на разные сорта яблок. Альтернативно можно связаться с рынком, чтобы уточнить, не представляют ли пропуски в столбце Appearance некое значение по умолчанию, с явным указанием которого они просто не хотели морочиться.
Даже обрезанные данные могут не представлять проблемы, если доступная часть
данных покрывает период времени, достаточный для наших целей, но все равно будет полезным узнать настоящее количество записей и диапазон дат данных для контекста. В то время как проблематичность разрывов в данных можно уменьшить при
помощи определенных статистических приемов, знание, что перерыв в сборе данных
обусловлен неким сбоем, может изменить тип выполняемых вами анализов или даже
вообще вопрос, на который вы пытаетесь получить ответ.
Иными словами, тогда как наличие полных данных всегда предпочтительнее, зная
причину отсутствия данных, вы, тем не менее, возможно, сможете продолжить работать над своим процессом по выпасу данных. Но всегда необходимо узнать причину отсутствия данных и обязательно задокументировать добытые сведения.
Большой объем
Какое количество точек данных будет "достаточным"? Как минимум, количество
записей набора данных должно быть достаточным, чтобы поддерживать тип анализа, необходимый для получения ответа на ваш конкретный вопрос. Если вам требуется подсчет, например, количества звонков на номер 3117, связанных с жалобами
на шум в определенный год, тогда "достаточное" количество данных означает записи всех звонков на этот номер в течение данного года.
Но если ваш вопрос затрагивает общие или обобщаемые закономерности или тенденции, то тогда критерий достаточности будет менее четким. Например, если
нужно определить наиболее "занятую" станцию системы Citi Bike, то какой должна
быть длительность рассматриваемого периода? Неделя? Месяц? Год? Рассматривать только рабочие дни или же все дни? Правильный ответ будет частично зави7
Номер в США, на который можно позвонить, чтобы узнать разного рода информацию о муниципальных, государственных, общественных и прочих службах, а также для жалоб неэкстренного характера.
110
|
Глава 3
сеть от более подробной конкретизации вашего вопроса. Вас интересует опыт
только регулярных пользователей или гостей города? Или же вы пытаетесь выработать понимание сути вопросов для планирования транзита, размещения розничных торговых точек или качества обслуживания? Кроме этого, вы заинтересованы в
том, какая из станций является действительно наиболее "занятой", или же больше в
какой-либо определенной интенсивности использования велосипедов? Как часто
бывает, правильный ответ в значительной степени сводится к правильной постановке вопроса, что в большинстве случаев требует предельной конкретности.
Но одна из наиболее сложных составляющих оценки "полноты" данных состоит в
трудности принятия во внимание факторов, которые могут оказывать влияние на
исследуемую вами закономерность или тренд, не имея достаточно хороших предварительных знаний о предметной области. Например, тогда как вы можете с легкостью ожидать, что уровень пользования услугами системы Citi Bike может варьироваться в зависимости от времени года, как насчет влияния, которое может оказать сокращение услуг общественного транспорта? Или повышение стоимости
проезда? Эти изменения могут иметь последствия для нашего анализа, но как мы
можем знать это на этапе, когда мы только приступаем к работе?
Ответ заключается, как это так часто бывает, в привлечении экспертов. Возможно,
что повышение платы за проезд в общественном транспорте временно повышает
количество аренд велосипедов, но только на несколько месяцев. Возможно, что
люди, регулярно использующие велосипед для поездок по своим делам, подготовлены к плохой погоде и продолжают свои поездки как обычно, даже когда идет
снег. При условии наличия бесконечного времени и бесконечного объема данных
мы, возможно, сможем ответить на эти вопросы сами, но обсуждение их с другими
людьми будет намного быстрее и гораздо более информативно. И хотите верьте,
хотите нет, но эксперты есть почти в любой области. Например, быстрый поиск в
поисковой системе Google Scholar (https://oreil.ly/eGJzD) по фразе "seasonal ridership bike sharing" (сезонные количества прокатов велосипедов) возвращает результаты от сообщений в блогах до исследований, прошедших экспертную проверку, по
этому вопросу.
Что это означает в аспекте полноты данных? Существующие исследования или, что
еще лучше, разговор в реальном времени с экспертом в данной предметной области
(см. разд. "Эксперты предметной области" далее в главе 6) помогут вам решить,
какие факторы включить в свой анализ, и в результате, какой объем данных нужен
для полноты выбранного анализа.
Многомерность
Выпас данных реального мира означает сложность реального мира, где исследуемое
явление может быть подвержено влиянию громадного количества факторов. Одним
из примеров будет пример со станцией системы Citi Bike с "наибольшей загруженностью": кроме сезонности и общественного транспорта, роль также могли бы играть
физические особенности окружающей местности (https://www.sciencedirect.com/
science/article/abs/pii/S136192091731057X) или плотность станций. Если наши дан-
Понимание качества данных
|
111
ные для системы Citi Bike содержат только информацию о количестве поездок, начавшихся и завершившихся на определенной станции, тогда будет очень трудно
создать анализ, который сказал бы нам что-либо большее, кроме как "на этой станции было взято и возвращено наибольшее количество велосипедов" за данный период времени.
Но в случае многомерных данных они будут обладать множественными атрибутами или свойствами, связанными с каждой записью. Например, для архивных данных системы Citi Bike (https://citibikenyc.com/system-data) мы знаем, что у нас
есть все следующие данные (листинг 3.1), которые мы получили благодаря нашей
небольшой работе по выпасу данных в разд. "Отправляемся в путь с данными Citi
Bike" главы 2.
Листинг 3.1. Свойства архивных данных системы Citi Bike в нашем распоряжении
['tripduration', 'starttime', 'stoptime', 'start station id', 'start station
name', 'start station latitude', 'start station longitude', 'end station id',
'end station name', 'end station latitude', 'end station longitude', 'bikeid',
'usertype', 'birth year', 'gender']
Как видим, данные для каждой записанной поездки содержат 15 разных свойств,
любое число из которых мы могли бы выгодно использовать для более осмысленного или подробного метода для получения понимания, какая из станций системы
Citi Bike является наиболее "загруженной".
Атомарность
Атомарные данные имеют высокую гранулярность (т. е. степень разбиения); они
одновременно и точно измерены, и не собраны в сводку статистик или индикаторов. Как правило, суммарные индикаторы типа скоростей и средних значений не
всегда особенно хорошо подходят для дальнейшего анализа в результате потери
большого количества подробностей базовых данных. Например, арифметическое
среднее для обоих следующих множеств чисел равно 30: 20, 25, 30, 45 и 15, 20, 40,
45. Тогда как сводные статистические данные часто полезны при сравнивании различных наборов данных, они не предоставляют достаточной информации о базовой
структуре данных, чтобы быть полезными для поддержки дальнейшего анализа.
Достижимость
Независимо от того, как набор данных выглядит изначально, реальность такова, что
по-настоящему чистый, хорошо организованный и не содержащий ошибок набор
данных практически невозможен. Частично это обуславливается тем обстоятельством, что со временем некоторые вещи (например, стоимость доллара) просто подвергаются изменениям. В результате этого те, кто занимаются выпасом данных,
должны всегда учитывать, что некоторые характеристики качества данных могут
112
|
Глава 3
потребовать проверки и улучшения. К счастью, именно в этом отношении гибкость
языка Python и его поддержка многократного использования кода по-настоящему
проявляют себя, поскольку выполнение этих задач будет становиться легче и быстрее по мере повышения уровня наших навыков программирования с течением времени.
Непротиворечивость
Данные высокого уровня целостности должны быть непротиворечивыми в нескольких разных отношениях. Часто наиболее очевидный тип непротиворечивости
набора данных связан с периодичностью данных, собранных в течение времени.
Постоянны ли интервалы времени между отдельными записями? Есть ли разрывы?
Нерегулярные интервалы между записями данных важно исследовать (если их
нельзя устранить), потому что они могут быть первым индикатором нарушений или
различий в самом процессе сбора данных. Другой источник широкомасштабной
противоречивости данных обычно возникает во всех случаях, связанных с использованием текстовых полей данных, в частности, поля, содержащие имена или описания, почти неизбежно будут содержать разные написания какого-либо термина.
Даже поля, задача стандартизации значений которых с виду может казаться прямолинейной, могут содержать подробности разного уровня. Например, поле "zip code"
(почтовый индекс) может содержать как обычные пятицифровые значения, так и
более точные "Zip+4"8 значения.
Другие типы непротиворечивости могут быть менее очевидными, но не менее важными. Например, единицы измерения должны быть одинаковыми во всем наборе
данных. Тогда как это может казаться очевидным, на самом деле обратная ситуация
может иметь место намного чаще, чем можно подумать сначала. Например, предположим, что мы рассматриваем данные о стоимости яблок на протяжении периода
десяти лет. Конечно же, все цены в данных могут быть записаны в долларах, но
стоимость самого доллара будет постоянно меняться по причине инфляции. Также
большинство из нас знают, что градусы Цельсия и Фаренгейта имеют разные значения, но это не единственный такой случай, так как имперская пинта содержит
приблизительно 568 миллилитров, тогда как американская — около 473 миллилитров.
Вполне вероятно, что и рассказы о невысоком росте Наполеона Бонапарта могут
быть основаны на противоречивости размера французского дюйма XIX столетия
(около 2,71 см) и его сегодняшней версии (около 2,54 см) (https://britannica.com/
story/was-napoleon-short).
Решением таких противоречивостей будет нормализация данных, прежде чем выполнять какие-либо сравнения или анализы. В большинстве случаев это вопрос
простой арифметики: нам просто нужно выбрать один вариант единицы, к которому мы преобразуем все другие (еще один важный момент для документации). Этот
подход применим даже с денежными единицами, работу с которыми аналитики
8
То есть 5-цифровое основное значение плюс дополнительное 4-цифровое значение. Например,
12345-6789. — Примеч. пер.
Понимание качества данных
|
113
часто начинают с преобразования к "настоящим" (т. е. индексированным с учетом
инфляции) долларам, используя какой-либо год в качестве эталона. Например,
сравнить реальную стоимость (в долларах) минимальной федеральной почасовой
оплаты в 2009 году (когда она была повышена последний раз) со стоимостью в
2021 году можно, используя калькулятор инфляции, например, на веб-сайте Бюро
трудовой статистики США (https://bls.gov/data/inflation_calculator.htm9). Мы увидим, что реальная стоимость часовой оплаты, в 2009 году составляющая $7,25, будет эквивалентна всего лишь $5,72 в 2021 году.
Кто на самом верху в хит-листе?
Поставщики данных часто не уточняют, какие единицы они используют или
были ли их данные нормализованы. Например, веб-сайт Box Office Mojo предоставляет текущую сумму сборов мировых фильмов с самыми большими сборами (https://boxofficemojo.com/chart/ww_top_lifetime_gross/?area=XWW), но
при этом неясно, была ли выполнена нормализация указанных сумм к текущим
долларам. Если не была, то очевидно, что выполнение такой нормализации вызвало бы определенную перестановку верхних позиций списка.
Ясность
Подобно коду Python, наши данные и их метки, в идеале, должны легко читаться и
интерпретироваться. Но в реальности описатели полей и даже наборов данных иногда могут быть не намного понятнее, чем таинственные коды, требующие постоянной сверки со словарем данных или иными ресурсами. Частично поэтому этот тип
целостности данных почти всегда является не предшественником, а продуктом выпаса данных определенной степени.
Например, есть ли какая-либо логика в том факте, что для демографических и жилищных оценок по результатам обследования американских общин выполненного
Бюро переписи населения США присвоен код DP05 (https://oreil.ly/kD48L)? Возможно. Но его значение очевидно для непостоянного пользователя данными Бюро
ничуть не больше, чем значение заголовка столбца DP05_0001E10. При этом архив с
таблицей этого обследования Бюро содержит множественные файлы, которые могут помочь вычислить значения имен файлов и заголовков столбцов. Разработка
ясного набора данных с высоким уровнем целостности может потребовать значительного объема работы по переименованию, переформатированию и переиндексации, особенно когда мы имеем дело с данными, созданными какой-либо правитель9
Ссылка открывает не сам калькулятор, а главную страницу сайта. Чтобы добраться до калькулятора,
на главной странице нужно выбрать Data Tools и в строке меню вверху страницы, в всплывающем
окне в столбце Data Retrieval Tools выбрать Top Picks, One Screen, Multi-Screen, Maps, and Calculators, далее в открывшемся новом окне под заголовком Databases, Tables & Calculators by Subject
щелкнуть по ссылке Calculators, a затем выбрать Inflation.
10
Кстати, это означает приблизительную оценку общей численности населения.
114
|
Глава 3
ственной организацией. И как всегда, критически важно в ходе работы документировать ваши источники информации и все переименования процессов.
Размерностно структурированные
Размерностно структурированные данные содержат поля, сгруппированные в практичные категории или дополнительно помеченные такими категориями, как, например, географический регион, год или язык. Такие свойства часто предоставляют
точки быстрого входа как для дополнения данных (эта тема рассматривается в разд.
"Аугментация данных" далее в этой главе), так и для анализа данных, поскольку
они понижают уровень усилий для операций корреляции и агрегации, которые нам
нужно выполнять самостоятельно. Эти свойства также могут служить указателями
тех аспектов, которые создатель данных посчитал важным записать.
Например, размерности данных системы Citi Bike содержат такие аспекты, как тип
учетной записи, по которой были арендованы велосипеды, — Subscriber или Customer, а также год рождения и пол владельца учетной записи. На этом основании
можно предполагать, что разработчики данных считали, что эти свойства могли бы
предоставить полезные знания о поездках и использовании велосипедов системы Citi
Bike. Но, как мы увидим в главе 7, они решили не включать в данные какого бы то ни
было типа указателя "будний день/выходной", что означает, что если нам потребуется эта размерность, нам будет нужно получить ее самим.
Декодирование данных
Хотя удобочитаемые имена файлов и заголовки столбцов, несомненно, полезны, наличие закодированных описаний данных не всегда плохо. Как мы увидим далее, создание надежных, высококачественных наборов данных иногда
требует объединения двух или больше источников данных. В таких случаях
может быть полезным использование стандартизованных, хотя иногда и непонятных кодов. Например, на веб-сайте (https://data.nysed.gov/) Департамента
образования штата Нью-Йорк предоставляются профили отдельных школ, и
для каждой из них также предоставляется несколько идентификационных номеров. Сами по себе эти идентификационные номера не содержат особой информации, но, выполнив в Интернете поиск по ним, можно получить ссылки
на другие документы и наборы данных, что будет способствовать упорядочиванию наборов данных из множественных источников с более высокой степенью достоверности.
Улучшение качества данных
Как упоминалось ранее, многие аспекты качества данных являются продуктом процесса выпаса данных, связано это с согласованием и нормализацией единиц измерения, уточнением значений неясных меток данных или поиском дополнительной
справочной информации о репрезентативности вашего набора данных. Одной из со-
Понимание качества данных
|
115
ставляющих, иллюстрируемых этим обстоятельством, является то, что в реальном
мире обеспечение качества данных, по крайней мере, в некоторой степени является
результатом множественных, повторяющихся процессов выпаса данных. Тогда как
эти фазы процесса выпаса данных могут обозначаться разными терминами, я обычно
называю их очисткой данных и аугментацией (дополнением) данных.
Очистка данных
В действительности очистка данных, по двум причинам, — это не столько отдельный шаг в процессе выпаса данных, сколько постоянная деятельность, сопровождающая все остальные шаги. Во-первых, большинство данных при их получении не
являются чистыми, а во-вторых, в какой именно очистке нуждается набор данных
(или часть его), часто раскрывается постепенно в процессе работы. На высшем
уровне чистые данные можно обобщить как не содержащие ошибок или опечаток.
Например, разные единицы измерения, разные варианты написания слов или терминов, а также поля, не разделенные должным образом и содержащие невозможные значения или вообще никаких значений. И хотя многие из этих недостатков
можно, по крайней мере, относительно легко распознать (хотя не всегда исправить), более глубокие проблемы с данными могут остаться. Изменения измерений,
ошибки вычислений и прочие упущения, особенно в данных, генерируемых системой, часто не проявляются до тех пор, пока не будет выполнен анализ какого-либо
уровня, и реальность данных была удостоверена людьми, имеющими значительные
экспертные знания и/или непосредственный опыт с предметом.
Итеративная природа процесса очистки данных служит примером, почему выпас
данных является циклом, а не линейной последовательностью шагов. В частности,
по мере того как в процессе работы проявляется дополнительная информация о
данных и углубляется ваше понимание их взаимосвязи с окружающим миром, может оказаться, что необходимо возвратиться к ранее выполненной работе и повторить операцию с определенными аспектами данных или же откорректировать эти
аспекты. Конечно же, это всего лишь еще одна причина, по которой документирование своей работы по выпасу данных является таким важным: документацию
можно использовать, чтобы быстро определить, где и как были выполнены какиелибо изменения или обновления, и согласовать их с вашими текущими знаниями о
данных. Без надежной документации вы можете быстро обнаружить, что необходимо все начинать с начала.
Аугментация данных
Аугментация набора данных — это процесс расширения или улучшения данных,
обычно объединения их с другими наборами данных. Это настоящая природа
"больших данных" в XXI столетии11. Используя общие качества разных наборов
11
Обсуждение вопроса больших данных см. в статье Critical Questions for Big Data, авторы danah boyd
и Kate Crawford, https://tandfonline.com/doi/full/10.1080/1369118X.2012.678878).
116
|
Глава 3
данных, можно собрать вместе данные из разных источников, чтобы получить более подробную картину интересующего нас явления, заполняя пробелы, предоставляя подтверждения или добавляя контекстуальные данные, помогающие нам лучше
оценить соответствие данных. В рассмотренном ранее примере с выбором "наилучшей" школы это может означать использование кодов школ, чтобы свести вместе несколько типов данных, собранных разными сущностями, например, результаты стандартизованных тестов уровня штатов и информацию о локальных зданиях.
Совмещая эффективное исследование и выпас данных, аугментация данных может
способствовать наращиванию качества данных и в получении ответов на вопросы
со слишком многими нюансами, чтобы на них можно было ответить на основе любого одного набора данных.
Подобно очистке данных, возможности для аугментации данных могут возникать
практически на любом этапе процесса выпаса данных. В то же самое время каждый
новый добавляемый набор данных будет порождать свой собственный процесс выпаса данных. Это означает, что в отличие от очистки данных, аугментация данных
не имеет окончательного "конечного состояния", т. е. всегда будет возможность
добавить еще один набор данных. Это еще одна причина, почему для процесса выпаса данных так важно в его начале сформулировать конкретный и лаконичный
вопрос о его цели: при отсутствии ясно выраженной формулировки о том, что мы
пытаемся исследовать, слишком легко исчерпать время или другие ресурсы, отведенные на решение нашей задачи. Но есть и хорошая новость: если вы должным образом вели свой дневник данных, вы никогда не упустите из виду перспективный
набор данных для возможного использования в каком-либо будущем проекте.
Заключение
Поскольку до настоящего времени у нас имеется лишь ограниченный опыт практической работы с реальными данными, многие из рассмотренных в этой главе понятий могут выглядеть несколько абстрактными. Но не стоит волноваться из-за этого.
Вскоре у нас будет приличный объем практической работы. В последующих главах
мы будем выполнять выпас данных, поступающих в разных форматах и из разных
источников, что предоставит нам возможность увидеть изнутри, как различные характеристики качества данных влияют на принимаемые нами решения при получении, оценке, очистке, анализе и предоставлении данных. Можете быть уверенными,
что по завершению прочтения этой книги вы сможете создавать содержательные,
точные и убедительные анализы и визуализации данных, чтобы поделиться своими
знаниями об окружающем нас мире.
В следующей главе мы начнем этот процесс с рассмотрения, как преобразовывать
данные в самых разнообразных форматах в структуру, которая позволит выполнять
требуемые нам очистку, дополнение и анализ. Ну-с, приступим.
ГЛАВА 4
Работа с файловыми и канальными
данными на языке Python
В главе 3 мы рассмотрели характеристики, способствующие улучшению качества
данных: от аспектов целостности данных, таких как их полнота, непротиворечивость и ясность, до качеств соответствия данных, таких как надежность, действительность и репрезентативность. Мы обсудили потребность как в очистке, так и в
стандартизации данных, а также необходимость в дополнении данных посредством
их объединения с другими наборами данных. Но как все эти теоретические понятия
можно реализовать на практике?
Очевидно, что начинать оценивать качество набора данных невозможно без предварительного просмотра его содержимого, но как часто бывает — легче сказать,
чем сделать. В течение десятилетий выпас данных был высокоспециализированным
занятием, понуждая компании и организации к созданию целого ряда разных (и
иногда закрытых) форматов цифровых данных, предназначенных для удовлетворения своих конкретных требований. Часто эти форматы используют свои собственные расширения файлов, с некоторыми из которых вам, наверное, приходилось
встречаться. Например, расширения файлов xls, csv, dbf и spss обычно обозначают
файлы данных1. Хотя конкретные структуры и прочие детали этих форматов отличаются друг от друга, все они относятся к типу, который я бы назвала основанными
на файлах или просто файловыми, имея в виду, что они содержат (более или менее)
архивные данные в статических файлах, которые можно загрузить из базы данных,
отправить по электронной почте или получить доступ к ним через файлообменные
сайты. Стоит отметить, что файловый набор данных будет содержать ту же самую
информацию что сегодня, что неделю, что месяц или год спустя.
В настоящее время такие файловые форматы контрастируют с форматами и интерфейсами данных, которые в течение последних 20 лет возникли параллельно с вебсервисами. В настоящее время интернет-данные используются для предоставления
обширного диапазона информации и услуг, от новостей до погодных сведений и
социальных сетей. Эти источники информации канального типа имеют свои собственные уникальные форматы и структуры. Этот тип данных реального времени
1
Сравните эти расширения с другими известными вам, например, mp4 или png, которые обозначают
музыку и изображения, соответственно.
118
|
Глава 4
обозначается расширениями типа xml, json и rss, и для доступа к ним часто требуется использование специализированных API-интерфейсов (Application Programming Interface — интерфейс прикладного программирования). В отличие от файловых форматов, при обращении к адресу или "эндпойту" канальных данных через
API-интерфейс они всегда предоставляют самые последние доступные данных, которые могут измениться за последние несколько дней, часов или даже секунд.
Конечно же, эти определения не идеальные. Многие организации (в особенности
правительственные учреждения) предоставляют для загрузки файловые данные,
заменяя эти файлы на новые (с теми же самыми именами) при каждом обновлении
источника данных. В то же самое время канальные данные можно загрузить и сохранить в обычном файле для дальнейшего использования, но их онлайновый источник обычно не предоставляет доступа к более старым версиям. Несмотря на это,
в большинстве случаев можно использовать высокоуровневые различия между
файловыми и канальными форматами данных, чтобы выбрать наиболее подходящий источник данных для конкретного проекта по обработке данных.
Как узнать, какой источник данных нам требуется, файлового или канального типа?
В большинстве случаев в этом вопросе у вас не будет особого выбора. Например,
компании социальных сетей предоставляют доступ к своим каналам данных посредством своих API-интерфейсов, но обычно не предоставляют ретроспективных
(прошлых) данных. Другие типы данных (особенно данные, синтезированные из
других источников или в значительной степени пересмотренные перед выпуском)
могут быть с большей степенью вероятности предоставлены в файловых форматах.
При наличии выбора между файловым и канальным форматом выбор между ними,
по сути, будет зависеть от природы вашего вопроса по выпасу данных: если он завязан на самых последних доступных данных, тогда, наверное, предпочтение следует отдать данным канального формата. Но если вас интересуют тренды, то наилучшим вариантом будут файловые данные, поскольку они с большей вероятностью будут содержать информацию, собранную в течение определенного периода
времени. Однако при наличии доступа к обоим форматам, нет никакой гарантии,
что они будут содержать одинаковые поля, в результате чего вам опять придется
принимать решение, какой из них выбрать для дальнейшей работы.
Один источник данных, два подхода
Мы уже работали с источником данных, доступным как в файловом, так и в
канальном формате, — это данные системы Citi Bike, представленные в главе
2. В частности, в разд. "Отправляемся в путь с данными Citi Bike" этой главы
мы использовали подмножество файловых данных для изучения, сколько было
предпринято поездок клиентами этой системы типа Subscriber и Customer
1 сентября 2020 г. Для вопросов выпаса данных касательно трендов по количеству поездок и деятельности станций эти ретроспективные файлы являются
неоценимым ресурсом.
Работа с файловыми и канальными данными на языке Python
|
119
В то же самое время, если бы нам требовалась информация о количестве доступных для аренды велосипедов на какой-либо конкретной станции Citi Bike именно в данный момент, эти файловые данные были бы для нас просто бесполезными. Но эту информацию можно получить посредством канала данных формата
json этой системы (https://gbfs.citibikenyc.com/gbfs/en/station_status.json). Объем текста, который отображается в окне браузера при открытии этой ссылки,
не особенно удобочитаем2 для пользователя, но он содержит информацию, отсутствующую в файловых данных, например количество велосипедов и парковочных мест, доступных в данный момент времени, а также количество неисправных велосипедов и т. п. Для того чтобы определить физическое местонахождение станции с определенным идентификационным номером station_id,
нам нужно дополнить эти данные информацией или с другого канала данных
системы Citi Bike, или файловыми данными, которые мы уже использовали.
Иными словами, во многих случаях имеет смысл, чтобы источник данных содержал данные как в файловом, так и в канальном формате. Какой из этих
форматов больше подойдет, будет зависеть (как и всегда) от стоящего перед
вами конкретного вопроса по выпасу данных.
На протяжении этой главы мы будем прорабатывать практические примеры по выпасу данных из нескольких источников с наиболее распространенными файловыми
и канальными форматами данных, ставя перед собой целью облегчение их рассмотрения, очистки, дополнения и анализа. Мы также рассмотрим некоторые форматы данных, более трудные для первичной обработки, с которыми вам, возможно,
понадобится работать в силу необходимости. В ходе этих процессов мы будем в
значительной степени полагаться на подборку библиотек, разработанных сообществом Python для этих целей, включая специализированные библиотеки и программы для обработки обширного диапазона документов — от электронных таблиц до
изображений. По завершении этой работы вы приобретете навыки и разработаете
примеры сценариев, необходимые для решения огромной разновидности проектов
по выпасу данных, тем самым прокладывая себе дорогу к проектам следующего
уровня.
Структурированные и неструктурированные данные
Прежде чем приступать к написанию кода и выпасу данных, я хочу вкратце обсудить один важный атрибут источников данных, который может повлиять на направление (и скорость) ваших проектов по выпасу данных, — наличие или отсутствие структуризации.
Цель большинства проектов по выпасу данных — генерировать знания и, часто,
использовать данные для улучшения принимаемых решений. Но на принятие решений обычно отводится определенный объем времени, поэтому наша работа с
2
Хотя вскоре мы научимся, как его обрабатывать.
120
|
Глава 4
данными также требует сбалансированности разных аспектов. Например, вместо
того, чтобы продолжать обработку или поиск набора данных, чтобы получить "идеальный" набор данных, можно объединить два или три менее идеальных набора,
чтобы создать действительную аппроксимацию исследуемого явления. При условии, что мы можем получить требуемую эффективность, не жертвуя слишком много времени в отношении качества данных, такое улучшение нашей работы с данными может также повысить ее результативность.
Один из самых простых способов повысить эффективность выпаса данных — найти такие форматы данных, к которым язык Python и другие вычислительные инструменты могут легко обращаться и работать с ними. Хотя достижения в области
компьютерного зрения, обработки текстов на естественных языках и машинном
обучении облегчили для компьютеров задачу анализа данных, независимо от их
базовой структуры или формата, факт остается фактом — структурированные,
машиночитаемые данные остаются наиболее простым типом данных для обработки. На самом деле, тогда как в качестве источника данных может быть использован
обширный ряд явлений или объектов — от интервью до изображений и книжных
текстов — для многих из нас "данные" часто представляются как структурированные, числовые данные больше, чем что-либо другое.
Что мы имеем в виду под "машиночитаемые"?
Хотите верьте, хотите нет, благодаря Закону об основах доказательной политики
(https://govinfo.gov/content/pkg/PLAW-115publ435/html/PLAW-115publ435.htm),
который вступил в силу в начале 2019 г., в США существует юридическое определение машиночитаемых данных. Согласно этому закону, машиночитаемыми данными являются следующие:
данные в формате, который может легко обрабатываться компьютером
без вмешательства человека, в то же самое время обеспечивая сохранность семантического значения.
Полезно, не так ли? И хотя это определение может выглядеть несколько формальным, по мере продолжения работы с данными (особенно, если вы запрашиваете их у правительственных учреждений) вы начнете осознавать его важность. В частности, когда речь заходит о работе с неструктурированными данными (например, текстовыми документами), существует большая разница
между данными, созданными программой текстового редактора, и данными,
полученными в результате сканирования бумажного документа. Тогда как и те
и другие являются машиночитаемыми в том смысле, что их можно просматривать на компьютере, желаем вам удачи в ваших попытках получить программный доступ к тексту отсканированного документа3. Поэтому ясное (юридическое!) определение значения "машиночитаемые" на самом деле очень даже полезно.
3
Как упростить эту задачу, мы рассмотрим в разд. "Выпас PDF-данных, используя Python" далее в
этой главе.
Работа с файловыми и канальными данными на языке Python
|
121
Структурированные данные — это любой тип данных, организованных и классифицированных каким-либо образом в какой-либо вариант записей и полей. В случае данных файловых форматов это обычно строки и столбцы, а для канальных
форматов это по сути списки объектов или словари.
В противоположность им, неструктурированные данные могут состоять из смеси
данных разных типов, совмещая текст, числа и даже фотографии или иные изображения. Например, содержимое журнала или романа или звуковой сигнал песни
обычно будут считаться неструктурированными данными.
Если вы сейчас думаете: "Погодите, но ведь романы имеют структуру! Как насчет
главы?" — тогда примите мои поздравления: вы уже думаете как специалист по
выпасу данных. Мы можем создавать данные почти обо всем, собирая информацию
об окружающем нас мире и структурируя ее4.
Ведь на самом деле все данные так и создаются: наборы данных, которые мы получаем посредством файлов и каналов, являются продуктом чьих-то решений о том,
как собирать и организовывать информацию. Иными словами, всегда есть несколько способов организации информации, но выбранная структура влияет на то, как
эту информацию анализировать. Вот почему предполагать, что данные могут каким-то образом быть "объективными", будет немного неверным, ведь, в конце концов, данные являются продуктом (который по своей природе субъективен) человеческих решений.
Например, попробуйте провести мини-эксперимент: подумайте, как вы организовываете коллекцию каких-либо предметов (например, музыкальные записи, книги,
игры или сорта чая). Потом спросите одного из своих друзей, как он организовывают свою коллекцию таких же предметов. Делаете ли вы это оба одинаковым способом или нет? Если нет, то чей способ "лучше"? Теперь спросите об этом еще кого-либо. И даже если системы организации, например, музыкальных записей, используемые вами и вашими друзьями, могут иметь некоторые похожие аспекты, я
бы очень удивилась, если бы любые две из них были в точности одинаковыми.
Иными словами, каждый из вас упорядочивает свои музыкальные записи слегка подругому, и при этом полностью убежден, что его система "наилучшая". И так оно и
есть. Для каждого его система является наилучшей именно для него.
Если это напоминает вам наше обсуждение в разд. "Как? И для кого?" в главе 3, то
это не случайно, поскольку результатом ваших усилий на получение ответа на ваш
вопрос для выпаса данных и сам ответ в итоге будет, как вы могли догадаться, другой набор данных, отражающий ваши интересы и приоритеты. Этот набор также
будет организован и структурирован, в результате чего работать с ним определенными способами будет легче, чем другими. Но основная идея здесь состоит не в
4
В теории вычислительных машин термины данные и информация применяются точно противоположным образом: данные — это сырые факты, собранные об окружающем нас мире, а информация
означает значащий конечный продукт процесса их организации и структурирования. Но в последние
годы, когда разговоры о "больших данных" доминируют во многих областях, моя интерпретация
данных стала более широко употребляемой, поэтому для ясности я буду продолжать использовать ее в
этой книге.
122
|
Глава 4
том, что любой из этих способов может быть правильным или неправильным, а в
том, что для каждого из них нужно будет идти на какие-либо компромиссы. Идентификация и подтверждение этих компромиссов является ключевой частью честного и ответственного использования данных.
Итак, ключевой компромисс при использовании структурированных данных состоит в необходимости полагаться на решения и приоритеты кого-то другого в организации базисной информации. Очевидно, что это может быть хорошим, или даже замечательным, обстоятельством, если структуризация этих данных была выполнена посредством открытого и прозрачного процесса с вовлечением хорошо
квалифицированных экспертов. Вдумчиво применяемые структуры данных могут
дать нам представление о предмете, о котором мы в противном случае могли бы
знать очень мало или вообще ничего. Но с другой стороны, существует и вероятность того, что мы унаследуем чужие предвзятые и плохо продуманные решения.
Конечно же, неструктурированные данные дают нам полную свободу организовывать информацию в структуры данных, которые наилучшим образом отвечают нашим потребностям. Неудивительно, что это вынуждает нас взять на себя ответственность за использование надежного процесса обеспечения качества данных, который может быть как сложным, так и трудоемким.
Как можно знать наперед, является ли определенный набор данных структурированным или неструктурированным? В этом вопросе нам определенно могут помочь
расширения файлов. Канальные форматы данных имеют хоть какую-нибудь структуру, даже если они содержат фрагменты произвольного текста типа сообщений
социальных сетей. Поэтому данные в файлах с расширениями .json, .xml, .rss или
.atom имеют, как минимум, структуру типа "записи и поля". Мы это рассмотрим в
разд. "Канальные данные — интерактивные обновления через Интернет" далее в
этой главе. Данные в файлах с расширениями .csv, .tsv, .txt, .xls(x) или .ods, как мы
увидим в следующем разделе, обычно имеют табличную структуру, со строками и
столбцами. А по-настоящему неструктурированные данные, скорее всего, будут в
документах с расширением .doc(x) или .pdf.
Интеллектуальный поиск конкретных типов данных
Поскольку при поиске информации большинство из нас первым делом обращается к поисковым системам Интернета, то разве не было бы здорово использовать мощь этих поисковых систем для более эффективного поиска требуемых нам источников (и форматов) данных?
Обычно поиск в Сети выполняется по ключевым словам, фразам или предложениям, но лишь небольшая корректировка этого подхода может существенно
изменить поиск полезных форматов данных. В частности, добавляя к тексту
поиска один или несколько операторов поиска, можно дать поисковой системе
указание возвращать результаты только с конкретных веб-сайтов или доменов
(например, .gov) или даже результаты только с конкретными расширениями
файла.
Работа с файловыми и канальными данными на языке Python
|
123
Например:
Поисковые термины или ключевые слова — filetype: .csv
Расширение файла .csv можно заменить любым другим требуемым вам расширением, включая .tsv, .txt, .pdf или даже .jpg, .mp3 и т. п. В результате будут
возвращены файлы с указанным расширением, которые также содержат указанные поисковые термины или ключевые слова.
Поисковые термины или ключевые слова — site: oreilly.com
Этот поиск возвратит результаты, содержащие указанные поисковые термины
или ключевые слова, только с веб-сайта oreilly.com. Это особенно удобно при
поиске данных, созданных или опубликованных определенной организацией.
Обратите внимание на то, что этот подход можно использовать для поиска в
рамках всего домена высшего уровня, например .gov или .co.uk.
Поисковые термины или ключевые слова — inurl: https
Возвращает результаты только с защищенных веб-сайтов.
Поисковые термины или ключевые слова — прочие термины или ключевые
слова
Хотя это не поможет вам найти конкретные файлы, использование дефиса (-)
может стать отличным способом сфокусировать поиск информации, когда вы
хотите специально исключить результаты, которые ассоциировались бы с основным поисковым термином. Чтобы увидеть этот тип поиска в действии,
сравните результаты поиска по ключевым словам steve jobs с результатами
поиска по ключевым словам steve jobs – apple.
Конечно же, использование поисковой системы — это всего лишь один из
многих возможных способов поиска данных. С дополнительными идеями по
вопросу поиска требуемых данных можно ознакомиться в приложении В.
Теперь, когда у нас есть солидные знания о разных типах источников данных, с которыми нам, скорее всего, придется работать, а также некоторое понятие того, как
их искать, можно приступать к выпасу данных.
Работа со структурированными данными
С самого начала цифровых вычислений таблица была одним из наиболее распространенных способов структуризации данных. Даже в настоящее время многие из
наиболее распространенных и удобных для обработки форматов данных представляют собой не более чем таблицы или наборы таблиц. Более того, мы уже работали
с одним из очень распространенных табличных форматов данных в главе 2, а именно, с форматом .csv (comma-separated values — значения, разделенные запятыми).
124
|
Глава 4
Файловые табличные форматы данных
Все табличные форматы данных, с которыми нам приходится иметь дело, являются
образцами так называемых файлов с разделенными данными, в которых каждая запись занимает отдельную строку, а поля или столбцы значений данных в строках
разделены определенным текстовым символом. Часто расширение файла набора
данных будет содержать признак текстового символа, используемого в качестве
разделителя. Например, расширение файла .csv означает comma-separated values,
поскольку в этих файлах в качестве разделителя используется запятая. А расширение файла .tsv означает tab-separated values, поскольку в них значения в строке разделяются символами табуляции. Далее приводится список расширений файлов,
обычно содержащих разделенные данные, и их краткое описание:
.csv
Как уже упоминалось ранее, .csv означает comma-separated values, т. е. значения,
разделенные запятыми. Эти файлы являются одними из наиболее распространенных среди файлов данных с табличной структурой, с которыми вам придется
работать. Почти все программные системы, обрабатывающие табличные данные
(например, правительственные или корпоративные системы данных, программы
электронных таблиц и даже специализированные программы по обработке коммерческих данных), могут выдавать данные в формате .csv, и, как мы видели в
главе 2, для работы с этим типом данных, используя язык Python, существуют
удобные библиотеки.
.tsv
Опять же, как упоминалось ранее, расширение .tsv означает tab-separated values,
т. е. значения, разделенные символами табуляции. Файлы с таким форматом данных используются уже достаточно давно, но данное описательное расширение начало широко употребляться сравнительно недавно. Поставщики данных часто не
объясняют, почему они используют один разделитель, а не другой, файлы значений, разделенных символом табуляции, широко используются для наборов данных, чьи значения должны содержать запятые, например почтовые адреса.
.txt
Структурированные файлы данных с этим расширением — часто просто замаскированные файлы .tsv. Расширение .txt часто использовалось в старых системах
данных для обозначения файлов, содержащих значения, разделенные символом
табуляции. Как мы увидим в проработанных примерах, следующих далее, прежде чем приступать к разработке кода, имеет смысл открыть и просмотреть в базовом текстовом редакторе (или редакторе кода наподобие редактора Atom) любой файл, данные которого вы намереваетесь обрабатывать. Самостоятельный
просмотр содержимого файла — это единственный надежный способ узнать, какие в нем используются разделители.
.xls(x)
Это расширение присваивается файлам электронных таблиц, создаваемых программой Microsoft Excel. Поскольку кроме формул, форматирования и прочих
Работа с файловыми и канальными данными на языке Python
|
125
функциональностей эти файлы могут содержать множественные "листы", для
хранения одного и того же объема данных им требуется больше памяти, чем для
файлов других типов. У них также есть и другие ограничения (например, они
могут содержать только определенное количество строк), что может привести к
неприятным последствиям для целостности вашего набора данных.
.ods
Означает open-document spreadsheet files, или файлы электронных таблиц формата "открытый документ". Это расширение установлено по умолчанию для
файлов электронных таблиц, создаваемых рядом пакетов офисных приложений
с отрытым исходным кодом, например LibreOffice (https://libreoffice.org/) и
OpenOffice (https://openoffice.org/download/index.html). Данные файлы имеют
возможности и ограничения наподобие файлов .xls(x).
Прежде чем углубляться в рассмотрение работы с каждым из этих типов файлов на
языке Python, стоит уделить немного времени на обсуждение того, когда стоит отдать предпочтение работе с табличными данными и где их искать в таком случае.
Когда лучше использовать табличные данные
В большинстве случаев у нас нет большого выбора касательно формата наших исходных данных. Более того, необходимость вообще заниматься выпасом данных в
большой степени вызвана несоответствием имеющихся у нас данных нашим требованиям. Однако все равно неплохо бы знать, с каким форматом данных вы предпочитаете работать, чтобы на этом основывать первоначальный поиск данных.
В разд. "Структурированные и неструктурированные данные" ранее в этой главе
мы обсудили преимущества и ограничения структурированных данных, и теперь
мы знаем, что табличный формат данных является одним из самых старых и наиболее распространенных форматов машиночитаемых данных. Это означает что на
протяжении многих лет исходные данные разных форматов были втиснуты в таблицы, хотя при этом они не обязательно могут хорошо подходить для представления в табличной форме. Тем не менее этот формат может быть полезным для получения ответов на вопросы о трендах и закономерностях в течение времени. Например, в упражнении с использованием данных системы Citi Bike в главе 2 мы
исследовали количество клиентов типа Customers и Subscribers, которые арендовали велосипеды в этой системе на протяжении одного месяца. Если бы мы хотели,
то мы могли бы выполнить те же самые вычисления для каждого доступного месяца аренды, чтобы получить знания о каких-либо закономерностях в соотношении
этих двух величин в течение всего этого времени.
Конечно же, табличный формат данных, как правило, не лучший вариант для данных реального времени или данных, в которых не все возможные измерения содержат одинаковые возможные значения. Для этих типов данных лучше подходят
канальные форматы данных, которые мы рассмотрим в разд. "Канальные данные —
интерактивные обновления через Интернет" далее в этой главе.
126
|
Глава 4
Где искать табличные данные
Поскольку бóльшая часть машиночитаемых данных находится в табличном формате, этот формат один из самых легких для поиска. Электронные таблицы широко
используются практически во всех отраслях, и большое количество информационных систем правительственных и коммерческих организаций полагаются на программы, которые организовывают данные таким способом. Почти при каждом запросе данных у эксперта или организации эти данные будут, с большой долей вероятности, предоставлены в табличном формате. Это также справедливо и для
большинства онлайновых порталов открытых данных и веб-сайтов обмена данными. Как рассматривалось во врезке "Интеллектуальный поиск конкретных типов
данных" ранее в главе, поиск табличных данных можно осуществлять и при помощи поисковых систем, если вы знаете, как это делать.
Выпас табличных данных
посредством языка Python
Чтобы проиллюстрировать, насколько просто работать с табличными данными в
Python, мы рассмотрим несколько примеров чтения данных из файлов всех типов,
упомянутых ранее в этом разделе, плюс, для полноты картины, из нескольких других типов. В последующих главах мы рассмотрим более обширную обработку,
включающую очистку, преобразование и оценку качества данных, но на данном
этапе мы ограничимся простыми операциями доступа к данным в файлах каждого
типа и взаимодействия с ними посредством языка Python.
Чтение данных из файлов CSV
В случае если вы подзабыли материал главы 2, освежим вашу память, как считывать данные из файла .csv, используя образец данных из набора данных системы
Citi Bike. Соответствующий код приводится в листинге 4.1. Как всегда, в комментариях я даю описание работы программы, а также ссылки на файлы исходного кода. Поскольку мы уже работали с файлами этого типа раньше, сейчас только рассмотрим, как отобразить первые несколько строк данных, чтобы посмотреть, что
они из себя представляют.
Листинг 4.1. Сценарий csv_parsing.py
#
#
#
#
#
#
Простой пример чтения данных из файла .csv посредством Python,
используя библиотеку csv.
Исходные данные были взяты из данных системы CitiBike:
https://drive.google.com/file/d/17b461NhSjf_akFWvjgNXQfqgh9iFxCu_/
которые находятся по следующей ссылке
https://s3.amazonaws.com/tripdata/index.html
# Импортируем библиотеку csv
import csv
Работа с файловыми и канальными данными на языке Python
|
127
# Открываем файл 202009CitibikeTripdataExample.csv в режиме чтения (r)
# Этот файл должен находиться в той же самой папке, что и файл автономного сценария
# или блокнота
source_file = open("202009CitibikeTripdataExample.csv","r")
# Передаем наш исходный файл source_file как аргумент (ингредиент) методу (рецепту)
# DictReader библиотеки csv.
# Сохраняем результат в переменной citibike_reader
citibike_reader = csv.DictReader(source_file)
# Метод DictReader добавил в наши данные некоторую полезную информацию.
# Например, свойство fieldnames позволяет нам обращаться ко всем значениям в первой
# строке (или заголовке)
print(citibike_reader.fieldnames)
# Отображаем первые пять строк данных
for i in range(0,5):
print (next(citibike_reader))
Это наша библиотека — рабочая лошадка для работы с табличными данными.
Встроенная функция open() принимает в качестве параметров имя файла и режим
работы с ним. В данном примере целевой файл (202009CltlbikeTripdataExample.csv)
должен находиться в той же самой папке, что и файл автономного сценария или
блокнота. Значение параметра режима может быть r (read — чтение) или w (write —
запись).
Отображая значения cltlbike_reader.fieldnames, мы можем видеть, что точным
именем столбца User Type (тип пользователя) является usertype.
Функция range() предоставляет нам возможность исполнять определенный
фрагмент кода заданное количество раз, начиная со значения первого аргумента и
заканчивая на единицу меньшим значением второго аргумента. Например, код, выделенный отступом в этой строке, будет исполнен пять раз, со значением счетчика i,
инкрементирующимся от 0 до 4, т. е. принимающим последовательные значения 0,
1, 2, 3, 4. Дополнительная информация по функции range() приводится во врезке
"Добавляем итераторы: функция range()" далее в этом разделе.
Результаты исполнения этого сценария должны выглядеть наподобие показанных в
листинге 4.2.
Листинг 4.2. Результат исполнения сценария csv_parsing.py
['tripduration', 'starttime', 'StartDate', 'stoptime', 'start station id',
'start station name', 'start station latitude', 'start station longitude', 'end
station id', 'end station name', 'end station latitude', 'end station
longitude', 'bikeid', 'usertype', 'birth year', 'gender']
{'tripduration': '4225', 'starttime': '2020-09-01 00:00:01.0430', 'StartDate':
'2020-09-01', 'stoptime': '2020-09-01 01:10:26.6350', 'start station id':
128
|
Глава 4
'3508', 'start station name': 'St Nicholas Ave & Manhattan Ave', 'start station
latitude': '40.809725', 'start station longitude': '-73.953149', 'end station
id': '116', 'end station name': 'W 17 St & 8 Ave', 'end station latitude': '40.
74177603', 'end station longitude': '-74.00149746', 'bikeid': '44317',
'usertype': 'Customer', 'birth year': '1979', 'gender': '1'}
...
{'tripduration': '1193', 'starttime': '2020-09-01 00:00:12.2020', 'StartDate':
'2020-09-01', 'stoptime': '2020-09-01 00:20:05.5470', 'start station id':
'3081', 'start station name': 'Graham Ave & Grand St', 'start station
latitude': '40.711863', 'start station longitude': '-73.944024', 'end station
id': '3048', 'end station name': 'Putnam Ave & Nostrand Ave', 'end station
latitude': '40.68402', 'end station longitude': '-73.94977', 'bikeid': '26396',
'usertype': 'Customer', 'birth year': '1969', 'gender': '0'}
Добавляем итераторы: функция range()
В отличие от многих других языков программирования, цикл for языка Python
по умолчанию обрабатывает все значения в списке или наборе данных. Это
может быть удобным для быстрой обработки всего набора данных, но не годится, если нужно обработать только часть его записей.
Эта проблема легко решается посредством использования переменной, называющейся итератором. Подобно любой переменной, итератору можно присвоить любое имя, хотя традиционно используется буква i (первая буква слова iterator). Как видно в листинге 4.1, одно из типичных использований итераторов Python — в функции range() (https://docs.python.org/3/tutorial/
controlflow.html#the-range-function). Это еще один пример функции управления потоком команд, которая, подобно циклам for и условным операторам if,
обладает специальными встроенными свойствами.
Например, функция range() содержит переменную итератора, которая позволяет немного модифицировать цикл for, позволяя ему обрабатывать не все строки, а только количество, указанное в итераторе. Иными словами, вместо этой
стандартной формы цикла for:
for элемент in полном_списке_элементов:
мы можем создать форму цикла for, который начинает обработку с определен-
ного элемента в списке и обрабатывает заданное количество элементов
for позиция_элемента in range (начальная_позиция,
>количество_элементов_для_обработки):
В листинге 4.1 вы решили отобразить первые пять строк исходного файла данных:
for i in range(0,5):
print (next(citibike_reader))
Работа с файловыми и канальными данными на языке Python
|
129
Следует обратить внимание на то, что диапазон итерации включает первое
значение, указанное в параметрах, но исключает второе. Это означает, что для
отображения пяти строк данных начальное значение итерации должно быть 0,
поскольку индексация, т. е. нумерация элементов, списков в Python начинается
с 0, а не с 1.
Чтение данных из файлов TSV и TXT
Несмотря на свое название, благодаря опции указания разделителя в функции
DictReader(), библиотека csv Python является, по сути, универсальным средством
для работы с табличными данными. По умолчанию функция DictReader() предполагает, что в качестве разделителя используется символ запятой. Но этот символ
очень легко изменить, просто указав другой при вызове этой функции. Например, в
листинге 4.3 в качестве разделителя указывается символ табуляции, но с той же
легкостью можно было бы указать и любой другой символ разделителя.
Листинг 4.3. Сценарий tsv_parsing.py
#
#
#
#
#
Простой пример чтения данных из файла .tsv посредством Python,
используя библиотеку csv. Исходные данные были загружены как файл .tsv
из электронной таблицы Google Джеда Шугермана (Jed Shugerman) по политикам США
с профессиональным прошлым в области юриспруденции:
https://docs.google.com/spreadsheets/d/1E6Z-jZWbrKmit_4lG36oyQ658Ta6Mh25HCOBaz7YVrA
# Импортируем библиотеку csv
import csv
# Открываем файл ShugermanProsecutorPoliticians-SupremeCourtJustices.tsv
# в режиме чтения ("r").
# Этот файл должен находиться в той же самой папке, что и файл автономного сценария
# или блокнота
tsv_source_file = open("ShugermanProsecutorPoliticians-SupremeCourtJustices.tsv","r")
# Передаем наш исходный файл tsv_source_file как аргумент (ингредиент) методу
# (рецепту) DictReader библиотеки csv.
# Сохраняем результат в переменной politicians_reader
politicians_reader = csv.DictReader(tsv_source_file, delimiter='\t')
# Метод DictReader добавил в наши данные некоторую полезную информацию. Например,
# свойство fieldnames позволяет нам обращаться ко всем значениям в первой строке
# (или заголовке)/
print(politicians_reader.fieldnames)
# Отображаем только первую строку данных, используя функцию next()
print (next(politicians_reader))
130
|
Глава 4
Этот набор данных доступен на веб-сайте информационного бюллетеня Data Is
Plural (Данные — множественные) Джереми Сингер-Вайна (Jeremy Singer-Vine's,
@jsvine, https://data-is-plural.com).
Результаты исполнения этого сценария должны выглядеть наподобие показанным в
листинге 4.4.
Листинг 4.4. Результат исполнения сценария tsv_parsing.py
['', 'Justice', 'Term Start/End', 'Party', 'State', 'Pres Appt', 'Other Offices
Held', 'Relevant Prosecutorial Background']
{'': '40', 'Justice': 'William Strong', 'Term Start/End': '1870-1880', 'Party':
'D/R', 'State': 'PA', 'Pres Appt': 'Grant', 'Other Offices Held': 'US House,
Supr Court of PA, elect comm for elec of 1876', 'Relevant Prosecutorial
Background': 'lawyer'}
Что такое расширение файла?
Удобно, конечно, когда компьютеры решают ряд задач за нас автоматически и
даже "автомагически", но будем надеяться, что процесс изучения выпаса данных и программирования на языке Python поможет прояснить одно важное обстоятельство: мы обладаем намного бóльшим влиянием на поведение наших
компьютеров, чем это может казаться с первого взгляда.
Идеальным примером этому будут расширения имен файлов. В этой главе я
постоянно выделяю моменты, как расширение файла может предоставить нам
подсказку о формате содержащегося в файле набора данных, а также, возможно, более эффективно помочь нам в поиске требуемых данных. Конечно же,
компьютеры тоже используют расширения файлов, чтобы делать предположение о содержимом определенных файлов, обычно полагаясь на них, чтобы выбрать правильную программу для открытия файла. Вот поэтому при попытке
изменить расширение файла, намеренно или непреднамеренно, отображается
предупреждающее сообщение примерно следующего типа: "Если вы измените
расширение имени файла, файл может стать непригодным для использования.
Вы действительно хотите сделать это?" Хотя, несомненно, это сообщение отображается в интересах пользователя, может создаться впечатление, что изменение расширения имени файла может действительно сделать файл непригодным для использования.
В действительности же это совсем не так. Изменение расширения имени файла, например, с .tsv на .txt или наоборот, никоим образом не влияет на его содержимое. Единственное, к чему это приведет, — изменится представление
компьютера, что делать с этим файлом, то есть какую программу использовать
для его открытия.
Работа с файловыми и канальными данными на языке Python
|
131
К счастью, средства Python наподобие тех, которые мы используем в этой книге, не делают подобных предположений. При работе с табличными данными,
при условии, что указанный символ разделителя совпадает с символом разделителя, используемого в файле, расширение имени файла не имеет абсолютно
никакого значения. Собственно говоря, мы создали файл .txt, который мы используем далее в листинге 4.5, просто скопировав файл .tsv из листинга 4.3 и
изменив его расширение.
Хотя в настоящее время расширение имени файла .tsv стало сравнительно распространенным, многие файлы, содержащие данные, разделенные символом табуляции, но созданные более старыми системами баз данных, могут иметь расширение
файла .txt. К счастью, как описано во врезке "Что такое расширение файла?", при
условии указания правильного символа разделителя это никоим образом не влияет
на обработку таких файлов, как демонстрируется в листинге 4.5.
Листинг 4.5. Сценарий txt_parsing.py
#
#
#
#
#
#
Простой пример чтения данных из файла .tsv посредством Python,
используя библиотеку csv. Исходные данные были загружены как файл .tsv
из электронной таблицы Google Джеда Шугермана (Jed Shugerman) по политикам США
с профессиональным прошлым в области юриспруденции
https://docs.google.com/spreadsheets/d/1E6Z-jZWbrKmit_4lG36oyQ658Ta6Mh25HCOBaz7YVrA
Исходный файл .tsv был скопирован и переименован с расширением .txt
# Импортируем библиотеку csv
import csv
# Открываем файл ShugermanProsecutorPoliticians-SupremeCourtJustices.txt
# в режиме чтения ("r").
# Этот файл должен находиться в той же самой папке, что и файл автономного сценария
# или блокнота
txt_source_file = open("ShugermanProsecutorPoliticians-SupremeCourtJustices.txt","r")
# Передаем наш исходный файл txt_source_file как аргумент (ингредиент) методу
# (рецепту) DictReader библиотеки csv.
# Сохраняем результат в переменной politicians_reader
# Добавляем параметр delimeter, присваивая ему значение символа табуляции, "\t"
politicians_reader = csv.DictReader(txt_source_file, delimiter='\t')
# Метод DictReader добавил в наши данные некоторую полезную информацию. Например,
# свойство fieldnames позволяет нам обращаться ко всем значениям в первой строке
# (или заголовке)/
print(politicians_reader.fieldnames)
# Отображаем только первую строку данных, используя функцию next()
print (next(politicians_reader))
132
|
Глава 4
Как обсуждалось во врезке "Пробелы — зло!" главы 1, при использовании в коде
символов пробелов и других непечатаемых символов они должны предшествоваться управляющими символами. В данном случае мы используем управляющие символы \t для обозначения непечатаемого символа табуляции. Кроме символа табуляции, нам часто придется использовать другие управляющие символы, такие как
\n для перехода на новую строку или \r для возврата каретки.
Если ваш код не содержит никаких ошибок, то результат его исполнения должен
выглядеть точно таким же, как и результат исполнения сценария в листинге 4.2,
показанный в листинге 4.3.
Здесь вы можете задаваться вопросом: как я могу знать, какой разделяющий символ
используется в обрабатываемом файле? Символ можно определить программным
способом, но самым простым будет просто посмотреть его самому. Всякий раз,
приступая (или планируя) к работе с новым набором данных, начните с открытия
его файла в наиболее простом текстовом редакторе, доступном на вашем устройстве, или в любом редакторе кода. Использование наиболее простого текстового редактора объясняется тем, что в случае особенно большого файла это позволит вашему устройству выделить максимальный объем памяти и вычислительных мощностей на чтение данных. Таким образом понижается вероятность зависания
программы редактора или сбоя устройства. Закрытие других исполняющихся программ и ненужных вкладок браузера также может быть полезным в этом отношении.
Далее в книге мы рассмотрим некоторые способы для просмотра небольших фрагментов действительно больших файлов, но сейчас настало время попрактиковать
навыки, необходимые для оценки качества данных, которые требуют просмотра
данных и принятия суждений о них. Таким образом, хотя существуют способы автоматизировать задачу определения используемого в данных символа разделителя,
будет быстрее и понятнее посмотреть его самому в текстовом редакторе. Кроме
этого, такой подход поможет вам одновременно ознакомиться и с другими важными аспектами ваших данных.
И все на свете просто текст
Одна из причин, почему так полезно просматривать файлы типа .csv, .tsv и
многих других форматов данных в текстовом редакторе, состоит в том, что
большинство форматов данных, с которыми мы будем работать, на наиболее
базовом уровне являются просто текстом. Абсолютно так же, как и обычный
текст на русском языке организован в предложения и абзацы при помощи
стандартного использования точек, пробелов, заглавных букв и новых строк,
форматы данных организовываются посредством знаков препинания, и на
практическом уровне один формат данных отличается от другого просто используемыми для его организации знаками препинания. Как мы уже видели,
базовые табличные данные разделяются на поля, или столбцы, при помощи
разделителей, и на записи, или строки, при помощи символа новой строки.
Работа с файловыми и канальными данными на языке Python
|
133
Канальные данные, которые мы рассмотрим в следующем разделе, отличаются
несколько большей гибкостью (и сложностью), но в итоге следуют своим собственным, сравнительно простым, правилам пунктуации и структуризации.
Конечно же, существует большое количество нетекстовых форматов данных,
которые обычно создаются специализированными, проприетарными программами, предназначенными для выполнения первичной обработки (а также анализа и визуализации) данных, не прибегая к написанию большого объема кода.
Эти программы могут способствовать более быстрому или достижимому решению высокоспецифичных задач, но их недостаток состоит в том, что они
часто дороги, сложны в освоении и негибки. Как мы увидим в разд. "XLSX,
ODS и все остальные" далее в главе, извлечение данных из этих проприетарных форматов также может стать трудной, или даже невозможной, задачей, а
полученные результаты — оказаться ненадежными. Поэтому, тогда как средства Python все еще могут помочь нам с выпасом данных некоторых наиболее
распространенных проприетарных форматов, в некоторых случаях наилучшей
опцией будет использование альтернативного программного обеспечения (или
привлечение чьей-либо помощи).
Выпас реальных данных:
понимание безработицы
Для исследования одного из более сложных табличных форматов данных мы используем набор данных по безработице в Соединенных Штатах. Почему? Потому
что, так или иначе, безработица затрагивает большинство из нас, а в последние десятилетия США испытывали одни из особенно высоких уровней безработицы. Статистика по безработице в США издается каждый месяц Бюро трудовой статистики
(БТС), но тогда как эти данные часто публикуются новостными источниками, они
обычно рассматриваются как некий тип абстрактного показателя, как себя ведет
"экономика". Но что эти числа представляют на самом деле, редко подвергается
глубокому обсуждению.
Когда я только начала работать в газете Wall Street Journal в 2007 г., моим первым
крупным проектом было создание интерактивной панели для исследования месячных данных экономических показателей, включая показатели по безработице. Одним из более интересных моментов, которые я узнала в процессе работы над проектом, было то, что для каждого месяца вычисляется не один показатель уровня безработицы, а несколько (шесть, если быть точным). В новостях обычно сообщается
показатель уровня безработицы, называющийся U3, который БТС описывает следующим образом:
Общая безработица как процент от гражданских трудовых ресурсов (официальный уровень безработицы).
134
|
Глава 4
На первый взгляд это выглядит как прямолинейное определение безработицы: какой процент из всех людей, которые по разумным ожиданиям могли бы работать,
не работает?
Но в действительности все немного посложнее. Что означает "быть трудоустроенным" или считаться частью "трудовых ресурсов"? Взглянув на разные показатели
безработицы, можно получить более ясное представление о том, что не учитывает
показатель U3. Показатель уровня безработицы U6 определяется следующим образом:
Общее количество безработных, плюс все лица, минимально связанные с трудовыми ресурсами, плюс общее количество занятых неполный рабочий день по
экономическим причинам в процентах от гражданских трудовых ресурсов, плюс
все лица, минимально связанные с трудовыми ресурсами.
Это более пространственное определение становится более понятным, когда мы
прочитаем сопроводительное примечание5:
ПРИМЕЧАНИЕ
Лицами, минимально связанными с трудовыми ресурсами, являются лица, которые в настоящее время не трудоустроены и не ищут трудоустройства, но выражают желание и доступны для трудоустройства, а также искали трудоустройство в последние 12 месяцев. Отчаявшиеся работники, представляющие подмножество минимально связанных с трудовыми ресурсами, предоставили причину,
связанную с рынком трудовых ресурсов, по которой они в настоящее время не
ищут работу. Лицами, работающими неполный рабочий день по экономическим
причинам, являются лица, которые хотят и доступны для трудоустройства полный рабочий день, но были вынуждены согласиться на неполное трудоустройство. Обновленные контрольные группы населения вводятся ежегодно с выпуском
данных за январь.
Иными словами, если вы хотите работать и искали работу в последнем году, но активно не искали ее в последнее время, или если вы работаете неполный рабочий
день, но хотите полное трудоустройство, тогда вы официально не считаетесь безработным по определению U3. Это означает, что показатель U3 не обязательно отражает экономическую реальность американских граждан, работающих на нескольких работах (которыми с большей вероятностью будут женщины с несколькими
детьми)6, и потенциально исполнителей разовых работ (количество которых по недавним подсчетам составляет вплоть до 30% всех трудовых ресурсов США)7. Не-
5
Из материалов Бюро трудовой статистики США (https://bls.gov/news.release/empsit.t15.htm).
См. статью Multiple Jobholders от 21 декабря 2018 г., авторы Stephane Auray, David L Fuller и
Guillaume Vandenbroucke (https://research.stlouisfed.org/publications/economic-synopses/2018/12/21/
multiple-jobholders).
7
См. статью New Recommendations on Improving Data on Contingent and Alternative Work Arrangements,
https://blogs.bls.gov/blog/tag/contingent-workers; и статью The Value of Flexible Work: Evidence
from Uber Drivers, автор M. Keith Chen и другие, серия Nber Working Paper Series No. 23296,
https://nber.org/system/files/working_papers/w23296/ w23296.pdf.
6
Работа с файловыми и канальными данными на языке Python
|
135
удивительно тогда, что месячный показатель U6 обычно на несколько процентов
выше, чем показатель U3.
Чтобы сравнить эти показатели во времени, эти данные можно загрузить из вебсайта Федеральной резервной системы города Сент-Луиса. Этот сайт также содержит тысячи других наборов данных экономических показателей, доступных для
загрузки в ряде форматов, включая файлы .xls(x) табличного типа, а также файлы
канального типа (которые мы используем в сценарии в листинге 4.16).
Данные для этих упражнений можно загрузить из базы данных экономических показателей Федеральной резервной системы FRED (https://fred.stlouisfed.org/series/
U6RATE). Эта страница также отображает график показателя уровня безработицы
U6, начиная с момента, когда этот показатель был создан в начале 90-х годов прошлого столетия.
К этому графику также можно добавить показатель U3. Для этого справа вверху
над графиком нажмите кнопку Edit graph и в открывшейся справа панели выберите вкладку ADD LINE. Далее введите в поле поиска слово UNRATE и в результатах поиска выберите Unemployment Rate. Наконец, нажмите кнопку Add data
series. Закройте боковую панель, нажав значок Х в ее правом верхнем углу, нажмите кнопку Download и выберите опцию Excel8. Будет загружен файл .xls, с которым
мы будем работать в последнюю очередь, поскольку, хотя этот тип файлов все еще
широко используется, это сравнительно устаревший формат, который в 2007 г. был
заменен в качестве формата по умолчанию для электронных таблиц MS Excel форматом .xlsx (https://en.wikipedia.org/wiki/Microsoft_Excel#File_formats).
Чтобы получить дополнительные требуемые нам форматы, откройте загруженный
файл в программе электронных таблиц (например, Google Sheets), выберите в меню
опцию Save As, а затем укажите .xlsx. Затем повторите эту процедуру, но в конце
укажите .ods. Теперь у вас должны быть следующие три файла, содержащие одинаковую информацию: fredgraph.xlsx, fredgraph.ods и fredgraph.xls.9.
Если открыть оригинальный файл fredgraph.xls, то можно заметить, что
кроме данных по безработице он содержит и другие данные. Например,
в заголовках указывается информация о происхождении данных и определении индикаторов U3 и U6. Для анализа показателей уровня безработицы, данные по которым содержат эти файлы, эти метаданные
нужно будет отделить от интересующих нас данных, находящихся далее в таблице. Но на данном этапе наша цель заключается просто в преобразовании всех этих файлов в формат .csv. Процесс очистки данных,
включающий удаление этих метаданных, мы рассмотрим в главе 7.
8
Эти инструкции также доступны на самом веб-сайте FRED (https://fredhelp.stlouisfed.org/fred/
graphs/customize-a-fred-graph/data-transformation-add-series-to-existing-line).
9
Эти файлы также можно загрузить из моего хранилища Google Drive (https://drive.google.com/drive/
u/0/folders/1cU5Tdg_fvrCcwvAAyhMOhpbEcI2fF7sb).
136
|
Глава 4
XLSX, ODS и все остальные
В целом, по мере возможности, лучше избегать работы напрямую с данными в
форматах .xlsx, .ods и в большинстве других нетекстовых табличных форматах
данных. Для простого исследования наборов данных я рекомендую просматривать
такие файлы, просто открывая их в какой-либо программе электронной таблицы, а
затем сохраняя их в формате .csv или .tsv, прежде чем работать с ними средствами
Python. Это не только облегчит работу с вашими файлами данных, но также даст
вам возможность увидеть их содержимое и понять, что оно собой представляет.
Сохранение файлов формата .xls(x) и других подобных форматов данных в формате
.csv или эквивалентном текстовом формате не только уменьшит размер файла, но и
даст вам лучшее представление о том, как выглядят "настоящие" данные при просмотре сохраненных таким образом файлов. Вследствие форматирования данных в
программах электронной таблицы, иногда отображаемое на экране представление
данных значительно отличается от их исходных неформатированных значений, хранящихся в файле. Например, значения, которые в электронной таблице отображаются как проценты (например, 10%), на самом деле в файле могут быть десятичными значениями (т. е. 0,1). Это может создавать проблемы, если основывать разные аспекты обработки или анализа средствами Python на увиденном вами в
электронной таблице, а не на действительном форматировании данных в таких текстовых форматах данных, как, например, .csv.
Тем не менее вам обязательно придется сталкиваться с ситуациями, в которых к
файлам формата .xls(x) и других подобных форматов нужно будет обращаться напрямую средствами Python10. Например, если вам нужно регулярно (скажем, каждый месяц) обрабатывать набор данных в формате .xls, то копирование и сохранение файла в нужном формате каждый раз будет неизбежно времязатратным.
К счастью, то активное сообщество Python, о котором мы говорили в разд. "Сообщество" главы 1, создало библиотеки, которые могут с легкостью работать с впечатляющим количеством форматов данных. Чтобы получить полное представление
о том, как эти библиотеки работают с более сложными исходными данными (и
форматами данных), чуть далее мы рассмотрим несколько примеров кода, который
считывает данные в указанном формате, а затем сохраняет эти же данные в новом
файле в формате .csv.
Но для использования этих библиотек их нужно сначала установить на своем устройстве, выполнив по отдельности следующие команды в окне терминала11.
pip install openpyxl
pip install pyexcel-ods
pip install xlrd==2.0.1
10
На момент работы над материалом данной книги программа электронной таблицы LibreOffice
поддерживает такое же количество строк, что и программа MS Excel (220), но намного меньше
столбцов. А программа Google Sheets, наоборот, может работать с большим количеством столбцов,
нежели MS Excel, но только с 40 000 строками максимально.
11
На момент работы над материалом этой книги все эти библиотеки были доступны для использования в среде Google Colab.
Работа с файловыми и канальными данными на языке Python
|
137
В последующих примерах кода мы используем библиотеку openpyxl для доступа
(или синтаксического разбора) данных в файлах .xlsx, библиотеку pyexcel-ods для
работы с файлами .ods и библиотеку xlrd для чтения данных с файлов .xls. (Дополнительная информация о том, как искать и выбирать требуемые библиотеки Python,
приводится в разд. "Где искать библиотеки" в приложении А.)
Чтобы лучше проиллюстрировать особенности этих разных форматов данных, мы
применим подход, подобный описанному в конце разд. "Выпас данных реального
мира: понимание безработицы" ранее в этой главе. В частности, мы откроем исходный файл .xls в какой-либо программе электронной таблицы, после чего сохраним его сначала как файл .xlsx, а затем как файл .ods, которые будут содержать
точно такие же данные, как и исходный файл .xls. В процессе дальнейшей работы
вы получите представление о том, как эти нетекстовые форматы усложняют процесс выпаса данных (по моему мнению, без какой бы то ни было для этого надобности).
Начнем с обработки файла .xlsx (см. листинг 4.6), используя версию данных по безработице, загруженных с веб-сайта FRED. Этот пример иллюстрирует одно из первых основных различий между работой с текстовыми файлами табличных данных и
нетекстовыми форматами. В частности, поскольку нетекстовые форматы поддерживают множественные "листы", в начале сценария мы используем цикл for, в теле
которого размещается код для создания отдельных выходных файлов (по одному
для каждого листа).
Листинг 4.6. Сценарий xlsx_parsing.py
#
#
#
#
#
Пример чтения данных из файла .xlsx средствами Python, используя библиотеку openpyxl.
Сначала нам нужно установить эту библиотеку при помощи установщика pip
https://pypi.org/project/openpyxl/
Исходные данные можно создать и загрузить со следующего веб-сайта
https://fred.stlouisfed.org/series/U6RATE
# Указываем "главу", т.е. метод, который нужно импортировать из библиотеки openpyxl.
# В данном случае это "load_workbook"
from openpyxl import load_workbook
# Импортируем библиотеку csv, чтобы создать выходной файл
import csv
# Передаем имя нашего файла в качестве аргумента методу load_workbook()
# библиотеки openpyxl.
# Сохраняем результат в переменной source_workbook
source_workbook = load_workbook(filename = 'fredgraph.xlsx')
# Рабочая книга .xlsx может содержать несколько листов.
# Отображаем их имена для справки
print(source_workbook.sheetnames)
138
|
Глава 4
# Обрабатываем в цикле рабочие листы в рабочей книге source_workbook
for sheet_num, sheet_name in enumerate(source_workbook.sheetnames):
# Создаем переменную, указывающую на текущий рабочий лист,
# передавая текущее значение переменной sheet_name в source_workbook
current_sheet = source_workbook[sheet_name]
# Отображаем имя листа sheet_name
print(sheet_name)
# Создаем выходной файл "xlsx_"+имя_листа
output_file = open("xlsx_"+sheet_name+".csv","w")
# Используем метод для записи библиотеки csv для записи строк данных в выходной
# файл output_file
output_writer = csv.writer(output_file)
# Обрабатываем в цикле каждую строку в листе
for row in current_sheet.iter_rows():
# Создаем пустой список для хранения значений ячеек (столбцов) каждой строки
row_cells = []
# Для каждой ячейки (столбца) в каждой строке...
for cell in row:
# Отображаем ее содержимое, просто чтобы посмотреть, как его видит код
print(cell, cell.value)
# Добавляем значения в конец списка, используя метод append()
row_cells.append(cell.value)
# Записываем нашу новую строку данных в выходной файл
output_writer.writerow(row_cells)
# Закрываем файл .csv, в который мы только что записали все данные
output_file.close()
Подобно методу DictReader() библиотеки csv, метод load_workbook() библиотеки
openpyxl добавляет свойства в исходные данные. В данном случае это свойство,
которое отображает имена всех листов данных в рабочей книге.
Хотя рабочая книга в данном примере содержит только один рабочий лист, в будущем их может быть больше. Для доступа как к итератору, так и к имени листа
используется функция enumerate(). Таким образом мы сможем создавать по одному
файлу .csv для каждого рабочего листа.
Для каждого листа в рабочей книге source_workbook будет требоваться свой собственный выходной файл .csv с однозначным именем. Для создания этих файлов мы
Работа с файловыми и канальными данными на языке Python
|
139
открываем новый файл в режиме записи, передавая функции open() аргумент
xlsx_"+имя_листа+".csv для имени файла и аргумент "w" для указания режима записи.
(До сих пор мы открывали файлы в режиме чтения, передавая функции open() аргумент режима работы "r".)
Функция iter_rows() библиотеки openpyxl преобразовывает строки рабочей книги source_workbook в список, который можно обрабатывать в цикле.
Библиотека openpyxl обрабатывает каждую ячейку данных как тип данных кортеж языка Python (https://docs.python.org/3/library/stdtypes.html#tuple). Если попытаться просто отображать строки текущего листа current_sheet напрямую, то мы
получим адреса ячеек, а не значения содержащихся в них данных. Чтобы решить
эту проблему, мы вкладываем еще один цикл внутрь текущего, который обрабатывает каждую ячейку строки и добавляет значения данных в список row_cells.
Обратите внимание на то, что этот код выровнен по левому краю со строкой кода
for cell in row выше. Это означает, что он находится вне этого цикла и будет исполнен только после того, как все ячейки (столбцы) текущей строки были добавлены в список.
Этот сценарий также демонстрирует, что как два повара могут подходить поразному к приготовлению одного и того же блюда, так и создатели библиотек могут принимать разные решения касательно того, как (ре)структурировать каждый
тип данных исходного файла, что имеет соответствующие последствия для нашего
кода. Например, разработчики библиотеки openpyxl решили хранить метку каждой
ячейки данных (например, A6) и содержащееся в ней значение в кортеже (tuple)
Python. Это решение является причиной, по которой нам требуется другой цикл for
для каждой строки данных — потому что для того, чтобы создать список Python,
который потом станет одной строкой в выходном файле .csv, нам нужно обрабатывать данные по ячейке за раз. Так, если открыть файл xlsx_FRED Graph.csv, созданный сценарием в листинге 4.6, в программе электронной таблицы, мы увидим, что
значения дат, которые в столбце observation_date исходного файла .xls были в формате ГГГГ-ММ-ДД, в этом файле будут в формате ГГГГ-ММ-ДД ЧЧ:ММ:СС. Это
объясняется тем, что разработчики библиотеки openpyxl решили автоматически
преобразовывать любые строковые данные, выглядящие наподобие дат, в формат
данных дат datetime Python. Очевидно, что все эти решения не являются ни правильными, ни неправильными. Нам просто нужно принимать их к сведению при
разработке нашего кода, чтобы не исказить или неверно истолковать исходные
данные.
Разобравшись с обработкой версии .xlsx нашего файла данных, посмотрим, что
происходит при обработке его версии .ods, как показано в листинге 4.7.
Листинг 4.7. Сценарий ods_parsing.py
# Пример чтения данных из файла .ods средствами Python,
# используя библиотеку pyexcel-ods. Сначала нужно установить эту библиотеку при помощи
# установщика pip: https://pypi.org/project/pyexcel-ods/
140
|
Глава 4
# Указываем "главу" или метод, который нужно импортировать из библиотеки pyexcel-ods.
# В данном случае это метод get_data
from pyexcel-ods import get_data
# Импортируем библиотеку csv, чтобы создать выходной файл
import csv
# Передаем имя файла в качестве аргумента методу get_data() библиотеки pyexcel-ods
# Сохраняем результат в переменной source_workbook
source_workbook = get_data("fredgraph.ods")
# Рабочая книга .ods может содержать несколько листов
for sheet_name, sheet_data in source_workbook.items():
# Отображаем имя листа sheet_name
print(sheet_name)
# Создаем выходной файл ods_"+sheet_name+".csv для хранения текущего листа
output_file = open("ods_"+sheet_name+".csv","w")
# Используем метод для записи writer() библиотеки csv для записи строк данных
# в выходной файл output_file
output_writer = csv.writer(output_file)
# Обрабатываем в цикле каждую строку в листе
for row in sheet_data:
# Записываем каждую строку напрямую в выходной файл, используя метод writerow()
output_writer.writerow(row)
# Закрываем файл .csv, в который мы только что записали все данные
output_file.close()
Библиотека pyexcel-ods преобразовывает наши исходные данные в тип данных
Python OrderedDict. Связанный метод items() позволяет обращаться к названию и
данным каждого листа как к паре ключ/значение, которые мы можем обрабатывать
в цикле. В данном случае ключом является sheet_name, а значением — все данные
рабочего листа.
В данном случае sheet_data уже является списком, поэтому мы можем просто
обработать его, используя простой цикл for.
Эта библиотека преобразовывает каждую строку рабочего листа в список, вследствие чего мы можем передавать эти строки напрямую методу writerow().
В случае использования библиотеки pyexcel-ods содержимое выходного файла .csv
намного больше похоже на содержимое исходного файла fredgraph.xls при его про-
Работа с файловыми и канальными данными на языке Python
|
141
смотре в программе электронной таблицы типа Google Sheets. Например, поле
observation_date имеет простой формат ГГГГ-ММ-ДД. Кроме этого, разработчики
библиотеки решили рассматривать значения строки как список, что позволяет сохранять каждую запись напрямую в выходной файл, не требуя создания дополнительных циклов или списков.
Наконец, посмотрим, что происходит при обработке исходного файла .xls, используя библиотеку xlrd. Соответствующий код приводится в листинге 4.8.
Листинг 4.8. Сценарий xls_parsing.py
# Простой пример чтения данных из файла .xls средствами Python,
# используя библиотеку xrld. Сначала установим библиотеку xlrd, используя установщик
# pip: https://pypi.org/project/xlrd/2.0.1/
# Импортируем библиотеку xlrd
import xlrd
# Импортируем библиотеку csv, чтобы создать выходной файл
import csv
# Передаем имя файла в качестве аргумента методу open_workbook() библиотеки xlrd.
# Сохраняем результат в переменной source_workbook
source_workbook = xlrd.open_workbook("fredgraph.xls")
# Рабочая книга .xls может содержать несколько листов
for sheet_name in source_workbook.sheet_names():
# Создаем переменную, указывающую на текущий рабочий лист,
# передавая текущее значение sheet_name методу sheet_by_name
current_sheet = source_workbook.sheet_by_name(sheet_name)
# Отображаем имя листа sheet_name
print(sheet_name)
# Создаем выходной файл xls_"+sheet_name+".csv для хранения текущего листа
output_file = open("xls_"+sheet_name+".csv","w")
# Используем метод для записи writer() библиотеки csv для записи строк данных
# в выходной файл output_file
output_writer = csv.writer(output_file)
# Обрабатываем в цикле каждую строку в листе
for row_num, row in enumerate(current_sheet.get_rows()):
# Каждая строка уже является списком, но для доступа к его значениям
# нужно использовать метод row_value().
142
|
Глава 4
# Записываем каждую строку напрямую в выходной файл, используя метод writerow()
output_writer.writerow(current_sheet.row_values(row_num))
# Закрываем файл .csv, в который мы только что записали все данные
output_file.close()
Обратите внимание на то, что эта структура похожа на структуру, используемую
при работе с библиотекой csv.
Функция get_row() библиотеки xlrd преобразовывает строки текущего рабочего
листа в список, который можно обрабатывать в цикле12.
Значения дат, записанные в выходной файл, будут иметь несколько странное
форматирование13. Мы рассмотрим, как решить эту проблему, в разд. "Декодирование дат Excel" главы 7.
В этом выходном файле мы увидим странный формат значений в поле
observation_date, что отражается словами создателей библиотеки xlrd14:
Даты в электронных таблицах Excel: в действительности, таковых нет.
Что у нас есть, так это числа с плавающей запятой и благие надежды.
(Dates in Excel spreadsheets: In reality, there are no such things.
What you have are floating point numbers and pious hope.)
В результате, чтобы из файлов .xls получить полезные даты, удобные для восприятия людьми, требуется выполнить значительную очистку. Этот вопрос обсуждается в разд. "Декодирование дат Excel" главы 7.
Будем надеяться, что эти упражнения продемонстрировали, как, используя несколько умных библиотек и немного откорректировав базовый код, средства Python
позволяют легко и быстро извлекать первичные данные из ряда табличных форматов. В то же самое время я надеюсь, что эти примеры также проиллюстрировали,
почему всегда предпочтительнее15 работать с текстовыми и/или открытыми форматами данных — ведь зачастую они требуют меньшего объема очистки и преобразований, чтобы привести их в ясное, пригодное для дальнейшей обработки состояние.
12
Дополнительную информацию по функции get_rows() см. в документации для библиотеки xlrd
(https://xlrd.readthedocs.io/en/latest/api.html#xlrd-sheet).
13
Дополнительную информацию по этому вопросу см. в документации для библиотеки xlrd
(https://xlrd.readthedocs.io/en/latest/dates.html).
14
Из статьи Dates in Excel Spreadsheets, авторы Stephen John Machin и Chris Withers
(https://xlrd.readthedocs.io/en/latest/dates.html).
15
При просмотре в текстовом редакторе каждого из выходных файлов, полученных в предыдущих
примерах, можно увидеть, что формат с открытым кодом .ods является самым простым и опрятным.
Работа с файловыми и канальными данными на языке Python
|
143
Данные фиксированной ширины
Одна из самых старых версий табличных данных — это данные фиксированной
ширины. Как можно предполагать по названию, каждый столбец данных этого
формата содержит конкретное предопределенное количество символов, и всегда
только данное количество символов. Это означает, что в файлах значений фиксированной ширины значащие данные часто дополняются дополнительными символами, например пробелами или нулями.
В современных системах данных этот формат используется редко, но с ним все еще
можно встретиться при работе с правительственными источниками данных, чья
инфраструктура может насчитывать десятки лет16. Например, управление NOAA
(National Oceanic and Atmospheric Administration — Национальное управление по
проблемам океана и атмосферы) США (https://noaa.gov/our-history), основанное
в начале XIX столетия, предоставляет бесплатный онлайн-доступ к широкому
диапазону подробной и актуальной информации посредством своей сети Global
Historical Climatology Network (Глобальная сеть исторической климатологии,
https://ncdc.noaa.gov/data-access/land-based-station-data/land-based-datasets/globalhistorical-climatology-network-ghcn). Большинство этих данных опубликованы в
формате фиксированной ширины. Например, информация об уникальных идентификаторах станций, их местонахождении и принадлежности к конкретной сети
хранится в файле ghcnd-stations.txt (https://www1.ncdc.noaa.gov/pub/data/ghcn/
daily). Для интерпретации значений метеорологических данных (многие из которых также предоставляются в файлах формата фиксированной ширины) нужно сопоставлять данные по станциям с метеорологическими данными.
Работать с данными фиксированной ширины может быть еще более сложно, чем с
данными других табличных форматов, если у вас нет доступа к метаданным, описывающим организацию файла и его полей. В случае разделенных данных часто можно
просто просмотреть файл в текстовом редакторе и достаточно уверенно идентифицировать используемый разделитель. В худшем случае можно просто попробовать обрабатывать файл, используя разные символы разделителя, чтобы увидеть, который
даст наилучшие результаты. В случае файлов данных фиксированной ширины, особенно если они большого размера, если какое-либо поле не содержит никаких данных, можно легко непреднамеренно объединить значения нескольких полей данных.
Данные, разделенные символами табуляции, по сравнению
с данными фиксированной ширины
Как упоминалось в разд. "Файловые табличные форматы данных" ранее в
этой главе, в настоящее время все еще можно столкнуться с файлами данных,
разделенных символами табуляции, имеющих расширение файла .txt, которое
также применяется и для файлов данных фиксированной ширины.
16
Как, например, в Пенсильвании (https://spotlightpa.org/news/2021/05/pa-unemployment-claimsoverhaul-ibm-gsi-benefits-labor-industry) или Колорадо (https://denverpost.com/2021/01/10/coloradounemployment-benefits-new-claims-system).
144
|
Глава 4
Поскольку для разделения полей оба эти формата данных полагаются на непечатаемые символы (пробел или символ табуляции), как можно точно знать, с
каким форматом данных мы имеем дело?
Это еще одна ситуация, когда вы можете избежать лишних проблем, самостоятельно исследовав файл в текстовом редакторе или редакторе кода, поскольку
эти два формата данных визуально явно отличаются друг от друга. Начните с
просмотра правой части документа: разделенные символом табуляции данные
будут иметь ступенчатый правый край, т. е. записи (столбцы) данных будут
разной длины. А данные фиксированной ширины будут выровнены по правому краю, т. е. все записи будут заканчиваться в одной точке.
Например, ниже приводится несколько строк данных формата tsv из файла
ShugermanProsecutorPoliticians-SupremeCourtJustices.tsv, с которым мы работали в листинге 4.3, как они выглядят в редакторе кода Atom.
42
43
44
Ward Hunt
Morrison Waite
John Marshall Harlan
1873-1882
1874-1888
1877-1911
R
R
R
NY
OH
KT
Grant
Grant
Hayes
А здесь показано несколько строк данных формата фиксированной ширины из
файла ghcnd-stations.txt:
AEM00041217
AEM00041218
AF000040930
24.4330
24.2620
35.3170
54.6510
55.6090
69.0170
26.8 ABU DHABI INTL
264.9 AL AIN INTL
3366.0 NORTH-SALANG GSN
41217
41218
40930
Как видите, в случае данных фиксированной ширины по правому краю выровнены не только сами записи, но также и все числовые поля. Это еще одна подсказка, что перед нами данные фиксированной ширины, а не разделенные символом табуляции.
К счастью, метаданные о файле ghcnd-stations.txt, который мы используем в качестве нашего источника данных, включены в файл readme.txt, который находится в той
же самой папке веб-сайта NOAA (https://www1.ncdc.noaa.gov/pub/data/ghcn/daily/
readme.txt).
Просматривая файл readme.txt, находим заголовок IV. FORMAT OF "ghcnd- stations.txt"
и следующую таблицу:
Variable
ID
LATITUDE
LONGITUDE
ELEVATION
STATE
NAME
GSN FLAG
HCN/CRN FLAG
WMO ID
Columns
1-11
13-20
22-30
32-37
39-40
42-71
73-75
77-79
81-85
Type
Character
Real
Real
Real
Character
Character
Character
Character
Character
Работа с файловыми и канальными данными на языке Python
|
145
За этой таблицей следует подробное описание содержимого или значения каждого
поля, включая информацию типа единиц измерения. Благодаря этому надежному
словарю данных, мы теперь знаем не только организацию файла ghcnd-stations.txt,
но также и как интерпретировать содержащуюся в нем информацию. Как мы увидим в главе 6, поиск (или создание) словаря данных является важной частью процесса оценки или улучшения качества наших данных. Но сейчас мы просто рассмотрим, как преобразовать эти данные фиксированной ширины в данные, разделенные запятой. Соответствующий код приводится в листинге 4.9.
Листинг 4.9. Сценарий fixed_width_parsing.py
#
#
#
#
#
Пример чтения данных из файла данных фиксированной ширины средствами Python.
Исходный файл данных для этого примера предоставлен управлением NOAA и доступен здесь:
https://www1.ncdc.noaa.gov/pub/data/ghcn/daily/ghcnd-stations.txt
Метаданные для файла доступны здесь:
https://www1.ncdc.noaa.gov/pub/data/ghcn/daily/readme.txt
# Импортируем библиотеку csv, чтобы создать выходной файл
import csv
filename = "ghcnd-stations"
# Для чтения обычного текстового файла не требуются никакие специальные библиотеки,
# поэтому просто открываем его в обычном режиме для чтения "r".
source_file = open(filename+".txt", "r")
# Встроенный метод readlines() делает то, что говорит его название:
# считывает содержимое входного текстового файла и преобразовывает его в список строк
stations_list = source_file.readlines()
# Создаем выходной файл для преобразованных данных
output_file = open(filename+".csv","w")
# Используем метод для записи writer() библиотеки csv для записи строк данных
# в выходной файл output_file
output_writer = csv.writer(output_file)
# Создаем список заголовков
headers = ["ID","LATITUDE","LONGITUDE","ELEVATION","STATE","NAME","GSN_FLAG",
"HCNCRN_FLAG","WMO_ID"]
# Записываем заголовки в выходной файл
output_writer.writerow(headers)
# Обрабатываем в цикле все строки нашего файла (множественные "листы" отсутствуют)
for line in stations_list:
# Создаем пустой список, в который будем добавлять каждый набор символов,
# составляющий данный "столбец" данных
new_row = []
146
|
Глава 4
# ID: positions 1-11
new_row.append(line[0:11])
# LATITUDE: positions 13-20
new_row.append(line[12:20])
# LONGITUDE: positions 22-30
new_row.append(line[21:30])
# ELEVATION: positions 32-37
new_row.append(line[31:37])
# STATE: positions 39-40
new_row.append(line[38:40])
# NAME: positions 42-71
new_row.append(line[41:71])
# GSN_FLAG: positions 73-75
new_row.append(line[72:75])
# HCNCRN_FLAG: positions 77-79
new_row.append(line[76:79])
# WMO_ID: positions 81-85
new_row.append(line[80:85])
# Теперь осталось только записать new_row в выходной файл, используя метод
# writerow()
output_writer.writerow(new_row)
# Закрываем файл .csv, в который мы только что записали все данные
output_file.close()
Так как исходный файл не содержит ничего, что можно было бы использовать в
качестве заголовков столбцов, нам нужно самим создать эти заголовки на основании информации в файле readme.text (https://www1.ncdc.noaa.gov/pub/data/ghcn/
daily/readme.txt). Обратите внимание на то, что эти заголовки не содержат никаких
специальных символов, а вместо пробелов используются символы подчеркивания.
Это сведет к минимуму возможные проблемы при последующих очистке и анализе
этих данных.
В Python строки текста рассматриваются просто как списки символов, поэтому
можно просто дать указание выдать нам символы между двумя значениями индекса. Подобно функции range(), здесь возвращается символ из позиции первого значения индекса, но не из второго, а из позиции, на единицу меньшей второго значения. Также вспомним, что в Python счет элементов списка начинается с нуля. Это
означает, что для каждой записи значение первого индекса будет на единицу меньше значения, указываемого в метаданных, но значение второго будет таким же, как
и в метаданных.
Работа с файловыми и канальными данными на языке Python
|
147
Если открыть файл .csv, созданный сценарием в листинге 4.9, в программе электронной таблицы, то можно будет увидеть, что форматирование значений в некоторых столбцах не будет единообразным. Например, в столбце ELEVATION числа с
десятичной частью выровнены по левому краю, а числа без десятичной части — по
правому. Что здесь происходит?
Опять же, пролить свет на эту проблему можно, открыв файл в текстовом редакторе. Хотя данные в созданном нашим сценарием файле технически разделены запятыми, значения, сохраненные в каждом из его разделенных столбцов, все так же
содержат дополнительные пробелы, которые они содержали в исходном файле. В
результате данные в нашем новом файле выглядят довольно похожими на данные
фиксированной ширины.
Иными словами, точно так же, как в случае с "датами" в файлах Excel, преобразование содержимого нашего файла в содержимое .csv не создает разумные типы
данных в выходном файле "автоматически". Задача определения типа данных, который должно иметь каждое поле, а также их очистки, чтобы они вели себя должным образом, является частью процесса очистки данных, который мы рассмотрим в
главе 7.
Канальные данные — интерактивные обновления
через Интернет
Структура форматов табличных типов данных хорошо подходит для мира, в котором большинство "данных" уже были отфильтрованы, откорректированы и преобразованы в сравнительно хорошо организованную коллекцию чисел, дат и коротких строк. Но с появлением Интернета возникла необходимость в передаче больших объемов информации типа "произвольного" текста, который можно найти,
например, в новостях и лентах социальных сетей. Поскольку в состав содержимого
данных этого типа обычно входят такие символы, как запятые, точки и кавычки,
которые влияют на его семантическое значение, вписать его в традиционный формат с разделителями будет, как минимум, проблематично. Более того, предрасположение форматов с разделителями к горизонтальной организации (что влечет за
собой большой объем прокрутки влево-вправо) противоречит сетевым обычаям
вертикальной прокрутки. Канальные форматы данных были разработаны, чтобы
устранить оба эти недостатка.
На высшем уровне существует два основных типа канальных форматов данных:
XML и JSON. Это текстовые форматы, которые позволяют поставщику данных определять свою собственную структуру данных, что делает их чрезвычайно гибкими
и, следовательно, полезными для широкого спектра содержимого, которое можно
найти на веб-сайтах и интернет-платформах. Независимо от местонахождения данных этих форматов, будь то в Интернете или на локальной машине, их можно распознать, помимо всего прочего, по их соответствующим расширениям файлов .xml
и .json.
148
|
Глава 4
Формат .xml
Формат XML (Extensible Markup Language — расширяемый язык разметки) охватывает широкий диапазон файловых форматов, включая форматы .rss, .atom и
даже .html. Будучи самым обобщенным типом языка разметки, формат XML обладает чрезвычайной гибкостью и был, скорее всего, первоначальным форматом
для каналов данных.
Формат .json
Формат JSON (JavaScript Object Notation — система обозначений объектов
JavaScript) появился несколько позже, чем формат XML, но служит той же самой цели. Формат JSON менее описателен, чем формат XML, и, следовательно,
файлы данных в этом формате имеют меньший размер. Это означает, что файлы
JSON могут содержать почти такой же объем данных, как и файлы XML, но при
этом занимают меньше места, что особенно важно для скорости мобильного Интернета. Не менее важен и тот факт, что файлы JSON являются, по сути, типом
данных больших объектов в языке программирования JavaScript, на основе которого созданы многие, если не большинство, веб-сайты и мобильные приложения. Это означает, что любой веб-сайт или программа, использующая JavaScript,
может с легкостью работать с данными в формате JSON, особенно по сравнению
с данными в формате XML. К счастью, типы данных объектов JavaScript очень
похожи на типы данных dict языка Python, что также делает очень прямолинейной работу с данными формата JSON в этом языке.
Прежде чем углубляться в рассмотрение, как работать с каждым из этих типов данных на языке Python, уделим немного времени на размышления о том, когда мы
можем отдать предпочтение работе с канальными данными и где их искать в таком
случае.
Когда лучше использовать канальные данные
В некотором смысле канальный тип данных является для XXI столетия тем, чем
табличные данные были для XX: сам объем канальных данных, создаваемых, сохраняемых и обмениваемых в Интернете каждый день, наверное, в миллионы раз
превышает объем всех вместе взятых табличных данных в мире. В значительной
мере это объясняется тем, что канальные данные обеспечивают работу веб-сайтов
социальных сетей, новостных приложений и всего прочего.
С точки зрения выпаса данных предпочтительней иметь данные канального типа в
тех случаях, когда исследуемое явление зависит от времени и обновляется часто
и/или непредсказуемо. Данные этого типа обычно создаются в ответ на различные
активные процессы, например публикации в социальных сетях, публикации новостей или, скажем, запись землетрясения.
Как и файловые табличные данные, канальные данные могут содержать архивную
информацию, но, как обсуждалось в начале этой главы, первые обычно отражают
данные в том состоянии, в каком они существовали в какой-либо определенный
Работа с файловыми и канальными данными на языке Python
|
149
момент времени. Последние же, в противоположность файловым, как правило,
организовываются в обратном хронологическом порядке (сначала самые последние) — первой записью будет запись, созданная наиболее недавно к моменту обращения к данным, а не в заданную дату.
Где искать канальные данные
Канальные данные используются, как правило, в Интернете, часто по специальным
URL-адресам, которые называются конечными точками (эндпойнтами) API-интерфейса. Мы будем обсуждать в подробностях работу с API-интерфейсами в главе 5,
а на данном этапе будет достаточно знать, что API-интерфейсы — это в действительности всего лишь веб-страницы, содержащие только данные. Такие страницы
можно просматривать в обычном веб-браузере, но все, что вы там увидите, — это
сами данные. Некоторые API-интерфейсы возвращают разные данные в зависимости от переданной им информации. Вот эта возможность, среди прочего, и делает
процесс работы с канальными данными таким гибким: изменив всего лишь несколько слов или значений в коде, можно получить доступ к совершенно другому
набору данных!
Чтобы найти API-интерфейсы, предоставляющие канальные данные, не требуется
применения никаких специальных поисковых стратегий, поскольку такие вебсайты и службы обычно хотят, чтобы их нашли. Почему? Простыми словами, код,
использующий API-интерфейс, обычно приносит некую выгоду предоставляющей
его компании, даже если эта выгода состоит просто в дополнительной публичности
для компании. Например, на начальном этапе существования Twitter многие вебразработчики создавали программы, используя API-интерфейс веб-сайта этой компании, тем самым как повышая полезность платформы, так и экономя компании
средства и усилия на выяснение требований пользователей и последующее удовлетворение этих требований. Благодаря предоставлению такого большого объема
данных своей платформы на бесплатной основе (на начальных этапах), этот APIинтерфейс способствовал возникновению нескольких компаний, которые в конечном итоге были приобретены компанией Twitter. С другой стороны, еще большее
количество компаний были вынуждены уйти с рынка вследствие изменения или
API-интерфейса, или условий его использования17. Это подчеркивает одну из конкретных проблем, которые могут возникнуть при работе с данными любого типа,
но особенно с данными канального типа, предоставляемыми коммерческими компаниями: как данные, так и доступ к ним могут измениться в любое время и без какого бы то ни было предупреждения. Таким образом, хотя источники канальных
данных действительно представляют ценность, они могут быть эфемерными во
многих отношениях.
17
См. статью Twitter's 10 Year Struggle with Developer Relations на веб-сайте компании Nordic APIs
(https://nordicapis.com/twitter-10-year-struggle-with-developer-relations).
150
|
Глава 4
Выпас канальных данных
средствами языка Python
Как и в случае с табличными данными, выпас канальных данных средствами языка
Python возможен благодаря сочетанию полезных библиотек и тому факту, что такие
форматы, как JSON, уже похожи на типы данных, существующих в языке Python.
Кроме этого, в последующих разделах мы увидим, что для наших целей форматы
XML и JSON часто взаимозаменяемы (хотя многие API-интерфейсы предоставляют
данные только в одном или другом из этих форматов).
XML: один язык разметки, чтобы править всеми остальными
Языки разметки являются самым старым видом стандартизированных форматов
документов в области вычислений. Они были разработаны с целью создания текстовых документов, удобочитаемых как людьми, так и машинами. В 90-х годах
прошлого столетия язык XML начал играть все более важную роль в инфраструктуре Интернета, когда вследствие большой разновидности устройств, обращающихся к веб-информации и отображающих ее, отделение информации о содержимом (например, текст и изображения) от информации о форматировании (например,
разметка страницы) стало более необходимым. В отличие от HTML-документов, в
которых содержимое и форматирование полностью перемешаны, XML-документы
не содержат никаких инструкций касательно того, как отображать содержащуюся в
них информацию. Вместо этого они состоят из тегов и атрибутов, играющих роль
метаданных о типе содержащейся в них информации, вместе с самой информацией.
Чтобы дать вам представление о том, как выглядит XML-документ, в листинге 4.10
приводится пример подобного документа.
Листинг 4.10. Пример XML-документа
<?xml version="1.0" encoding="UTF-8"?>
<mainDoc>
<!--Это комментарий.-->
<elements>
<element1>Какой-либо текст в документе</element1>
<element2>Какие-либо другие данные в документе</element2>
<element3 someAttribute="aValue" />
</elements>
<someElement anAttribute="anotherValue">Другое содержимое</someElement>
</mainDoc>
Здесь нужно обратить внимание на пару моментов. Самая первая строка называется
объявлением типа документа (document type или doc-type). В ней предоставляется
информация о том, что всю следующую часть документа нужно интерпретировать
как XML (а не как какой-либо другой язык веб-разработки разметки, некоторые из
которых мы вкратце рассмотрим далее в этой главе).
Работа с файловыми и канальными данными на языке Python
|
151
Начиная со строки:
<mainDoc>
начинается содержимое самого документа. Одним из факторов, придающих языку
XML такую гибкость, является то, что он содержит только две настоящие грамматические структуры, обе из которых включены в листинг 4.10:
Теги
Теги могут быть или парными (как element1, element2, someElement или даже
mainDoc), или самозакрывающимися (как element3). Имя тега всегда заключается в
угловые скобки (<>). В случае закрывающего тега немедленно после открывающей скобки следует косая черта (/). Пара из открывающего и закрывающего тега, а также самозакрывающийся тег, также называются элементами XML.
Атрибуты
Атрибуты допускаются только внутри тегов (как anAttribute в листинге 4.10).
Атрибут — это пара типа ключ/значение, в которой после имени атрибута (или
ключа) немедленно следует знак равенства (=), а за ним значение в двойных кавычках ("").
Элементом XML является все содержимое, заключенное между открывающим тегом и соответствующим закрывающим тегом (например, <elements> и </elements>).
Таким образом, данный XML-элемент может содержать несколько тегов, каждый
из которых может также содержать другие теги. Любой элемент также может иметь
любое количество атрибутов (или ни одного). Самозакрывающийся тег также считается элементом.
Другое значимое правило для структурирования XML-документов состоит в том,
что при наличии вложенных тегов самый последний открытый тег нужно закрыть
самым первым. В листинге 4.11 показан пример правильной XML-структуры.
Листинг 4.11. Пример правильной структуры XML
<outerElement>
<!-- Обратите внимание на то, что элемент innerElement1 закрыт
перед открытием элемента innerElement2.-->
<innerElement1>Какое-либо содержимое</innerElement1>
<innerElement2>Другое содержимое</innerElement2>
</outerElement>
А структура в листинге 4.12 неправильная.
Листинг 4.12. Пример неправильной структуры XML
<outerElement>
<!-- НЕПРАВИЛЬНО! Элемент innerElement2 был открыт
прежде, чем был закрыт элемент innerElement1 -->
<innerElement1>Какое-либо содержимое<innerElement2>Другое содержимое</innerElement1>
</innerElement2>
</outerElement>
152
|
Глава 4
Этот принцип "последний открыт, первый закрыт" также называется вложением
(nesting), подобно вложению циклов for, как было показано на рис. 2.318. Вложение
имеет особенную важность в XML-документах, поскольку оно управляет одним из
основных механизмов, используемых для чтения (или парсинга) XML-документов
с кодом (или документов, составленных на других языках разметки). В XMLдокументах первый элемент после объявления doc-type называется корневым (root)
элементом. В отформатированных XML-документах корневой элемент будет всегда
выровнен по левому краю, а следующий вложенный элемент будет сдвинут на один
уровень вправо и называется дочерним (child) элементом. Таким образом, в листинге 4.10 элемент <mainDoc> будет корневым, а элемент <elements> будет его вложенным дочерним элементом. Подобным образом, элемент <mainDoc> является
родительским (parent) элементом элемента <elements> (см. листинг 4.13).
Листинг 4.13. XML-документ с комментариями
<?xml version="1.0" encoding="UTF-8"?>
<mainDoc>
<!-- Элемент mainDoc является корневым элементом, а элемент elements является его
дочерним элементом-->
<elements>
<!-- Элемент element является родителем (parent) элементов element1, element2
и element3, которые являются сестринскими (siblings) элементами друг с другом
(т. е. одноуровневыми элементами). -->
<element1>Текстовые данные в документе.</element1>
<element2>Какие-либо другие данные в документе.</element2>
<element3 someAttribute="aValue" />
</elements>
<!-- Элемент someElement также является дочерним элементом (child) элемента mainDoc
и сестринским элементом элемента elements. -->
<someElement anAttribute="anotherValue">Другое содержимое</someElement>
</mainDoc>
Принимая во внимание этот тренд к генеалогическому жаргону, вы, наверное, задаетесь вопросом: если элемент <elements> является родителем элемента <element3>,
а элемент <mainDoc> является родителем элемента <elements>, то не делает ли это
элемент <mainDoc> прародителем (grandparent) элемента <element3>? И да и нет. Тогда как элемент <mainDoc> является родителем родителя элемента <element3>, термин
прародитель никогда не используется для описания структуры XML, поскольку с
таким подходом все может стать запутанным очень быстро. Вместо этого эти взаимоотношения описываются точно так, как есть: элемент <mainDoc> является родителем родителя элемента <element3>.
18
В отличие от кода Python, вложенные элементы XML-документов не обязательно должны выделяться отступами для их правильной работы, хотя такое форматирование определенно делает их более
удобочитаемыми.
Работа с файловыми и канальными данными на языке Python
|
153
К счастью, атрибуты XML-элементов избавлены от подобной сложности: они просто идут как пары ключ/значения и могут существовать только внутри тегов XML:
<element3 someAttribute="aValue" />
Обратите внимание на отсутствие пробела по обеим сторонам знака равенства, как
и в случае между косой чертой и закрывающей угловой скобкой.
Подобно писательскому процессу на русском языке (или на языке Python), вопрос,
когда использовать теги, а когда атрибуты для определенного элемента информации, в значительной степени зависит от предпочтений и стиля автора. Например,
документы в листингах 4.14 и 4.15 содержат одинаковую информацию об этой книге, но каждый из них имеет слегка разные структуры.
Листинг 4.14. Образец данных XML о книге — предпочтение отдается атрибутам
<aBook>
<bookURL url="https://www.oreilly.com/library/view/practical-python-data/
9781492091493"/>
<bookAbstract>
В наборах данных нас ждут фантастические открытия и ценные истории,
и эта книга поможет нам открыть их.
</bookAbstract>
<pubDate date="2022-02-01" />
</aBook>
Листинг 4.15. Образец данных XML о книге — предпочтение отдается элементам
<aBook>
<bookURL>
https://www.oreilly.com/library/view/practical-python-data/9781492091493
</bookURL>
<bookAbstract>
В наборах данных нас ждут фантастические открытия и ценные истории,
и эта книга поможет нам открыть их.
</bookAbstract>
<pubDate>2022-02-01</pubDate>
</aBook>
Такая степень гибкости означает, что язык XML легко адаптируется к широкому
спектру источников данных и предпочтений форматирования. В то же самое время
он может легко создать ситуацию, в которой для каждого нового источника данных
требуется создание специального кода. Очевидно, что это была бы довольно неэффективная система, особенно если бы большое количество людей и организаций
публиковали довольно похожие типы данных.
Поэтому неудивительно, что существует большое количество спецификаций XML,
определяющих дополнительные правила для форматирования XML-документов,
154
|
Глава 4
предназначенных для хранения определенных типов данных. Далее приводится
список и краткое описание нескольких заслуживающих внимания примеров таких
спецификаций, с которыми вам, возможно, придется столкнуться в процессе своей
работы по выпасу данных. Несмотря на разные названия и расширения файлов этих
форматов, с ними всеми можно работать, используя один и тот же метод, который
приводится чуть далее в листинге 4.16.
RSS
Спецификация XML RSS (Really Simple Syndication — действительно простая
синдикация) была впервые представлена в конце 90-х годов прошлого столетия
для работы с новостной информацией. Для этих целей также широко используется формат XML .atom.
KML
Международный стандарт KML (Keyhole Markup Language — язык разметки
Keyhole) применяется для работы как с двумерными, так и с трехмерными географическими данными, и совместим с такими инструментами, как Google Earth.
SVG
Формат SVG (Scalable Vector Graphics — масштабируемая векторная графика)
широко применяется для веб-графики благодаря своей возможности масштабировать рисунки без потери качества. Многие распространенные программы
для работы с графикой могут сохранять результаты в файлах .svg, которые затем
можно вставлять в веб-страницы и другие документы, которые будут хорошо
выглядеть на экранах и устройствах самых разных размеров.
EPUB
Открытый стандарт EPUB (Electronic publication format — формат электронных
публикаций) широко применяется для цифровой публикации книг.
Как можно видеть из предшествующего списка, некоторые распространенные варианты формата XML ясно указывают на свою связь с XML, но другие — нет19.
Теперь, когда у нас есть общее представление о структуре формата XML, рассмотрим, как можно выполнить обработку содержимого файла в этом формате средствами Python. Хотя язык Python обладает некоторыми встроенными инструментами
для парсинга XML-содержимого, мы будем использовать библиотеку lxml, которая
особенно хорошо справляется с парсингом XML файлов большого размера
(https://nickjanetakis.com/blog/how-i-used-the-lxml-library-to-parse-xml-20x-fasterin-python#xmltodict-vs-python-s-standard-library-vs-lxml). Хотя в последующем
примере используются файлы довольно небольшого размера, практически тот же
самый код можно было бы использовать и для файлов значительно большего размера.
19
Интересный факт: вторая буква "x" в названии формата .xlsx означает XML.
Работа с файловыми и канальными данными на языке Python
|
155
Для начала мы используем версию XML данных показателя по безработице U6,
которые я уже загрузила из веб-сайта FRED, используя его API-интерфейс20. Загрузите копию этого файла с моего хранилища Google Drive (https://drive.google.com/
file/d/1gPGaDTT9Nn6BtlTtVp7gQLSuocMyIaLU), а затем преобразуйте содержащиеся в нем данные из формата XML в формат .csv, используя для этого сценарий
в листинге 4.16. Но сначала нам нужно установить библиотеку lxml при помощи
установщика pip:
pip install lxml
Листинг 4.16. Сценарий xml_parsing.py
#
#
#
#
#
#
#
Простой пример чтения данных из файла .xml средствами Python, используя библиотеку
lxml. Сначала нужно установить эту библиотеку при помощи установщика pip:
https://pypi.org/project/lxml/
Полезное руководство можно найти по этой ссылке https://lxml.de/tutorial.html
Используется экземпляр следующих данных
https://api.stlouisfed.org/fred/series/observations?series_id=U6RATE& \
api_key=ВАШ_КЛЮЧ_API
# Указываем "главу" или метод, который нужно импортировать из библиотеки lxml.
# В данном случае это etree, что означает ElementTree
from lxml import etree
# Импортируем библиотеку csv, чтобы создать выходной файл
import csv
# Задаем имя файла
filename = "U6_FRED_data"
# Открываем файл данных для чтения, указывая режим rb
xml_source_file = open(filename+".xml","rb")
# Передаем наш файл xml_source_file в качестве параметра методу etree.parse()
# библиотеки lxml.
# Сохраняем результат в переменной xml_doc
xml_doc = etree.parse(xml_source_file)
# Начинаем обработку с получения корневого элемента текущего xml-документа
document_root = xml_doc.getroot()
# Отображаем его, просто чтобы увидеть, какой он
print(etree.tostring(document_root))
20
Мы пошагово рассмотрим использование интерфейсов API, подобных этому, в главе 5, но использование этого документа на данном этапе позволит нам посмотреть, как разные форматы данных
влияют на наши взаимодействия с данными.
156
|
Глава 4
# Проверяем, что корневой элемент document_root является хорошо сформированным
# XML-элементом
if etree.iselement(document_root):
# Создаем выходной файл xml_"+filename+.csv
output_file = open("xml_"+filename+".csv","w")
# Используем метод для записи writer() библиотеки csv для записи строк данных
# в выходной файл output_file
output_writer = csv.writer(output_file)
# Берем первый элемент нашего xml-документа (используя document_root[0])
# и записываем его ключи атрибутов как заголовки столбцов в наш выходной файл
output_writer.writerow(document_root[0].attrib.keys())
# Теперь обрабатываем в цикле каждый элемент нашего XML файла
for child in document_root:
# Теперь используем метод .values(), чтобы получить значения всех элементов
# в виде списка, а затем используем этот список напрямую с методом .writerow()
output_writer.writerow(child.attrib.values())
# Закрываем файл .csv, в который мы только что записали все данные
output_file.close()
В данном случае файл данных не содержит ничего, что можно было бы использовать в качестве имени файла (например, имя листа), поэтому мы просто сами создаем имя и используем его как для загрузки исходных данных, так и в качестве
имени выходного файла.
Значение режима использования файла (т. е. "r"), которое мы до сих пор указывали во втором параметре функции open(), предполагало интерпретирование содержимого исходного файла как текст. Но поскольку библиотека lxml ожидает байтовые, а не текстовые данные, то в данном случае мы указываем режим "rb" (read
bytes).
Во многих случаях XML-содержимое плохо отформатировано. Чтобы удостовериться в том, что материал, который выглядит как качественный XML-материал, в
действительности является таковым, мы извлечем корневой элемент текущего
XML-документа и убедимся в том, что он работоспособен.
Поскольку наш XML-материал в настоящее время сохранен как байтовые данные, нам нужно использовать метод etree.tostring(), чтобы просматривать его в
этом виде.
Благодаря библиотеке lxml, каждый элемент (или узел) XML нашего документа
имеет свойство attrib с типом данных Python dict (dictionary — словарь). Все ключи атрибута XML-элемента возвращаются в виде списка посредством метода
.keys() (https://docs.python.org/3/library/stdtypes.html#typesmapping). Поскольку
Работа с файловыми и канальными данными на языке Python
|
157
все элементы в нашем исходном файле идентичны, можно использовать ключи
первого из них, чтобы создать строку заголовков для нашего выходного файла.
Библиотека lxml (https://lxml.de/tutorial.html#elements-are-lists) преобразовывает XML-элементы в списки, в результате чего все элементы в документе можно
просматривать при помощи простого цикла for…in.
Как оказалось, версия XML данных по безработице имеет очень простую структуру: это просто список элементов со всеми значениями, к которым мы хотим получить доступ, сохраненными в виде атрибутов.
В результате мы смогли извлечь значения атрибутов из каждого элемента в виде
списка и записать их напрямую в выходной файл .csv всего лишь одной строкой
кода.
Конечно же, во многих случаях интересующие нас данные имеют более сложный
формат XML, например RSS или Atom. В листинге 4.17 приводится пример обработки данных в более сложном формате XML. В частности, выполняется парсинг
канала RSS рассказов о науке и окружающей среде компании BBC. Копию этого
исходного файла можно загрузить из моего хранилища Google Drive
(https://drive.google.com/file/d/1zOaksshLfmXxLTipoOjTTnuO6PsVQgg2).
Листинг 4.17. Сценарий rss_parsing.py
#
#
#
#
#
Простой пример чтения данных из файла .xml средствами Python, используя библиотеку
lxml. Сначала нужно установить эту библиотеку при помощи установщика pip:
https://pypi.org/project/lxml/
Используется экземпляр следующих данных:
http://feeds.bbci.co.uk/news/science_and_environment/rss.xml
# Указываем "главу" или метод, который нужно импортировать из библиотеки lxml.
# В данном случае это etree, что означает ElementTree
from lxml import etree
# Импортируем библиотеку csv, чтобы создать выходной файл
import csv
# Присваиваем имя файла для простоты
filename = "BBC News - Science & Environment XML Feed"
# Открываем файл данных для чтения, указывая режим rb
xml_source_file = open(filename+".xml","rb")
# Передаем наш файл xml_source_file в качестве параметра методу etree.parse()
# библиотеки lxml.
# Сохраняем результат в переменной xml_doc`
xml_doc = etree.parse(xml_source_file)
158
|
Глава 4
# Начинаем обработку с получения корневого элемента текущего xml-документа
document_root = xml_doc.getroot()
# Если корневой элемент document_root является хорошо сформированным XML-элементом
if etree.iselement(document_root):
# Создаем выходной файл "rss_"+filename+".csv"
output_file = open("rss_"+filename+".csv","w")
# Используем метод для записи writer() библиотеки csv для записи строк данных
# в выходной файл output_file
output_writer = csv.writer(output_file)
# Элемент document_root[0] является "канальным" элементом
main_channel = document_root[0]
# Метод find() возвращает !только! первый экземпляр имени элемента
article_example = main_channel.find('item')
# Создаем пустой список для хранения будущих заголовков столбцов
tag_list = []
for child in article_example.iterdescendants():
# Добавляем каждый тег к нашему будущему списку заголовков
tag_list.append(child.tag)
# Если текущий тег имеет какие-либо атрибуты
if child.attrib:
# Обрабатываем в цикле ключи атрибута в теге
for attribute_name in child.attrib.keys():
# Добавляем имя атрибута к списку tag_list заголовков столбцов
tag_list.append(attribute_name)
# Записываем содержимое списка tag_list в выходной файл как заголовки столбцов
output_writer.writerow(tag_list)
# Теперь нужно извлечь !все! элементы <item> из исходного файла
# Поэтому вместо метода find() используем метод findall()
for item in main_channel.findall('item'):
# Пустой список для хранения содержимого новой строки
new_row = []
# Получаем содержимое каждого элемента, используя список тегов
for tag in tag_list:
Работа с файловыми и канальными данными на языке Python
|
159
# Если элемент содержит что-либо с данным именем тега
if item.findtext(tag):
# добавляем его в конец новой строки
new_row.append(item.findtext(tag))
# В противном случае, проверяем, что это атрибут isPermaLink
elif tag == "isPermaLink":
# Берем его значение из элемента <guid>
# и добавляем его в конец нашей строки
new_row.append(item.find('guid').get("isPermaLink"))
# Записываем новую строку в выходной файл
output_writer.writerow(new_row)
# Закрываем файл .csv, в который мы только что записали все данные
output_file.close()
Как всегда, мы хотим сбалансировать материал, обрабатываемый программно и
просматриваемый визуально. При просмотре данных становится ясно, что информация каждой статьи сохраняется в отдельном элементе item. Но, поскольку копирование имен отдельных тегов и атрибутов потребовало бы слишком много времени и было бы чревато ошибками, мы обработаем только один элемент item и создадим список всех тегов (и атрибутов) в нем, которые мы затем используем в
качестве заголовков столбцов в выходном файле .csv.
Метод iterdescendants() специфичен библиотеке lxml. Он возвращает только потомков элемента XML, тогда как более широко употребляемый метод iter() возвращает как сам элемент, так и его потомков (https://lxml.de/api.html#iteration).
Использование child.tag позволит извлечь текст tagname элемента потомка. Например, для элемента <pubDate> будет возвращено значение pubDate.
В нашем элементе <item> атрибут имеет только один тег, но мы все равно хотим
включить его в наш выход.
Метод keys() возвращает список всех ключей в списке атрибутов, принадлежащих тегу. Его имя нужно получить в виде строки, а не в виде списка с одним элементом.
Целью всего цикла for для списка article_example было лишь создание списка
заголовков tag_list, но это стоило приложенных усилий.
Как видно в листинге 4.17, использование в Python библиотеки lxml позволяет выполнять парсинг (синтаксический разбор) более сложного XML документа довольно легко.
160
|
Глава 4
Тогда как формат данных XML продолжает пользоваться популярностью для каналов новостей и другого рода файлов, некоторые его особенности делают его менее
удобным для обработки крупных объемов канальных данных современного Интернета.
Прежде всего, это вопрос размера. Тогда как файлы XML могут содержать удобное
описание своего содержимого, что уменьшает надобность в отдельных словарях
данных, тот факт, что большинство элементов содержат как открывающий, так и
соответствующий закрывающий тег (например, <item> и </item>), делает этот формат несколько многословным, т. е. XML-документ содержит большой объем текста,
не являющегося непосредственного его содержимым. Это обстоятельство не имеет
большого значения в случае документов, содержащих несколько десятков или даже
тысяч элементов, но при необходимости обрабатывать миллионы или даже биллионы сообщений социальных сетей, весь этот избыточный текст может существенно тормозить обработку.
Во-вторых, формат XML не так уж и сложно преобразовать в другие форматы данных, однако этот процесс не совсем гладкий. Библиотека lxml (среди прочих) позволяет довольно легко выполнять парсинг XML-содержимого средствами Python,
но решение этой же задачи посредством языка, заточенного под работу в Интернете, например JavaScript, сопряжено с большими сложностями и трудностями. Учитывая широкое использования языка JavaScript в Интернете, совсем неудивительно,
что в какой-то момент был разработан канальный формат данных, работающий без
проблем, характерных для этого языка. Как мы увидим в следующем разделе, многие ограничения формата данных XML решаются объектно-подобной природой
формата JSON, который в настоящее время является наиболее популярным форматом для канальных данных в Интернете.
Формат JSON: данные следующего поколения
В принципе, формат JSON похож на формат XML в том отношении, что в нем используется принцип вложения для группирования связанных элементов информации в записи и поля. Формат JSON также достаточно удобочитаем для людей, хотя
то обстоятельство, что он не поддерживает комментарии, означает, что для каналов
JSON-данных могут требоваться более надежные словари данных, чем для документов XML.
Для начала рассмотрим пример небольшого документа JSON, показанного в листинге 4.18.
Листинг 4.18. Пример документа JSON
{
"author": "Susan E. McGregor",
"book": {
"bookURL": "https://www.oreilly.com/library/view/practical-python-data/
9781492091493/",
Работа с файловыми и канальными данными на языке Python
|
161
"bookAbstract": "В наборах данных нас ждут фантастические открытия и ценные истории,
и эта книга поможет нам открыть их.",
"pubDate": "2022-02-01"
},
"papers": [{
"paperURL": "https://www.usenix.org/conference/usenixsecurity15/
technical-sessions/presentation/mcgregor",
"paperTitle": "Investigating the computer security practices and needs of
journalists",
"pubDate": "2015-08-12"
},
{
"paperURL": "https://www.aclweb.org/anthology/W18-5104.pdf",
"paperTitle": "Predictive embeddings for hate speech detection on
twitter",
"pubDate": "2018-10-31"
}
]
}
Подобно формату XML, грамматические правила формата JSON довольно простые:
документы JSON имеют всего лишь три различных структуры данных, которые
были представлены в документе листинга 4.18. А именно:
Пары ключ/значение
Технически, все в JSON-документе является парами ключ/значение, где ключ
заключен в кавычки и расположен слева от двоеточия (:), а значение указывается
справа от двоеточия. Обратите внимание на то, что тогда как ключ всегда должен быть строкой, то его значения могут быть строками (как в author), объектами (как в book) или списками (как в papers).
Объекты
Элементы объектов заключены в фигурные скобки — {}. Документ в листинге
4.18 содержит в целом четыре объекта: сам документ (обозначен фигурными
скобками, выровненными по левому краю), объект book и два безымянных объекта в списке papers.
Списки
Списки заключаются в квадратные скобки ([]) и могут содержать только объекты, разделенные запятыми.
Хотя оба формата XML и JSON можно использовать для кодирования одних и тех
же данных, в их возможностях есть существенные различия. Например, формат
JSON не позволяет указать тип документа doc-type, а также не поддерживает комментарии. Кроме этого, тогда как в XML списки в некоторой степени подразуме-
162
|
Глава 4
ваются (любой повторяющийся элемент функционирует как нечто вроде списка), то
в JSON списки обозначаются заключением их элементов в квадратные скобки — [].
Наконец, хотя формат JSON был разработан с учетом JavaScript, его структуры, как
вы, возможно, заметили, очень похожи на типы dict и list языка Python. Это обстоятельство и делает задачу парсинга документов JSON, а также посредством языка
JavaScript (и рядом других языков) средствами Python достаточно прямолинейной.
Чтобы продемонстрировать все эти возможности, в листинге 4.19 приводится код
для парсинга тех же самых данных, что и в листинге 4.16, полученных посредством API-интерфейса веб-сайта FRED, но в формате JSON. Этот файл данных
можно загрузить с моего хранилища Google Drive по следующей ссылке:
https://drive.google.eom/file/d/1Mpb2f5qYgHnKcU1sTxTmhOPHfzIdeBsq/view?usp=
sharing.
Листинг 4.19. Сценарий json_parsing.py
#
#
#
#
Простой пример чтения данных из файла .json средствами Python,
используя встроенную библиотеку json. Используется экземпляр следующих данных:
https://api.stlouisfed.org/fred/series/observations?series_id=U6RATE& \
file_type=json&api_key=ВАШ_КЛЮЧ_API
# Импортируем библиотеку json для работы с входным файлом
import json
# Импортируем библиотеку csv, чтобы создать выходной файл
import csv
# Задаем имя файла
filename = "U6_FRED_data"
# Открываем файл данных для чтения, указывая режим r
json_source_file = open(filename+".json","r")
# Передаем наш исходный файл json_source_file как аргумент (ингредиент)
# методу (рецепту) load() библиотеки json.
# Сохраняем результат в переменной json_data
json_data = json.load(json_source_file)
# Создаем выходной файл "json_"+filename+".csv"
output_file = open("json_"+filename+".csv","w")
# Используем метод для записи writer() библиотеки csv для записи строк данных
# в выходной файл output_file
output_writer = csv.writer(output_file)
# Используем ключи первого элемента (в позиции 0) в качестве заголовков столбцов
output_writer.writerow(list(json_data["observations"][0].keys()))
Работа с файловыми и канальными данными на языке Python
|
163
for obj in json_data["observations"]:
# Создаем пустой список для хранения значений каждого объекта
obj_values = []
# Для каждого ключа (который будет столбцом) в каждом объекте
for key, value in obj.items():
# Отображаем ее содержимое, просто чтобы посмотреть, как его видит код
print(key,value)
# Добавляем значения в наш список
obj_values.append(value)
# Получив всю строку, записываем данные в выходной файл
output_writer.writerow(obj_values)
# Закрываем файл .csv, в который мы только что записали все данные
output_file.close()
Поскольку библиотека json интерпретирует каждый объект как объект представления словаря (https://docs.python.org/3/library/stdtypes.html#dict-views), то нам
нужно дать указание программе Python преобразовать его в обычный список, используя для этого функцию list().
В большинстве случаев самым простым способом найти имя (или "ключ") главного объекта JSON в обрабатываемом документе будет простой визуальный просмотр документа. Но поскольку данные JSON часто отображаются в одной строке,
то получить лучшее представление о ее структуре можно, вставив эту строку в поле
онлайнового форматера JSONLint (https://jsonlint.com/). Таким образом мы увидим, что наши целевые данные являются списком с ключом observations.
Вследствие особенностей работы библиотеки json, если попытаться просто записывать строки напрямую, мы получим значения, помеченные dict, а не значения
самих данных. Поэтому нам нужен еще один цикл, который обрабатывает по одному все значения в каждом объекте json и добавляет их в конец списка obj_values.
Хотя формат JSON менее удобочитаем для людей, чем формат XML, у него есть
другие достоинства, о которых мы уже упоминали ранее, например меньший размер файлов и более широкая совместимость кода. Подобным образом, тогда как
формат JSON не так описателен, как формат XML, источники данных JSON (часто
API-интерфейсы) обычно достаточно хорошо задокументированы, что уменьшает
необходимость предполагать, что данная пара ключ/значение может описывать.
Но, как и вся работа с данными, первый шаг к выпасу данных в формате JSON состоит в формировании как можно большего понимания его контекста.
164
|
Глава 4
Где же вы, знаки непечатаемые?
В отличие от файлов .tsv и .txt, и самого языка Python, непечатаемые знаки не
играют никакой роли ни в XML, ни в JSON. При условии, что все угловые и фигурные скобки и другие знаки пунктуации расставлены должным образом, все
содержимое документов этих форматов можно втиснуть в одну строку, и они
будут работать без каких бы то ни было проблем. Примеры, которые мы рассматривали в этой главе, были опрятно отформатированы, но на практике редко
можно встретить подобное оформления документов этих типов, особенно в Интернете. Хотя многие веб-браузеры отображают документы XML в представлении с правильными отступами (см., например, ежедневную карту сайта газеты
Los Angeles times — https://latimes.com/sitemap-202101.xml), большая часть
данных JSON отображаются как следующие одна за другой строки текста (как в
случае с канальными данными реального времени системы Citi Bike —
https://feeds.citibikenyc.com/stations/stations.json).
Поскольку эффективный парсинг данных любого из этих форматов требует понимания его общей структуры, первый ключевой шаг состоит в получении
должным образом отформатированного представления содержимого любого
обрабатываемого вами файла канальных данных. В случае хорошо структурированного содержимого XML для этого достаточно открыть файл (или URLадрес) в веб-браузере21.
Для файлов .json меньшего размера их данные можно скопировать непосредственно из самого файла (выбрав все содержимое, нажав комбинацию клавиш
<Ctrl>+<A>, и далее скопировать, нажав комбинацию клавиш <Ctrl>+<C>), а
затем вставить в онлайновое средство форматирования типа JSONLint
(https://jsonlint.com/) или JSON Formatter (https://jsonformatter.org/json-prettyprint). Но в случае файлов особенно большого размера или отсутствия доступа
к Интернету, можно создать новый, отформатированный файл JSON из неотформатированного исходного файла, выполнив в терминале следующую команду Python:
cat ugly.json | python -mjson.tool > pretty.json
где ugly.json обозначает исходный неотформатированный файл JSON. В результате исполнения этой команды будет создан новый файл pretty.json, который
затем, чтобы просмотреть структуру документа, можно открыть в любом текстовом редакторе или редакторе кода.
21
Если страница отображается с применением таблицы стилей, как в случае с каналом BBC, с
которым мы работали в сценарии в листинге 4.17, для просмотра "сырого" XML-содержимого нужно
щелкнуть правой кнопкой по странице и в открывшемся контекстном меню выбрать последовательность команд Developer Tools | View Page Source.
Работа с файловыми и канальными данными на языке Python
|
165
Работа с неструктурированными данными
Как обсуждалось в разд. "Структурированные и неструктурированные данные"
ранее в этой главе, процесс создания данных зависит от внедрения в информацию
некой структуры, без наличия которой мы не сможем выполнять методический
анализ информации или извлекать из нее смысл. Хотя неструктурированная информация часто содержит большие фрагменты созданного человеком "свободного"
текста, как табличные, так и канальные данные сравнительно структурированы и,
что более важно, машиночитаемы.
В противоположность, когда мы имеем дело с неструктурированными данными,
наша работа всегда связана с приближениями: мы не можем быть уверенными в
том, что наши программные усилия по выпасу возвратят точную интерпретацию
лежащей в основе информации. Это объясняется тем, что большинство неструктурированных данных являются представлением содержимого, предназначенного для
восприятия и интерпретирования людьми. А как мы рассматривали в главе 2, компьютеры могут обрабатывать большие объемы данных намного быстрее и с меньшим количеством ошибок, чем люди, но их все равно можно сбить с толку неструктурированными данными, которые не представляли бы никакой проблемы для
людей. Например, слегка модифицированный дорожный знак "Стоп" компьютер
воспринимает как знак ограничения скорости (https://spectrum.ieee.org/cars-thatthink/transportation/sensors/slight-street-sign-modifications-can-fool-machinelearning-algorithms). Это означает, что при работе с данными, не читаемыми компьютерами, нужно самостоятельно выполнять их дополнительную проверку и верификацию. Отметим, что посредством языка Python такие данные все же можно
преобразовать в более удобный для работы формат.
Текст на основе изображений:
доступ к данным в формате PDF
Формат PDF (Portable Document Format — формат переносимого документа) был
создан в начале 90-х годов прошлого столетия как механизм для сохранения визуальной целостности электронных документов из обширного спектра источников,
будь то созданных программами текстовых редакторов или взятых из печатных материалов22. Сохранение внешнего вида документов также означало, что в отличие
от машиночитаемых форматов (например, документов, созданных программными
средствами обработки текстов), было трудно извлечь или изменить содержимое
таких документов, а это важная возможность для создания широкого спектра материалов, от цифровых версий контрактов до формальных писем.
22
Дополнительную информацию см. на странице About PDF веб-сайта компании Adobe
(https://acrobat.adobe.com/us/en/acrobat/about-adobe-pdf.html).
166
|
Глава 4
Иными словами, трудность выпаса данных документов PDF была на первых порах
заложена в них преднамеренно. Но поскольку возможность доступа к данным печатных документов является общей проблемой, то работа в области оптического
распознавания символов (ОРС) началась еще в конце XIX столетия23. Более того,
цифровые средства OCR были доступны в виде программных пакетов и интернетуслуг на протяжении нескольких десятилетий, поэтому, хотя эти средства и не идеальны, данные в файлах этого формата все же не полностью недоступны.
Когда следует работать с текстом в формате PDF
Работа с файлами PDF — это крайняя мера (так же как и парсинг данных вебресурсов (web-scraping), как мы рассмотрим далее в главе 5). По большому счету,
если есть возможность избежать использования информации из PDF документов,
следует воспользоваться этой возможностью. Поэтому процесс извлечения информации из PDF-документов, как правило, требует проверки точности этих результатов и является неотъемлемой частью любого рабочего процесса выпаса данных на
основе PDF-документов. При всем этом существует громадный объем информации,
доступной только в виде изображений или отсканированных документов PDF, и
платформа Python предоставляет эффективный способ для извлечения достаточно
точной копии текста таких документов.
Где искать PDF-документы
Если вы уверены, что требуемые вам данные существуют только в формате PDF,
тогда можно (и нужно) воспользоваться советами во врезке "Интеллектуальный
поиск конкретных типов данных" ранее в этой главе и выполнить поиск файлов
этого типа в Интернете. Также, при запросе информации у какого-либо лица или
организации, наиболее вероятно, что они предоставят ее вам в PDF-формате, и вам
нужно будет самому решить задачу, как извлечь из полученных документов требуемую вам информацию. Таким образом, вам не нужно будет долго искать PDFдокументы, к сожалению, скорее они сами найдут вас.
Выпас PDF-данных, используя Python
Поскольку PDF-документы могут содержать не только отсканированные изображения, но и машиночитаемый текст (например, документы программных средств обработки текстов), то текстовую информацию из подобных файлов можно извлечь
программным образом со сравнительно небольшим количеством ошибок. И хотя
это звучит просто, этот метод все равно может быть ненадежным, поскольку файлы
.pdf создаются с использованием широкого спектра кодировок, которые могут
23
См. статью Disruptive Developments in Document Recognition, автор George Nagy, в журнале Pattern
Recognition Letters, № 79, (2016): стр. 106112, https://doi.org/10.1016/j.patrec.2015.11.024. Доступна
здесь: https://ecse.rpi.edu/~nagy/PDF_chrono/ 2016_PRL_Disruptive_asPublished.pdf.
Работа с файловыми и канальными данными на языке Python
|
167
трудно поддаваться правильному определению программой. Поэтому, хотя программный подход извлечения текста из файлов .pdf может давать результаты высокой точности, возможность его применения для любого pdf-файла имеет низкую
вероятность.
Мы опишем процесс использования OCR для распознавания и извлечения текста из
файлов .pdf. Он состоит из двух шагов:
1. Преобразование страниц документа в отдельные изображения.
2. Оптическое распознавание символов на изображениях страниц, извлечение текста и запись полученного текста в отдельные текстовые файлы.
Неудивительно, что для выполнения всех этих задач нам нужно установить еще
несколько библиотек Python. Первым делом нам нужна пара библиотек для преобразования страниц PDF-документов в изображения. Первая из них, библиотека общего назначения poppler, устанавливается, исполняя в терминале следующую команду:
sudo apt install poppler-utils
Эта библиотека нужна для обеспечения работоспособности другой, специфичной
для Python, библиотеки pdf2image. Как можно судить по ее названию, библиотека
pdf2image используется для преобразования файла .pdf в ряд изображений. Устанавливается эта библиотека, исполняя в терминале следующую команду:
pip install pdf2image
Далее нам нужно установить инструменты для реализации процесса оптического
распознавания символов. Первый из них — библиотека общего назначения
tesseract-ocr (https://github.com/tesseract-ocr/tesseract), которая использует машинное обучение для распознавания текста в изображениях. Для установки выполните
в терминале следующую команду:
sudo apt-get install tesseract-ocr
Второй инструмент — библиотека Python pytesseract, использующая библиотеку
tesseract-ocr. Устанавливается она следующей командой:
pip install pytesseract
Наконец, нам нужна вспомогательная библиотека компьютерного видения для
Python, которая заполняет промежуток между библиотекой для обработки изображений страниц и библиотекой для оптического распознавания символов. Это библиотека opencv-python, которая устанавливается, исполнив в терминале следующую команду:
pip install opencv-python
Вот и все. Это может выглядеть как большое количество дополнительных библиотек, но следует иметь в виду, что с технической точки зрения мы используем здесь
продукт машинного обучения, одну из тех "умных" технологий интеллектуальной
обработки данных, которая лежит в основе бóльшей части "искусственного интеллекта". К счастью для нас, библиотека Tesseract сравнительно надежная и широкоиспользуемая: хотя она была создана в начале 80-х годов прошлого столетия ком-
168
|
Глава 4
панией Hewlett-Packard как проприетарная система, в 2005 г. она была выложена в
открытый доступ и в настоящее время поддерживает свыше 100 языков. Поэтому
решение в листинге 4.20 можно использовать и с другими языками, кроме английского.
Листинг 4.20. Сценарий pdf_parsing.py
# Простой пример чтения данных из файла .pdf средствами Python,
# используя библиотеку pdf2image для преобразования файла в изображения, а затем
# используя библиотеки openCV bna tesseract для извлечения текста из изображений
# Исходные данные были загружены со следующего веб-сайта:
# https://files.stlouisfed.org/files/htdocs/publications/page1-econ/2020/12/01/ \
# unemployment-insurance-a-tried-and-true-safety-net_SE.pdf
# Импортируем встроенную библиотеку os Python для создания новой папки для хранения
# преобразованных изображений и выходного текста
import os
# Импортируем метод convert_from_path() из библиотеки pdf2image
from pdf2image import convert_from_path
# Встроенная библиотека glob используется для обработки в цикле всех файлов в папке,
# которые имеют определенное расширение файла, например png
import glob
# cv2 — это рабочее имя библиотеки openCV
import cv2
# Библиотека pytesseract используется для взаимодействия с процессом OCR tesseract
import pytesseract
# Мы будем использовать название pdf для присвоения имен нашим сгенерированным
# изображениям и текстовым файлам
pdf_name = "SafetyNet"
# Исходный файл pdf должен находиться в той же самой папке, что и этот сценарий Python
pdf_source_file = pdf_name+".pdf"
# Если папки с таким же именем, как у нашего исходного файла pdf, не существует:
if os.path.isdir(pdf_name) == False:
# создаем новую папку с таким именем
target_folder = os.mkdir(pdf_name)
# Сохраняем все страницы PDF в переменной
pages = convert_from_path(pdf_source_file, 300)
Работа с файловыми и канальными данными на языке Python
|
169
# Обрабатываем в цикле все преобразованные страницы, нумеруя их, чтобы номера страниц
# можно было использовать для обозначения полученных изображений
for page_num, page in enumerate(pages):
# Создаем уникальное имя файла для каждого изображения страницы, объединяя имя
# папки и номер страницы
filename = os.path.join(pdf_name,"p"+str(page_num)+".png")
# Сохраняем изображение страницы в системе
page.save(filename, 'PNG')
# Далее обрабатываем все файлы в папке с расширением файла .png
for img_file in glob.glob(os.path.join(pdf_name, '*.png')):
# Заменяем косую черту в имени файла изображения точкой
temp_name = img_file.replace("/",".")
# Получаем уникальное имя страницы (например, p2) из temp_name
text_filename = temp_name.split(".")[1]
# Далее создаем новый файл для записи (который также должен быть в целевой папке),
# присваивая ему то же самое имя, что и у соответствующего файла изображения,
# но с расширением файла .txt
output_file = open(os.path.join(pdf_name,text_filename+".txt"), "w")
# Интерпретируем изображение, используя библиотеку cv2
img = cv2.imread(img_file)
# Создаем новую переменную для хранения результатов метода image_to_string()
# библиотеки pytesseract
converted_text = pytesseract.image_to_string(img)
# Записываем извлеченный текст в выходной файл
output_file.write(converted_text)
# Закрываем выходной файл
output_file.close()
Здесь мы передаем путь к нашему исходному файлу (в данном случае это просто
имя файла, поскольку он находится в той же самой папке, что и наш сценарий) и
требуемое разрешение в точках на дюйм (DPI — dots per inch) выходных изображений методу convert_from_path(). Использование более низкого разрешения ускорило
бы обработку, но могло бы значительно ухудшить точность обработки по оптическому распознаванию символов. Разрешение в 300 DPI является стандартным "печатным" разрешением.
170
|
Глава 4
Здесь мы используем метод join() библиотеки os для сохранения новых файлов в
целевой папке. Также используем функцию str() для преобразования номера страницы в строку, которая используется для создания имени файла.
Обратите внимание на то, что выражение *.png означает "файл с любым именем с
расширением файла .png". Функция glob() создает список всех имен файлов в папке, в которой хранятся наши изображения (имя которой в данном случае равно значению переменной pdf_name).
Манипулирование строками не представляет никаких проблем! Чтобы создать
уникальные (но соответствующие именам исходных файлов изображенй) имена
файлов для наших файлов .txt, полученных в результате обработки OCR файлов
.png, нам нужно извлечь уникальное имя страницы из переменной img_file, чье значение начинается со строки SafetyNet/ и заканчивается строкой .png. Косая черта
заменяется точкой, в результате чего мы получаем нечто наподобие значения
SafetyNet.p1.png, после обработки которого функцией split() по точкам получаем
список ["SafetyNet", "p1", "png"]. Наконец, мы можем получить имя страницы в
позиции 1. Выполнение всех этих манипуляций требуется по той причине, что мы
не можем быть уверенными, что функция glob() извлечет из папки с изображениями, например, значение p1.png первым, или что она вообще будет извлекать изображения по порядку.
В принципе, этот сценарий служит нашим целям: посредством нескольких десятков
строк кода он сначала преобразовывает многостраничный pdf-файл в изображения,
а затем записывает их содержимое (бóльшую часть) в последовательность новых
текстовых файлов.
Но подход типа "все в одном" также имеет свои ограничения. Преобразование файла PDF в изображения или изображений в текст представляет собой задачу, которую может требоваться выполнять довольно часто, но не всегда в связке с другими
операциями. Иными словами, в долгосрочной перспективе для решения этой задачи
было бы гораздо полезнее использовать два отдельных сценария, исполняя их последовательно — один после другого. Более того, немного повозившись, мы могли
бы разделить этот сценарий таким образом, чтобы можно было преобразовывать в
изображения любые файлы PDF или любые изображения в текст, без необходимости создавать какой бы то ни было код. Неплохо, не так ли?
Этот процесс повторного продумывания и реорганизации работающего кода называется рефакторингом кода (refactoring), или перепроектированием. Можно провести аналогию с корректурой или редактированием обычного текста, при этом
цель в обоих случаях одинаковая: сделать конечный результат более понятным и
простым для понимания. И так же как и в случае с документированием, рефакторинг кода на самом деле является всего лишь еще одним важным способом масштабирования вашей работы по выпасу данных, поскольку он намного упрощает
переиспользование вашего кода. Мы рассмотрим разные стратегии для рефакторинга кода и многократного использования сценариев в главе 8.
Работа с файловыми и канальными данными на языке Python
|
171
Обращение к таблицам PDF посредством Tabula
Посмотрев на текстовые файлы, созданные в результате исполнения сценария в
листинге 4.20, можно заметить, что они содержат много "лишнего" материала наподобие номеров страниц, заголовков, символов разрыва страницы и прочего "хлама" (https://en.wikipedia.org/wiki/Cruft). В то же самое время в них отсутствуют
некоторые ключевые элементы, такие как изображения и таблицы.
И хотя анализ изображений выходит за рамки данной книги (это намного более
специализированная область), стоит отметить, что нередко документы PDF содержат таблицы с данными, которые могли бы представлять для нас интерес. На самом деле эта задача настолько распространенная, скажем, в области СМИ, что
группа журналистов спроектировала и создала инструмент, называющийся Tabula
(https://tabula.technology/), предназначенный специально для ее решения.
Инструмент Tabula не является библиотекой Python — это отдельная программа.
Чтобы попробовать этот инструмент, загрузите установщик для вашей системы
(https://tabula.technology/#download-install). Для машин Chromebook и Linux нужно загрузить файл .zip и следовать инструкциям в файле README.txt. Независимо
от используемой машины, вам, вероятнее всего, потребуется сначала установить
программную библиотеку Java, выполнив в терминале следующую команду:
sudo apt install default-jre
Подобно некоторым другим инструментам с открытым исходным кодом, которые
мы рассмотрим в последующих главах (например, инструмент OpenRefine, который
я использовала для подготовки некоторых образцов данных для главы 2 и вкратце
рассматриваю в главе 11), инструмент Tabula работает "за кулисами" (хотя некоторые его операции отображаются в окне терминала), и взаимодействие с ним осуществляется через браузер. Такой подход позволяет получить доступ к традиционному графическому интерфейсу, освобождая компьютерные ресурсы для интенсивной работы с данными.
Заключение
Будем надеяться, что примеры кода в этой главе позволили вам начать формировать представление о широком спектре задач по выпасу данных, которые можно
решить посредством сравнительно небольшого объема кода Python, используя комбинацию тщательно подобранных библиотек и нескольких фундаментальных понятий программирования, с которыми мы познакомились в главе 2.
Возможно, вы заметили, что за исключением примера с PDF-документом, выход
всех других примеров в этой главе был, по сути, в формате .csv. Это никакая не
случайность. Формат .csv не только эффективен и универсален, но, как оказывается, табличные данные требуются для выполнения практически любых базовых статистических анализов или визуализаций. Это не означает, что анализ нетабличных
172
|
Глава 4
данных невозможен; как раз наоборот, это то, чему посвящена бóльшая часть современных исследований в области компьютерных наук (например, машинное обучение). Но поскольку эти системы часто сложные и непрозрачные, они не очень
подходят для того типа выпаса данных, на котором мы концентрируемся в этой
книге. Таким образом, мы будем направлять нашу энергию на такой тип анализов,
которые могут помочь нам понять, объяснить и выразить новые знания об окружающем нас мире.
В этой главе мы концентрировались на табличных данных и заранее подготовленных версиях канальных данных, далее в главе 5 мы рассмотрим, как использовать
средства Python совместно с API-интерфейсами и парсингом данных для получения
данных из систем и, при необходимости, непосредственно из самих веб-страниц.
ГЛАВА 5
Доступ к интернет-данным
Интернет — это невероятный источник данных. Возможно, именно по этой причине данные стали доминирующей составляющей нашей социальной, экономической,
политической и даже творческой жизни. В главе 4 наши усилия по выпасу данных
концентрировались на процессе доступа и переформатирования файловых данных,
уже сохраненных на локальных устройствах или в сетевом облаке. В то же самое
время, бóльшая часть этих данных первоначально была получена из Интернета —
были ли они загружены с веб-сайта, как данные по безработице, или получены по
URL-адресу, как данные системы Citi Bike. Но теперь, когда мы знаем, как использовать средства Python для парсинга и преобразования разных файловых форматов,
настало время взглянуть на механизм первоначального получения этих файлов,
особенно если они содержат канальные данные реального времени. Для этого мы
посвятим бóльшую часть этой главы описанию получения доступа к данным, предоставляемым посредством API-интерфейсов, которые были вкратце упомянуты в
главе 4. Интерфейсы API — это основной (а иногда и единственный) способ получения доступа к данным, генерируемым службами реального времени или системами обслуживания по требованию, такими как платформы социальных сетей, потоковой музыки и поисковых систем, а также другими частными и официальными
(например, создаваемыми правительством) источниками данных.
Тогда как многие достоинства API-интерфейсов (см. врезку "Зачем использовать
API-интерфейсы" далее в главе, чтобы освежить свои знания на эту тему) делают
их популярным ресурсом для предоставления компаниями, собирающими данные,
эта практика чревата значительными издержками и рисками. Для бизнесов, полагающихся в своей деятельности на такую рекламу, как платформы социальных сетей, внешний продукт или проект, который собирает слишком большие объемы
данных, представляет собой риск относительно получения прибыли. Легкая доступность такого большого объема личных данных также значительно повышает
уровень угрозы для конфиденциальности (https://dataprivacylab.org/projects/
kanonymity/kanonymity.pdf). В результате этого для получения доступа к данным
посредством многих API-интерфейсов требуется предварительная регистрация в
системе сборщика данных и даже вход в систему с использованием кода или процесса аутентификации при каждом запросе данных. В то же самое время доступность данных, предоставляемая API-интерфейсами, является мощным инструмен-
174
|
Глава 5
том для улучшения прозрачности правительственных систем1 и ответственности
частных компаний2. Поэтому предварительная работа по созданию учетной записи
и защите любых ваших сценариев Python для доступа к данным на основе APIинтерфейсов определенно стоит затраченных усилий.
В этой главе мы обсудим, как посредством API-интерфейсов можно получить доступ к целому ряду наборов данных c веб-ресурсов. Мы рассмотрим все их разновидности, от базовых ресурсов, не требующих входа в систему, до высокозащищенных API-интерфейсов с многоэтапным процессом входа в систему, используемых такими социальными сетями, как, например, Twitter. Как мы увидим в разд.
"Доступ к веб-данным XML и JSON" далее в этой главе, более простой вариант
предполагает использование библиотеки запросов Python requests для загрузки вебстраницы, уже отформатированной как JSON или XML. Все, что нам нужно, — это
URL-адрес. В разд. "Специализированные API-интерфейсы: добавление простой
аутентификации" далее в этой главе мы перейдем к рассмотрению процесса получения доступа к данным, предоставляемым посредством API-интерфейса базы данных FRED (Federal Reserve Economic Database — база экономических данных федеральной резервной системы США). Это те же самые данные, которые мы просматривали, используя сценарии в листингах 4.16 и 4.19, но теперь вместо
использования предоставленных мною файлов мы будем программно загружать
данные, которые будут наиболее свежими на момент исполнения сценария.
Для этого на веб-сайте FRED потребуется создать как учетную запись, так и свой
собственный ключ API-интерфейса. Наконец, в разд. "Специализированные APIинтерфейсы: работа с протоколом OAuth" далее в этой главе мы рассмотрим более сложный процесс аутентификации для подключения к API-интерфейсу, требуемого для платформ социальных сетей наподобие Twitter. Несмотря на значительный объем требуемой предварительной работы, изучение, как программно
взаимодействовать с API-интерфейсами, подобными этому, принесет вам большую
отдачу. В частности, в большинстве случаев вы сможете в любое время извлечь
наиболее актуальные данные, предоставляемые этими службами, просто исполнив
эти сценарии, без какой бы то ни было их модификации3. Конечно же, поскольку
API-интерфейсы предоставляются не всеми источниками требуемых нам данных, в
заключительном разделе этой главы "Извлечение веб-данных: источник данных последней надежды" мы рассмотрим, как программно, с должной ответственностью,
извлекать (scrape) данные из веб-сайтов, используя библиотеку Beautiful Soup для
Python. Хотя во многих случаях эти задачи по получению доступа к данным можно
реализовать при помощи браузера и мыши, вы быстро увидите, как использование
1
Например, Министерством финансов США (https://fiscaldata.treasury.gov/api-documentation).
Два примера приводятся в статьях на веб-сайтах The Markup (https://themarkup.org/google-thegiant/2021/04/09/how-we-discovered-googles-social-justice-blocklist-for-youtube-ad-placements) и NPR
(https://npr.org/2021/08/04/1024791053/facebook-boots-nyu-disinformation-researchers-off-its-platformand-critics-cry-f).
3
Это также первый шаг к созданию своих приложений.
2
Доступ к интернет-данным
|
175
вспомогательных средств Python позволяет масштабировать наши усилия по извлечению данных, ускоряя этот процесс и делая его воспроизводимым.
Зачем использовать API-интерфейсы?
В разд. "Где искать канальные данные" в главе 4 мы коснулись некоторых
причин, по которым компании и организации предоставляют доступ к данным
посредством API-интерфейсов. В частности, когда люди для создания какоголибо продукта или услуги пользуются данными свободного доступа, получаемыми через API-интерфейс компании, они, по сути, предоставляют этой компании бесплатную рекламу торговой марки. Эти проекты на основе APIинтерфейса действуют как своего рода бесплатная служба по исследованиям и
разработкам для исходной компании: приложения, которые показывают хорошие результаты, указывают на потенциально прибыльное новое направление
развития бизнеса.
Но предоставление API-интерфейса для своих данных может быть привлекательным даже для правительственных и некоммерческих организаций. Эти организации
также извлекают выгоду в виде публичности и получения уникальных знаний, которые могут быть получены в результате использования их данных другими. С
точки зрения выпаса данных эти API-интерфейсы помогают поддерживать усилия
по обеспечению прозрачности, облегчая гражданским технологам (добровольцам
или работающим на другой основе) создание более удобных и доступных интерфейсов к информации, представляющей общественный интерес. Отделяя данные от
интерфейса для доступа к ним, API-интерфейсы предоставляют способ создания
инструментов для работы с общедоступными данными, которые более устойчивы и
совместимы с возможными будущими разработками.
Доступ к веб-данным XML и JSON
В разд. "Выпас канальных данных средствами языка Python" главы 4 мы исследовали процесс получения доступа и преобразования данных в двух распространенных форматах данных: XML и JSON. Однако мы не рассмотрели, как перенести эти
данные из Интернета на свой компьютер. Эта задача легко решается с помощью
разносторонней библиотеки requests для Python: получить доступ к данным и загрузить их на локальную машину можно посредством всего лишь нескольких строк
кода, без использования веб-браузера.
Для сравнения начнем с ручной загрузки двух файлов, которые мы использовали в
предыдущих примерах: файл статей из RSS-канала компании BBC, который мы
использовали в сценарии в листинге 4.17, и JSON-данные системы Citi Bike, которые мы упоминали во врезке "Один источник данных, два подхода" главы 4.
176
|
Глава 5
Для обоих этих источников данных процесс, по сути, одинаковый:
1. Открываем в браузере целевой URL-адрес, которым в данном случае является
один из следующих:
http://feeds.bbci.co.uk/news/science_and_environment/rss.xml
https://gbfs.citibikenyc.com/gbfs/en/station_status.json
2. Щелкаем правой кнопкой по странице, в открывшемся контекстном меню выбираем опцию Save As и сохраняем файл в той же самой папке, где находится наш
блокнот Jupyter или файл сценария Python.
Вот и все! Теперь можно исполнить сценарий в листинге 4.17 с этим файлом XML или
вставить данные JSON системы Citi Bike в форматер JSONLint (https://jsonlint.com),
чтобы увидеть, как они будут выглядеть, когда сформатированы должным образом.
Обратите внимание на то обстоятельство, что хотя страница со статьями компании
BBC выглядит в браузере как обычная веб-страница, согласно своему расширению
.xml она сохраняется как хорошо отформатированный код XML.
Теперь, увидев, как эта часть процесса выполняется вручную, посмотрим, как можно сделать то же самое при помощи средств Python. Для краткости сценарий в листинге 5.1 загружает и сохраняет один за другим оба файла.
Листинг 5.1. Сценарий data_download.py
#
#
#
#
#
Простой пример загрузки данных веб-сайта средствами Python,
используя библиотеку requests
Исходные данные загружаются из следующих URL-адресов:
http://feeds.bbci.co.uk/news/science_and_environment/rss.xml
https://gbfs.citibikenyc.com/gbfs/en/station_status.json
# Библиотека requests позволяет создавать код на Python, который функционирует
# подобно браузеру
import requests
# Имя нашего XML файла
XMLfilename = "BBC_RSS.xml"
# Открываем новый файл в режиме записи для сохранения в нем загруженных XML-данных
xml_output_file = open(XMLfilename,"w")
# Используем метод get() библиотеки requests для доступа к содержимому нашего
# целевого URL-адреса и сохраняем его переменной xml_data
xml_data = requests.get('http://feeds.bbci.co.uk/news/science_and_environment/rss.xml')
# Метод get() библиотеки requests помещает содержимое веб-страницы в свойство text,
# которое мы запишем непосредственно в файл xml_output_file
xml_output_file.write(xml_data.text)
Доступ к интернет-данным
|
177
# Закрываем выходной файл xml_output_file
xml_output_file.close()
# Выбранное имя файла JSON
JSONfilename = "citibikenyc_station_status.json"
# Открываем новый файл в режиме записи для сохранения в нем загруженных JSON-данных
json_output_file = open(JSONfilename,"w")
# Используем метод get() библиотеки requests для доступа к содержимому нашего
# целевого URL-адреса и сохраняем его переменной json_data
json_data = requests.get('https://gbfs.citibikenyc.com/gbfs/en/station_status.json')
# Метод get() извлекает содержимое веб-страницы и записывает ее текст
# непосредственно в выходной файл json_output_file
json_output_file.write(json_data.text)
# Закрываем выходной файл json_output_file
json_output_file.close()
Довольно просто, не так ли? За исключением разных имен файлов, файлы .xml и
.json, созданные сценарием в листинге 5.1, точно такие же, как и их соответствующие версии, которые мы загрузили вручную. Теперь, когда у нас есть работающий
должным образом сценарий загрузки данных, все, что нам нужно сделать, чтобы
получить самые последние данные с этих URL-адресов, — это просто снова исполнить его, и старые данные в созданных ранее файлах будут заменены новыми.
Загружать вручную или создать программу загрузки?
Почти на каждом этапе нашей работы по выпасу данных будут случаться моменты, когда нам нужно будет принять решения относительно способа выполнения определенной задачи: делать это вручную или же создать какую-либо
программу для ее выполнения. Например, в сценарии в листинге 4.19 мы не
пытались, чтобы наша программа выясняла что-либо о структуре файла JSON,
а просто сами посмотрели на данные, а затем создали соответствующий код.
Теперь, когда мы знаем, как загружать данные, используя средства Python, перед нами стоит еще один выбор: сохранять данные вручную или же создать
программы для выполнения этой задачи.
В большинстве случаев, на начальных этапах работы с данными я всегда рекомендую загружать пробный экземпляр данных вручную, просто потому, что
это обычно проще, чем создание сценария для этого. В особенности, если исходные данные имеют формат XML или JSON, то вы, вероятно, нашли их в
Интернете, и поэтому эти данные уже открыты в браузере.
178
|
Глава 5
На данном этапе мы можем просто щелкнуть правой кнопкой по странице и в
контекстном меню выбрать опцию сохранить данные на наше устройство, вместо того, чтобы запускать блокнот Jupyter или редактор кода, чтобы создать
код для загрузки этих данных. Поскольку нам нужно внимательно просмотреть эти данные, чтобы оценить их качество, такой подход к первой загрузки
практически всегда является разумным. Если же мы решим, что данные заслуживают более пристального внимания, мы, скорее всего, захотим автоматизировать процесс загрузки, создав для этого сценарий Python.
Знакомство с API-интерфейсами
До этого времени большинство нашей работы по выпасу данных концентрировалась на источниках данных, чье содержимое почти полностью контролируется поставщиком данных. Фактически содержимое файлов электронных таблиц и документов (и даже веб-страниц, содержащих данные XML и JSON, к которым мы
только что обращались в сценарии в листинге 5.1) может меняться в зависимости
от времени доступа к ним и мы не можем, по сути, никоим образом влиять на то,
какие данные они содержат.
В то же самое время большинство из нас привыкли обращаться к Интернету, чтобы
получить информацию, которая намного больше соответствует нашим требованиям. В процессе поиска информации зачастую наш первый шаг состоит в вводе ключевых слов или фраз в поисковую систему, ожидая получить в ответ список тщательно подобранных результатов, основанных (как минимум частично) на указанной комбинации терминов. Конечно же, мы не можем контролировать, какие вебстраницы доступны для поиска, но это настолько распространенный для большинства из нас процесс, что мы редко останавливаемся, чтобы подумать о том, что
происходит за его кулисами.
Несмотря на визуальные интерфейсы, поисковые системы в действительности являются всего лишь частным случаем API-интерфейсов. По сути, это просто вебстраницы, которые позволяют взаимодействовать с базой данных, содержащей информацию о веб-сайтах в Интернете, такую как их URL-адреса, названия, тексты,
изображения, видео и многое другое. При вводе поискового термина с последующим нажатием клавиши <Enter> поисковая система осуществляет запрос к своей
базе данных на предмет содержимого, которое в некотором отношении соответствует поисковому термину, а затем обновляет текущую веб-страницу, отображая на
ней список полученных результатов. Хотя специализированные API-интерфейсы,
предоставляемые платформами социальных сетей и другими онлайновыми службами, требуют аутентификации и структурирования поисков особым образом, поисковые системы и более специализированные API-интерфейсы обладают достаточно многими общими функциями, чтобы можно было узнать что-то полезное,
разобрав по частям базовый поиск системы Google.
Доступ к интернет-данным
|
179
Базовые API-интерфейсы
на примере поисковой системы
Хотя поисковая система в Интернете, вероятно, является самой простой формой
API, ее поведение на экране не всегда очевидно. Например, в результате поиска в
Google по фразе "weather sebastopol" на экране, скорее всего, отобразится страница
наподобие показанной на рис. 5.1.
Рис. 5.1. Пример результатов поиска
Хотя данный формат результатов поиска будет вам довольно знакомым, давайте
рассмотрим более подробно, что происходит в строке URL-адреса этой страницы.
В вашем случае его содержимое будет, вероятнее всего, иным, чем показанное на
рис. 5.1, но оно должно содержать по крайней мере некоторую одинаковую информацию. В частности, текст в строке URL должен содержать следующий фрагмент:
q=weather+sebastopol
Нашли его? Отлично. Теперь, не обновляя страницу, замените текст в строке поиска на "weather san francisco" и нажмите клавишу <Enter>. Опять проверьте текст в
строке URL; он должен содержать следующий фрагмент:
q=weather+san+francisco
Наконец, вставьте в строку URL следующий текст и нажмите клавишу <Enter>:
https://www.google.com/search?q=weather+san+francisco
Заметили что-либо? Если все сработало должным образом, то результаты поиска, полученные после ввода в строку поиска Google текста "weather san francisco", будут такими же (или почти такими же), как и результаты, полученные вследствие добавления
к URL поиска Google пары ключ/значение q=weather+san+francisco (создав, таким
образом, полный URL https://www.google.com/search?q=weather+san+francisco). Это
объясняется тем, что фрагмент q=weather+san+fransisco является частью строки запроса, которая доставляет наш фактический элемент поиска в базу данных Google.
Весь остальной текст — это просто дополнительная информация, добавляемая системой Google для целей специфичного оформления или отслеживания.
180
|
Глава 5
Хотя поисковая система Google может добавлять (и добавляет) в наш URL поиска
все, что ей заблагорассудится, добавлять в него полезные пары ключ/значение можем также и мы. Например, во врезке "Интеллектуальный поиск конкретных типов данных" главы 4 мы рассмотрели, как можно выполнить поиск файлов конкретного типа. Так, для поиска файлов типа .xml в поле поиска нужно добавить текст
filetype: .xml. Тот же самый результат можно получить, добавив в строку запроса в
поле URL соответствующую пару ключ/значение as_+filetype=xml:
https://www.google.com/search?q=weather+san+francisco&as_filetype=xml
Модифицированный таким образом URL не только возвратит результаты в требуемом формате, но также обновит соответствующим образом содержимое поля поиска.
В данной ситуации система поиска Google ведет себя почти почти так же, как и более специализированный API-интерфейс, который мы будем рассматривать в оставшейся части этой главы. Большинство API-интерфейсов имеют общую структуру, которую мы увидели в этом примере поиска. В частности, конечная точка (в
данном случае это https://www.google.com/search) объединяется с одним или несколькими параметрами запроса или парами ключ/значение (например,
as_filetype=xml или q=weather+san+fran cisco), которые составляют строку запроса,
добавляемую после символа вопросительного знака (?). На рис. 5.2 приводится общий обзор конечной точки API-интерфейса и структуры строки запроса.
Рис. 5.2. Базовая структура строки запроса
Тогда как это довольно универсальная структура, следующие две вариации строчных API-интерфейсов запросов могут оказаться полезными:
пары
ключ/значение
(например,
as_filetype=xml,
num=5
или даже
q=weather+san+francisco) могут указываться в любом порядке, при условии, что
они добавляются после символа вопросительного знака (?), обозначающего начало строки запроса;
конкретные ключи и значения, разрешенные для данного API-интерфейса, опре-
деляются поставщиком этого интерфейса. Их можно узнать из документации
для интерфейса или экспериментальным путем (хотя последний подход может
вызвать проблемы). Все элементы, добавляемые в строку запроса, которые не
являются действительным ключом или параметром, будут, скорее всего, игнорироваться системой.
Доступ к интернет-данным
|
181
Хотя эти характеристики присущи почти всем API-интерфейсам, для получения
допуска к данным подавляющее большинство этих интерфейсов требует сначала
выполнить идентификацию (или аутентификацию) пользователя, выполнив вход в
систему. Кроме этого, запросы к API-интерфейсу должны содержать уникальные
специализированные "ключи". Эту часть процесса API-интерфейсов мы и рассмотрим следующей.
Специализированные API-интерфейсы:
добавление простой аутентификации
При работе с большинством API-интерфейсов первым делом необходимо создать
какую-либо учетную запись у поставщика данного интерфейса. Хотя многие APIинтерфейсы предоставляют свои услуги бесплатно, процесс компилирования и
хранения данных и их поиска и возврата через Интернет сопряжен с рисками и стоит денег, поэтому поставщики хотят отслеживать пользователей своих APIинтерфейсов и в случае необходимости иметь возможность ограничивать доступ к
интерфейсу4. Первая часть процесса аутентификации обычно состоит из создания
учетной записи и получения ключа API для пользователя и/или для каждого проекта, программы или приложения, которое планируется использовать для взаимодействия с API-интерфейсом. В "базовом" процессе аутентификации для работы с APIинтерфейсом, подобном тому, который мы осуществим здесь, после создания своего ключа API-интерфейса на веб-сайте поставщика данной услуги для успешного
получения данных нужно лишь добавить этот ключ к запросу данных подобно любому другому параметру запроса.
В качестве примера рассмотрим, как организовать программный доступ к данным
по безработице, с которыми мы работали в сценарии в листинге 4.19. Для этого
первым делом нам нужно создать учетную запись на веб-сайте FRED и запросить у
них ключ для API-интерфейса. Для загрузки данных нам нужно просто добавить
полученный ключ в нашу строку запроса.
Получение ключа для API-интерфейса FRED
Чтобы создать учетную запись для базы данных FRED (Federal Reserve Economic
Database — база экономических данных федеральной резервной системы), откройте
ее веб-страницу по адресу https://fred.stlouisfed.org и щелкните по ссылке MY
ACCOUNT в правом верхнем углу (рис. 5.3).
Следуйте инструкциям в открывшемся всплывающем окне и создайте новую учетную запись или выполните вход, используя свою учетную запись Google. После
4
Большинство поставщиков API-интерфейсов могут закрыть доступ к своей услуге в любое время и
по любому поводу. Но обычно они это делают лишь в случае слишком большого количества отправляемых запросов в течение слишком короткого времени.
182
|
Глава 5
создания учетной записи щелчок по ссылке MY ACCOUNT открывает выпадающее меню, содержащее опцию API Keys, как показано на рис. 5.4.
Рис. 5.3. Ссылка для входа в базу данных FRED
Рис. 5.4. Опции меню MY ACCOUNT веб-сайта FRED
Щелчок по этой опции открывает страницу, на которой можно запросить один или
несколько ключей API, нажав кнопку Request API Key. В результате нажатия этой
кнопки откроется страница, на которой нужно предоставить краткое описание
(длиной в одно-два предложения) приложения, в котором будет использоваться запрашиваемый ключ API. На этой же странице под полем описания приложения
предлагается ознакомиться с условиями предоставления услуг и подтвердить факт
ознакомления, установив соответствующий флажок. Чтоб отправить запрос, нажмите кнопку Request API Key.
Запрос ключа API обычно удовлетворяется, и в результате открывается промежуточная страница, на которой отображается ваш https://research.stlouisfed.org/
useraccount/apikeys ключ API. Если закрыть эту страницу, то в дальнейшем просмотреть свои ключи API можно на странице по адресу https://research.stlouisfed.org/
useraccount/apikeys.
Использование ключа API для запроса данных
Получив ключ API, можно исследовать процесс запроса данных, которые мы использовали для сценария в листинге 4.19. Для начала вставьте следующий адрес в
поле URL браузера:
https://api.stlouisfed.org/fred/series/observations?series_id=U6RATE&file_type=json
Доступ к интернет-данным
|
183
Даже если вы в настоящее время выполнили вход на веб-сайт FRED в используемом браузере, вы получите сообщение об ошибке наподобие следующего:
{"error_code":400,"error_message":"Bad Request. Variable api_key is not set.
Read https:\/\/research.stlouisfed.org\/docs\/api\/api_key.html for more
information."}
Это сообщение об ошибке довольно информативное: оно не только извещает о том,
что что-то пошло не так, но также содержит информацию, что могло вызвать эту
ошибку. В частности: Variable api_key is not set. Таким образом, чтобы исправить
ошибку, нам нужно лишь добавить наш ключ API в запрос как дополнительный
параметр:
https://api.stlouisfed.org/fred/series/observations?series_id=U6RATE&file_type=json&
api_key= ВАШ_КЛЮЧ_API
Должно быть очевидным, что ключ API вставляется вместо текста ВАШ_КЛЮЧ_API. Теперь в браузере должно отобразиться содержимое наподобие следующего:
{"realtime_start":"2021-02-03","realtime_end":"2021-02-03","observation_start":
"1600-01-01","observation_end":"9999-12-31","units":"lin","output_type":1,
"file_type":"json","order_by":"observation_date","sort_order":"asc","count":324,
"offset":0,"limit":100000,"observations":[{"realtime_start":"2021-02-03",
"realtime_end":"2021-02-03","date":"1994-01-01","value":"11.7"},
...
{"realtime_start":"2021-02-03","realtime_end":"2021-02-03","date":"2020-12-01",
"value":"11.7"}]}
Довольно ловко, не так ли? Теперь, когда мы знаем, как использовать наш ключ
API для создания запросов на получение данных, рассмотрим, как индивидуализировать эти запросы, а также, как защитить свой ключ API при его использовании в
сценариях Python.
Чтение документации по API-интерфейсу
Как мы видели в предшествующем примере, имея ключ API, можно получать самые последние данные из базы данных FRED в любое требуемое нам время, просто
создав строку запроса и добавив в нее наш ключ API.
Но как узнать, какие пары ключ/значение разрешены в API-интерфейсе базы данных FRED и какую информацию они возвращают? Единственный надежный способ для этого — прочитать документацию по API-интерфейсу, которая должна содержать руководство и (будем надеяться) примеры по его использованию.
К сожалению, для документации по API-интерфейсам не существует общепринятого стандарта, поэтому использование нового API-интерфейса всегда представляет
собой процесс проб и ошибок, особенно в случае некачественной документации
или отсутствия требуемой информации в предоставляемых примерах. Более того,
даже найти документацию для определенного API-интерфейса не всегда просто, и
зачастую поиск документации проще осуществить в сети.
184
|
Глава 5
Например, чтобы с домашней страницы базы данных FRED (https://
fred.stlouisfed.org/) добраться до документации по ее API-интерфейсу, нужно
щелкнуть по вкладке Tools, которая находится в панели вкладок, расположенной
посредине страницы, а затем из предлагаемых опций выбрать ссылку Developer
API, в результате чего откроется страница с содержанием документации по этому
API-интерфейсу (https://fred.stlouisfed.org/docs/api/fred). А вот поиск в сети по
фразе "fred api documentation" сразу же выдаст страницу документации API-интерфейса базы данных FRED (рис. 5.5).
Рис. 5.5. Страница содержимого документации по API-интерфейсу базы данных FRED
К сожалению, список ссылок на этой странице в действительности является списком конечных точек, т. е. разных базовых адресов URL, используемых для запроса более конкретной информации. (Вспомним, что конечная точка — это все,
что находится в адресе URL до символа вопросительного знака (?), который отделяет ее от строки запроса.) В предшествующем примере для получения данных из
базы данных FRED мы взяли конечную точку https://api.stlouisfed.org/fred/
series/observations, объединили ее с парами ключ/значение series_id=U6RATE,
file_type=json, а затем добавили наш ключ API.
Прокрутим страницу содержимого документации до раздела Series, а затем щелкнем по ссылке "fred/series/observations". В результате откроется страница
https://fred.stlouisfed.org/docs/api/ fred/series_observations.html, содержащая список всех действительных ключей (или параметров) запроса для данной конечной
точки, а также действительные значения для этих ключей и несколько примеров
URL запросов (рис. 5.6).
Количество возвращаемых наблюдений можно ограничить, используя параметр
limit, а упорядочить возвращаемые результаты в обратном порядке можно, добавив
Доступ к интернет-данным
|
185
пару ключ/значение sort_order=desc. Можно также указать возвращение данных в определенном формате (например, в XML, используя пару ключ/значение file_type=xml)
или с использованием определенных единиц (например, отобразить вывод в виде
процентного изменения по отношению к прошлому году можно, используя пару
ключ/значение units=pc1).
Рис. 5.6. Страница содержимого документации
для конечной точки observations API-интерфейса базы данных FRED
Путешествие в прошлое
Приблизительно в то же самое время, когда я открыла, что показатель безработицы U3 не охватывает многих людей, которые, вероятно, считали (и почти
определенно ощущали) себя безработными, я также узнала еще кое-что о публикации экономических индикаторов: их значения часто подвергаются последующему изменению. После нескольких месяцев моей работы с данными экономических показателей один из моих коллег мимоходом упомянул, что скорость роста валового внутреннего продукта (ВВП) США за предыдущий
квартал была официально исправлена. "Что?" — подумала я. Учитывая то обстоятельство, что ВВП часто используется в национальной экономической политике, мысль о том, что этот индикатор можно было просто изменить несколько месяцев после его публикации, была просто шокирующей.
186
|
Глава 5
Например, спад экономической деятельности официально определяется как
"отрицательный рост" в течение нескольких последовательных кварталов, когда ВВП следующего квартала меньше предыдущего по крайней мере два раза
подряд. Но какие последствия пересмотр значений этих показателей постфактум может иметь для реальных людей, особенно для тех, которые начинают
чувствовать эффекты спада еще задолго до публикации первого варианта этих
данных?
И хотя я ничего не могу поделать с фактом существования таких корректировок, они по крайней мере отслеживаются в базе данных FRED, и все эти изменения можно увидеть, используя ее API-интерфейс. Параметр vintage_dates для
конечной точки observations (https://fred.stlouisfed.org/docs/api/fred/series_
observations.html) как раз и используется для этой цели. Например, получить
значение показателя безработицы U6 состоянием на май 2020 можно, исполнив запрос со следующим URL-адресом:
https://api.stlouisfed.org/fred/series/observations?series_id=U6RATE&
file_type=json&sort_order=desc&limit=3&vintage_dates=2020-05-31
&api_key=YOUR_API_KEY_HERE
Можно увидеть, что в конце мая значение показателя безработицы U6 за
март 2020 г. было 8,7%, но затем оно было откорректировано до 8,8%. Значение этого же показателя за апрель сначала было 22,8%, но затем было откорректировано до 22,9%. И хотя эти корректировки могут выглядеть незначительными, следует иметь в виду, что уровень безработицы часто меняется не
больше, чем на несколько десятых процента в ту или иную сторону в течение
всего года.
Самое главное, эти изменения иллюстрируют одну из наиболее тонких проблем, связанных с целостностью данных: совпадает ли текущая информация с
той, которая была доступна в течение исследуемого периода времени? Если
нет, то необходимо должным образом откорректировать наши утверждения, а
может быть, и сами данные.
Защита своего ключа API
при использовании сценариев Python
Как вы уже могли догадаться, для загрузки данных с веб-сайта FRED (или с другого подобного API-интерфейса) в примере в листинге 5.1 нужно всего лишь заменить один из адресов URL полной строкой запроса, поскольку создаваемая им вебстраница — это всего лишь один из многих файлов JSON в Интернете.
В то же самое время этот запрос содержит особенно критичную информацию: наш
ключ API. Помните: что касается FRED (или владельца любого другого API-интерфейса), ответственность за любую деятельность с использованием вашего ключа
API несете вы. Это означает, что в то время как вы документируете, сохраняете и
Доступ к интернет-данным
|
187
управляете версиями своего кода при помощи средств наподобие Git, нельзя допустить, чтобы ваши ключи API или другие учетные данные попали в файл, к которому могут получить доступ сторонние лица.
Чтобы должным образом защитить свой ключ API, необходимо приложить некоторые усилия. Если вы новичок в области работы с данными,
языком Python или API-интерфейсами (или со всеми тремя), вы можете
поддаться искушению пропустить следующие пару разделов и просто
оставить свои учетные данные для API-интерфейса в файлах, которые
могут быть загруженными в Интернет5. Не следует поддаваться этому
соблазну! В данный момент вы можете думать: "Да кому нужно утруждать себя просмотром моей работы?" или: "Я всего лишь экспериментирую в любом случае, что с того, если кто-то и узнает эти учетные данные?". Но вам следует знать две вещи.
Во-первых, как и в случае с документацией, если вопрос защиты своих
учетных данных не решить должным образом сейчас, сделать это в будущем будет намного дольше и потребует больших усилий, потому что
вы можете забыть, с чем именно вы имеете дело, или потому, что к тому
времени это может быть уже слишком поздно. Во-вторых, обычно мало
кто из нас считает, что мы делаем что-то "важное" или достаточно заметное, чтобы оно представляло интерес для кого-либо другого. Но реальность такова, что злоумышленникам все равно, на кого повесить ответственность за свои деяния, и если вы представляете для них легкую цель,
они могут избрать для этого именно вас. Но последствия могут не ограничиваться запретом доступа к платформе данных. В 2021 г. бывший
директор компании SolarWinds заявил, что масштабный несанкционированный доступ к тысячам систем с повышенным уровнем защиты посредством взлома программного обеспечения компании был отчасти
возможным вследствие слабого пароля, который был загружен в файл
личной учетной записи на GitHub одного стажера6. Поэтому, даже если
вы "просто практикуетесь", будет не лишним с самого начала начать
практиковать и навыки безопасности.
Процесс защиты учетных данных для API-интерфейса состоит из двух частей:
1. Ключ API или другую конфиденциальную информацию нужно хранить отдельно от остального кода. В нашем случае мы будем сохранять эти учетные данные
5
Например, при сохранении своего кода командой git push.
См. статьи Hackers Used SolarWinds' Dominance against It in Sprawling Spy Campaign, авторы Raphael
Satter, Christopher Bing и Joseph Menn (https://reuters.com/article/global-cyber-solarwinds/hackers-atcenter-of-sprawling-spy-campaign-turned-solarwinds-dominance-against-it-idUSKBN28P2N8) и Former
SolarWinds CEO Blames Intern for solarwinds123 Password Leak, авторы Brian Fung и Geneva Sands
(https://www.cnn.com/2021/02/26/politics/solarwinds123-password-intern).
6
188
|
Глава 5
в отдельном файле, который загружается основным кодом только при исполнении сценария.
2. Нужно создать надежный способ, чтобы ни при каких обстоятельствах не допустить копирования этого файла с учетными данными в какое бы то ни было сетевое хранилище при выполнении резервного копирования кода, используя, например, систему Git.
Для этого мы вставим слово credentials в название любого файла, который содержит наши учетные данные, а затем используем файл gitignore, чтобы не допустить
загрузки этих файлов на GitHub.
Самый простой способ для надежного достижения обеих этих целей — определить
соглашение по именованию всех файлов, содержащих ключи API или иную конфиденциальную информацию, связанную с выполнением входа в систему. В нашем
случае таким способом будет обязательная проверка на наличие слова credentials в
названии любого такого файла. Затем мы создадим или обновим специальный файл
Git .gitignore, который содержит правила, указывающие системе Git, какие файлы в
папке репозитория никогда не должны подвергаться операции коммита и/или загрузке на GitHub. Добавив в этот файл правило для нашего файла учетных данных
(который содержит слово credentials в названии), мы гарантируем, что никакой
файл, содержащий конфиденциальную информацию для входа в систему, не будет
случайно загружен на GitHub.
Создание файла учетных данных
До сих пор мы размещали весь код для выполнения определенной задачи (например, загрузки или преобразования файла с данными) в один файл Python или в
блокнот. Но чтобы обеспечить как безопасность, так и возможность многократного
использования, при работе с API-интерфейсами будет гораздо разумнее отделить
рабочий код от учетных данных. К счастью, это очень простой процесс.
Сначала создадим и сохраним пустой файл Python с названием FRED_credentials.py.
Для простоты сохраним этот файл в той же самой папке, в которой будет находиться код Python для загрузки данных с базы данных FRED.
Затем создадим новую переменную и присвоим ей значение нашего ключа API для
этой базы данных, как показано в листинге 5.2.
Листинге 5.2. Пример содержимого файла для хранения учетных данных для доступа
к базе данных FRED
my_api_key = "ваш_ключ_api_в_двойных_кавычках"
В завершение, сохраните файл с ключом.
Доступ к интернет-данным
|
189
Использование учетных данных
в отдельном сценарии
Ключ API, сохраненный в виде переменной в отдельном файле, можно импортировать в любой сценарий, требующий этого ключа, используя тот же самый способ,
посредством которого мы ранее импортировали библиотеки сторонних разработчиков. В листинге 5.3 приводится код сценария для загрузки данных индикатора безработицы U6 из базы данных FRED, используя наш ключ API, хранящийся в отдельном от сценария файле FRED_credentials.py.
Листинг 5.3. Сценарий FRED_API_example.py
# Импортируем библиотеку requests
import requests
# Импортируем наш ключ API
from FRED_credentials import my_api_key
# Указываем требуемую конечную точку веб-сайта FRED
FRED_endpoint = "https://api.stlouisfed.org/fred/series/observations?"
# Также задаем параметры запроса и их значения
FRED_parameters = "series_id=U6RATE&file_type=json"
# Создаем полный URL-адрес для нашего запроса API, добавляя наш ключ API в конец
complete_data_URL = FRED_endpoint + FRED_parameters +"&api_key="+my_api_key
# Открываем новый файл в режиме записи для сохранения в нем загруженных JSON-данных
FRED_output_file = open("FRED_API_data.json","w")
# Используем метод get() библиотеки request для получения содержимого нашего целевого
# URL-адреса и сохраняем его в переменной FRED_data
FRED_data = requests.get(complete_data_URL)
# Записываем содержимое веб-страницы в текстовый файл
FRED_output_file.write(FRED_data.text)
# Закрываем выходной файл FRED_output_file
FRED_output_file.close()
Доступ сценария к нашему ключу API осуществляется при помощи ключевого
слова from и указания названия нашего файла, содержащего этот ключ (обратите
внимание на то, что расширение файла .py не указывается), после чего осуществляется импортирование значения переменной, используя ключевое слово import.
190
|
Глава 5
Что такое pycache?
Возможно, вы заметили, что при исполнении сценария в листинге 5.3 не только загружаются данные FRED, но также создается новая папка __pycache__,
которая содержит один файл, называющийся FRED_credentials.cpython38.pyc
(или что-то похожее). Откуда взялся этот файл и что он собой представляет?
При импортировании кода из другого файла Python он сначала преобразовывается в так называемый байткод, который, собственно, и используется устройством для исполнения сценария Python7. Файлы с байткодом Python имеют расширение .pyc. Даже если вам еще не приходилось работать непосредственно с
такими файлами, ваше устройство уже содержит большое количество файлов
.pyc, связанных с импортируемыми библиотеками. Но они хранятся в удаленной части файловой системы, посещать которую нам обычно нет надобности,
поэтому мы их и не замечаем. В обоих случаях система сохраняет ресурсы,
преобразовывая содержимое импортируемых файлов в байткод и сохраняя его
в файле .pyc только один раз, а не при каждом исполнении сценария. Как при
импортировании библиотек, так и наших учетных данных, система полагается
на байткод этих ресурсов, чтобы немного ускорить исполнение сценариев.
К счастью, нам ничего не нужно делать ни с папкой __pycache__, ни с содержащимся в ней файлом .pyc. При желании их можно даже удалить (они появятся снова после следующего исполнения сценария Python, который импортирует ваши учетные данные). Поскольку размер файла с учетными данными
очень незначительный, создание его байткода при каждом исполнении сценария никоим образом не скажется на скорости исполнения. С другой стороны,
если его присутствие вас не беспокоит, то можете просто оставить его в покое.
Теперь, когда мы знаем, как отделить наш ключ API от главной части кода, нам
нужно позаботиться о том, чтобы не допустить случайной загрузки файла с этим
ключом в Интернет при выполнении операции git commit и/или git push. Простым
и эффективным способом обеспечить такую безопасность будет использование
специального файла .gitignore.
Основы работы с файлом .gitignore
Как можно видеть из его названия, файл .gitignore позволяет указывать файлы, которые должны игнорироваться при работе с Git. Создавая (или модифицируя) для
репозитория в файле .gitignore правила сопоставления образцу, можно заранее определить типы файлов, которые наш репозиторий будет отслеживать или выгружать во внешнее хранилище. Хотя эту задачу можно решить вручную (никогда не
7
Вот почему программа, которая обеспечивает исполнение сценариев Python, часто называется
интерпретатором — она преобразовывает созданный людьми исходный код сценария в байтовый
код, который понимает ваше устройство.
Доступ к интернет-данным
|
191
используя команду git add с файлами, которые мы хотим исключить из отслеживания), использование файла .gitignore гарантирует такое поведение8, а также предотвращает вывод предупреждений системой Git о наличии неотслеживаемых файлов
при каждом исполнении команды git status. При отсутствии файла .gitignore при
каждом коммите нам нужно было бы подтверждать, какие файлы мы хотим игнорировать, что крайне утомительно и может привести к ошибкам. Лишь одной выполненной в спешке команды git add –A будет достаточно, чтобы начать отслеживать наши конфиденциальные файлы с учетными данными. А удаление материла из
истории Git сопряжено с намного бóльшими сложностями, чем их добавление. Поэтому будет разумнее выполнить немного подготовительной работы, дабы избежать возможных проблем.
Иными словами, файл .gitignore — это наш друг, который позволяет нам создавать
общие правила, не допускающие случайного отслеживания файлов, которые мы не
хотим отслеживать. Это также обеспечивает выдачу уведомлений системой Git о
статусе только тех файлов, для которых мы хотим их получать.
На данный момент мы создадим новый файл .gitignore в той же самой папке/репозитории, в котором находится наш файл FRED_credentials.py, просто для того, чтобы разобраться, как он работает9. Для этого создайте новый файл в редакторе кода
Atom (или добавьте новый файл непосредственно в репозитарий GitHub) и сохраните его под именем .gitignore в той же самой папке, в которой находится файл
FRED_credentials.py. При этом обратите внимание на важный факт — имя этого
файла должно начинаться с точки (.).
Затем вставьте в этот файл следующий код:
# Игнорируем все файлы, содержащие учетные данные
**credentials*
Как и в сценариях Python, комментарии в файлах .gitignore начинаются с символа
решетки (#), поэтому первая строка в этом файле просто описывает его содержимое. А код во второй строке (**credentials*) — это что-то типа регулярного выражения, т. е. специальной системы сопоставления образцу, позволяющей описывать
строки (включая названия файлов) таким образом, как мы бы объясняли это другому человеку10. В данном случае выражение **credentials* означает "любой файл в
этом репозитории, который содержит слово credentials". Добавление этой строки в
наш файл .gitignore обеспечивает, что любой файл в этом репозитории, чье название содержит слово credentials, никогда не будет отслеживаться или загружаться на
GitHub.
8
Даже при исполнении команды, скажем, git add –A или git commit –a.
Репозиторий может содержать несколько файлов .gitignore в разных папках. Подробную информацию по этому вопросу см. в документации (https://git-scm.com/docs/gitignore).
10
Этот подход был использован с библиотекой glob в листинге 4.20. Мы исследуем его более подробно в разд. "Регулярные выражения: супермощное средство сопоставления строк" главы 7.
9
192
|
Глава 5
Чтобы увидеть наш файл .gitignore в действии, сохраним его, а затем в терминале
исполним следующую команду:
git status
Вывод результатов исполнения этой команды должен содержать новый файл
FRED_credentials.py для кода сценария в листинге 5.3, но он не должен указываться
как untracked (неотслеживаемый). Чтобы убедиться в том, что файлы, которые
нужно игнорировать при резервном копировании Git, действительно игнорируются,
можно выполнить следующую команду:
git status --ignored
Вывод этой команды будет содержать список файлов в репозитории, которые в настоящее время игнорируются. Кроме файла FRED_API_example.py, этот список,
скорее всего, будет содержать папку __pycache__, для которой также не нужно выполнять копирование в репозиторий.
Куда пропал наш файл .gitignore?
После сохранения и закрытия файла .gitignore он может не отображаться в
файловой системе. Поэтому обнаружение этого файла на своем устройстве посредством как визуального файлового интерфейса, так и средствами командной строки, может представлять определенную проблему.
Это объясняется тем, что по умолчанию большинство операционных систем
скрывают все файлы, название которых начинается с точки. И хотя файл
.gitignore всегда можно легко найти (и редактировать) на веб-сайте GitHub,
сделать это на своем устройстве можно, использовав несколько приемов, облегчающих его обнаружение и работу с ним.
Наш файл .gitignore является одним из нескольких файлов на вашем устройстве, чьи названия начинаются с точки, которые вам когда-либо понадобится
просматривать и редактировать. Поэтому проще всего его найти будет, используя командную строку, перейдя в соответствующую папку или репозиторий (команда cd) и исполнив команду для вывода списка всех объектов в данной папке:
ls -a
Если окажется, что папка не содержит файла .gitignore, его можно создать. Но
если такой файл уже есть в папке, то как можно его редактировать? Скорее
всего, вы запускали редактор кода Atom, щелкая по его иконке, но этот редактор также можно запустить из командной строки посредством ключевого слова atom (подобно запуску на исполнение сценариев Python, используя ключевое
слово python или py). Таким образом, открыть файл .gitignore для его редактирования в Atom можно, исполнив следующую команду:
atom .gitignore
Исполнение команды может занять несколько секунд (даже при наличии других файлов, открытых в Atom), но использование командной строки для запуска программ является легким способом нахождения и редактирования файлов,
даже если это скрытые файлы.
Доступ к интернет-данным
|
193
Специализированные API-интерфейсы:
работа с протоколом OAuth
На данном этапе мы располагаем всем необходимым для работы с API-интерфейсами, в которых используется так называемый базовый процесс аутентификации: мы создаем учетную запись с поставщиком API-интерфейса и получаем ключ
API, который мы добавляем в конец нашего запроса данных, как это делается в
листинге 5.3.
Хотя это очень простой процесс, он все же обладает несколькими недостатками.
Современные API-интерфейсы способны на большее, чем просто возвращать данные: они также используются приложениями для, например, публикации обновлений в учетных записях социальных сетей или добавления элементов в онлайновое расписание. Чтобы эти приложения могли выполнять такие операции, они
требуют какого-либо доступа к соответствующей учетной записи. Но вы вряд ли
захотите предоставлять свое учетные данные приложениям и программам. Если
эту информацию предоставить приложениям просто так, то запретить приложению доступ к своей учетной записи в будущем можно будет, только изменив свое
имя пользователя и пароль. Однако в этом случае вам нужно будет предоставить
новые учетные данные тем приложениям и программам, с которыми вы хотите
продолжать работу. Таким образом ситуация может стать очень сложной и запутанной достаточно быстро.
Рабочий процесс аутентификации OAuth предназначен для решения этой проблемы: предоставляя способ доступа к API-интерфейсу без необходимости передачи
имени пользователя и пароля. Это достигается путем создания сценария так называемого цикла авторизации, состоящего из трех базовых шагов:
1. Получаем и кодируем ключ API и "секрет" (каждый из которых представляет
собой простую строку, предоставляемую поставщиком API-интерфейса, как в
случае с ключом API для базы данных FRED).
2. Отправляем закодированную информацию этих двух строк как еще один ключ
специальной авторизующей конечной точки (URL).
3. Получаем токен доступа (еще одна строка) от этой авторизующей конечной точки. Именно этот токен доступа мы, собственно, и отправляем конечной точке
данных API-интерфейса вместе с информацией запроса.
Хотя этот процесс может поначалу выглядеть довольно запутанным, на практике
бóльшая часть его сводится к обмену строками с определенными URL-адресами в
определенном порядке. Да, по ходу обмена строками нам нужно будет выполнять
определенный объем кодирования их, но, как вы уже могли догадаться, эта задача
будет выполняться для нас соответствующей библиотекой Python.
Несмотря на необходимость завершить цикл авторизации и получить токен доступа, процесс взаимодействия с этими более специализированными API-интерфейсами при помощи средств Python, по сути, такой же, как и тот, который мы рассмот-
194
|
Глава 5
рели в разд. "Специализированные API-интерфейсы: добавление простой аутентификации" ранее в этой главе. А именно, создаем учетную запись, запрашиваем
учетные данные для API, а затем создаем файл, который содержит эти учетные
данные и предварительно обрабатывает их, чтобы их можно было использовать в
нашем основном сценарии. Затем наш основной сценарий извлечет эти учетные
данные, использует их для запроса данных у нашей целевой платформы, а затем
запишет полученные данные в файл результатов.
В этом примере мы будем работать с API-интерфейсом платформы Twitter, но приблизительно тот же самый процесс можно применять и с другими платформами,
использующими подход OAuth (например, Facebook). Но мы не будем уделять
слишком много времени обсуждению организации конкретных запросов, поскольку для подробной информации о любом API-интерфейсе может потребоваться отдельная книга. Тем не менее, освоив процесс аутентификации, вы будете обладать
необходимыми знаниями, чтобы начать экспериментировать с целым рядом других
API-интерфейсов, и начать практиковаться с ними, чтобы получить требуемые вам
данные. Итак, приступим к работе.
Получение учетной записи разработчика Twitter
Нашим первым шагом (как и в случае с большинством других API, возможностями
которых мы хотим воспользоваться) будет запрос к Twitter на получение ключа
API. Даже если у вас уже есть учетная запись в Twitter, вам все равно нужно подать
заявку на получение "доступа разработчика". Этот процесс в целом займет около 15
минут (без учета времени на проверку и/или одобрение вашей заявки компанией).
Первым делом откройте страницу подачи заявки на доступ к API-интерфейсу разработчика Twitter (https://developer.twitter.com/en/apply-for-access) и нажмите на
ней кнопку Apply for a developer access. После входа в систему (если он еще не
был выполнен) вам будет предложено предоставить дополнительную информацию
о том, как вы планируете использовать данный API-интерфейс. Для данного упражнения можно выбрать опции Hobbyist и Exploring the API, как показано на
рис. 5.7.
На следующем этапе нужно будет предоставить описание планируемого использования API-интерфейса объемом свыше 200 символов. Здесь можно ввести нечто
наподобие следующего:
Using the Twitter API to learn more about doing data wrangling with Python.
Interested in experimenting with OAuth loops and pulling different kinds
of information from public Twitter feeds and conversations.
(Использование API-интерфейса Twitter необходимо, чтобы узнать больше
о выпасе данных с помощью средств Python. Заинтересован(а)
в экспериментировании с циклами аутентификации OAuth и получении
разного рода информации из общедоступных каналов и разговоров в Twitter.)
Доступ к интернет-данным
|
195
Рис. 5.7. Выбор планируемого использования API-интерфейса разработчиком Twitter
Поскольку наша цель в данном упражнении — практиковаться в загрузке данных
с платформы Twitter, используя сценарий Python и цикл аутентификации OAuth,
переключатели для всех четырех последующих вопросов можно установить на
No, как показано на рис. 5.8. Но если вы захотите использовать API-интерфейс
Twitter для других целей, вам нужно будет ответить на эти вопросы должным образом.
Рис. 5.8. Выбор планируемого использования API-интерфейса разработчиком Twitter
На следующих двух экранах проверьте выбранные вами опции и установите флажок, чтобы принять Соглашение с разработчиком. Затем нажмите кнопку Submit
196
|
Глава 5
application, в результате чего вам будет отправлено сообщение с предложением
верифицировать адрес своей электронной почты. Если в течение нескольких минут
это сообщение не появится в папке Входящие вашего почтового ящика, проверьте
папки Spam и корзины. Для подтверждения своей электронной почты щелкните по
соответствующей ссылке в этом сообщении.
Создание приложения и учетных данных Twitter
После одобрения Twitter вашего доступа разработчика можно приступать к созданию своего приложения. Для этого нужно войти в свою учетную запись Twitter и
открыть страницу https://developer.twitter.com/en/portal/projects-and-apps. В центре этой страницы нажмите кнопку Create Project.
В результате последует процесс наподобие процесса подачи заявки на получение
доступа разработчика. В частности, нужно будет предоставить название для своего
проекта, указать планируемое использование API-интерфейса Twitter, дать краткое
описание планируемого использования, а также предоставить имя первого приложения (app), связанного с этим проектом. Соответствующие снимки экранов приводятся на рис. 5.95.12.
Рис. 5.9. Экран присвоения названия проекту Twitter
Доступ к интернет-данным
Рис. 5.10. Экран указания цели проекта Twitter
Рис. 5.11. Экран описания проекта Twitter
|
197
198
|
Глава 5
Рис. 5.12. Экран названия приложения проекта Twitter
Рис. 5.13. Экран с информацией о ключах API и токене
Доступ к интернет-данным
|
199
По завершении этого процесса отобразится экран с вашим ключом API, секретным
ключом API и токен носителя (bearer token), как показано на рис. 5.1311.
Что такое, в конце концов, "приложение" (app)?
Для большинства из нас слово "приложение" вызывает ассоциацию с играми и
услугами, доступными на телефоне, а не с использованием средств Python для
извлечения данных посредством API-интерфейса. Но для большинства поставщиков API-интерфейсов приложением (app) является все, что взаимодействует программным образом с их службами, будь то сценарий Python для загрузки сообщений или полномасштабное мобильное приложение, которое
пользователи могут устанавливать на свои устройства. Тем не менее, когда вы
получите свою учетную запись разработчика и начнете загружать сообщения
Twitter, вы можете честно говорить, что являетесь разработчиком приложений
(app). С чем вас и поздравляем!
В целях безопасности вы сможете просматривать свой ключ API (API key) и секретный ключ API (API secret key) только на этом экране, поэтому мы сразу же сохраним их в нашем файле для учетных данных Twitter. (Обратите внимание на то,
что в других местах инструментальной панели разработчика эти учетные данные
называются API Key (ключ API) и API Key Secret (секрет ключа API), — как видите, даже у крупных технических компаний могут быть проблемы с использованием
постоянной терминологии.) Но не стоит волноваться из-за этого. Если вы случайно
уйдете с этого экрана, неправильно скопируете ключи или допустите какую-либо
другую ошибку, то всегда можно возвратиться на инструментальную панель
(https://developer.twitter.com/en/portal/dashboard) и щелкнуть по значку ключа
справа в поле приложения (рис. 5.14).
Затем в разделе Consumer Keys нажмите кнопку Regenerate, чтобы получить новый ключ API и ключ секрета FPI (рис. 5.15).
Теперь нам нужно сохранить ключ API и ключ секрета API для нашего приложения
в новом файле учетных данных, подобном тому, который мы создали для хранения
ключа API для базы данных FRED. Для этого создайте новый файл
Twitter_credentials.py в папке, в которой будет находится ваш сценарий Python для
извлечения данных Twitter, и вставьте в него свой ключ API и ключ секрета API,
как показано в листинге 5.4.
Листинг 5.4. Сценарий Twitter_credentials.py
my_Twitter_key = "ваш_ключ_api_в_двойных_кавычках"
my_Twitter_secret = "ваш_ключ_секрета_api_в_двойных_кавычках"
11
Эти ключи были заменены и больше не действительны.
200
|
Глава 5
Рис. 5.14. Инструментальная панель Twitter с приложениями
Рис. 5.15. Экран разработчика Twitter для создания новых ключей
Доступ к интернет-данным
|
201
Непременно включите слово credentials в название файла, в котором
хранятся ваши ключ API и ключ секрета API для Twitter. Вспомним, что
в разд. "Основы работы с файлом .gitignore" ранее в этой главе мы
создали правило для игнорирования при резервном копировании любых
файлов, содержащих в своем названии слово credentials, чтобы ни при
каких обстоятельствах не допустить случайной загрузки наших ключей
API на сервер GitHub. Поэтому обязательно перепроверьте правильность написания названия файла с учетными данными. Для перестраховки можно выполнить следующую проверку в командной строке,
чтобы подтвердить, что все файлы с учетными данными действительно
игнорируются для целей резервного копирования:
git status --ignored
Кодирование ключа API и ключа секрета API
До сих пор нам не нужно было делать ничего другого, что мы не делали в случае с
API-интерфейсом базы данных FRED. Единственная несущественная разница —
вместо одной переменной в нашем файле учетных данных мы создали две переменных: my_Twitter_key и my_Twitter_secret.
Но теперь, прежде чем приступать к следующему шагу в процессе аутентификации,
нам нужно немного поработать со значениями этих переменных, чтобы преобразовать их в правильный формат. Я не буду особо вдаваться в подробности происходящего "под капотом" в следующих шагах; будет достаточным сказать, что эти
операции кодирования и декодирования необходимы для защиты исходных строковых значений наших ключа API и ключа секрета API, чтобы их можно было безопасно пересылать по Интернету.
Для этого в наш файл Twitter_credentials.py мы добавим несколько строк кода, в
результате чего его содержимое будет выглядеть, как показано в листинге 5.5.
Листинг 5.5. Модифицированный файл Twitter_credentials.py
my_Twitter_key = "ваш_ключ_api_в_двойных_кавычках"
my_Twitter_secret = "ваш_ключ_секрета_api_в_двойных_кавычках"
# Импортируем библиотеку base64
import base64
# Сначала объединяем ключ API и ключ секрета API в одну строку, разделяя их двоеточием
combined_key_string = my_Twitter_key+':'+my_Twitter_secret
# Обрабатываем эту объединенную строку методом encode(), указывая формат ASCII
# (см.: https://en.wikipedia.org/wiki/ASCII)
encoded_combined_key = combined_key_string.encode('ascii')
202
|
Глава 5
# Кодируем полученную строку в формате ASCII с использованием base64
b64_encoded_combined_key = base64.b64encode(encoded_combined_key)
# Декодируем строку обратно в представлении ASCII, чтобы подготовить
# ее для передачи по Интернету
auth_ready_key = b64_encoded_combined_key.decode('ascii')
Эта библиотека используется для преобразования исходного ключа API и ключа
секрета API в необходимый формат для передачи в конечную точку авторизации
Twitter.
Закодировав должным образом наш ключ API и ключ секрета API, мы можем импортировать в сценарий для указания и извлечения данных только переменную
auth_ready_key. Теперь для извлечения сообщений из Twitter нам осталось только
отправить в конечную точку авторизации запрос на получение токена доступа.
Запрос токена доступа и данных из API-интерфейса Twitter
Подобно примеру, код которого приводится в листинге 5.3, мы создадим файл сценария (или блокнот кода) Python, в котором будут выполняться следующие два шага нашего процесса загрузки данных с платформы Twitter:
1. Запрос (и получение) токена доступа (который также называется токеном на
предъявителя — bearer token) у Twitter.
2. Добавление полученного токена доступа в запрос данных к Twitter и получение
результатов.
Запрос токена доступа, используя методы get и post
Для запроса токена доступа у Twitter нужно всего лишь отправить правильно
оформленный запрос к конечной точке авторизации https://api.twitter.com/oauth2/
token. Но вместо использования метода get (который мы использовали в листингах
5.1 и 5.3), когда ключ auth_ready_key добавляется в конец URL конечной точки, в
данном примере мы используем метод post.
В данном случае метод post важен тем, что он предоставляет дополнительную
безопасность по сравнению с методом get12. Но основная причина его использования состоит в том, что он является стандартным способом в тех случаях, когда от
API-интерфейса требуется больше, чем просто возвратить данные. Таким образом,
тогда как в листинге 5.3 мы получали данные, которые были уникальными для нашего запроса, API-интерфейс возвратил бы точно такие же данные для любого другого пользователя, отправившего такой же запрос. В противоположность, при ис12
Например, содержимое запросов post не сохраняется в истории браузера подобно тому, как это
делается для запросов get.
Доступ к интернет-данным
|
203
пользовании метода post для отправления ключа auth_ready_key API-интерфейс
платформы Twitter обработает наш уникальный ключ и возвратит уникальный токен доступа. Поэтому мы и используем метод post.
При построении нашего запроса post в Python нам нужно создать два объекта dict.
Один из них будет содержать заголовки запроса, включая наш ключ auth_ready_key
и некоторую другую информацию, а другой — данные запроса, которые в данном
случае будут указывать, что мы запрашиваем учетные данные. Затем мы просто
передадим эти два объекта в виде параметров методу post библиотеки requests вместо добавления их в конец строки URL-адреса. Соответствующий код приводится в
листинге 5.6.
Листинг 5.6. Сценарий Twitter_data_download.py
# Импортируем зашифрованный ключ из файла учетных данных
from Twitter_credentials import auth_ready_key
# Импортируем библиотеку request для получения данных из Интернета
import requests
# Указываем конечную точку Twitter для получения нашего маркера доступа
# (или токена носителя)
auth_url = 'https://api.twitter.com/oauth2/token'
# Добавляем наш ключ auth_ready_key в шаблонный объект dict, предоставляемый
# в документации по API-интерфейсу Twitter
auth_headers = {
'Authorization': 'Basic '+auth_ready_key,
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
}
# Еще один объект dict содержит описание запрашиваемых данных
auth_data = {
'grant_type': 'client_credentials'
}
# Посылаем наш запрос в конечную точку авторизации и сохраняем полученные результаты
# в переменной auth_resp
auth_resp = requests.post(auth_url, headers=auth_headers, data=auth_data)
# Извлекаем маркер доступа из данных в формате json, присланных нам конечной точкой
авторизации
access_token = auth_resp.json()['access_token']
Данный объект dict содержит информацию, которая требуется конечной точкой
авторизации, чтоб отправить нам маркер доступа. Эта информация включает наш
зашифрованный ключ и его формат данных.
204
|
Глава 5
Формат объектов auth_headers и auth_data был определен поставщиком APIинтерфейса.
Довольно простой процесс, не так ли? Если все пройдет хорошо (что мы скоро проверим), в этот сценарий нужно будет добавить лишь несколько строк кода, чтобы
использовать полученный токен доступа для запроса настоящих данных о происходящем в Twitter.
Но как насчет токена доступа
в инструментальной панели разработчика?
Возможно, что при копировании своего ключа API и ключа секрета API из инструментальной панели разработчика (см. рис. 5.13) вы обратили внимание на
наличие в ней токена на предъявителя (Bearer Token). Почему мы не использовали этот токен вместо того, чтобы создавать сценарий для получения еще одного?
Вкратце, потому что хотя API-интерфейс платформы Twitter предоставляет токены доступа в инструментальной панели, эта возможность отсутствует во
многих других подобных API-интерфейсах. Поскольку нашей целью является
проиллюстрировать более общий процесс взаимодействия с API-интерфейсами,
использующими OAuth, мы рассматриваем процесс прохождения цикла запроса токена доступа во всех подробностях, чтобы этот код можно было модифицировать и использовать с другими API-интерфейсами.
Получив токен доступа (токен на предъявителя), можно переходить к созданию запроса для извлечения сообщений из платформы Twitter. В целях демонстрации мы
используем простой запрос, в частности, базовый запрос поиска недавних сообщений Twitter, содержащих слово Python, с возвратом максимум четырех сообщений.
Этот запрос мы создадим на базе запроса из листинга 5.6, включив в его заголовок
полученный нами токен доступа. Полученные по этому запросу данные мы запишем в файл. Но поскольку кроме текста сообщений ответ содержит большой объем
других данных JSON, мы выведем на экран только текст каждого сообщения Twitter, просто чтобы быть уверенными в том, что мы получили правильные (если иногда и неожиданные) результаты.
Листинг 5.7. Сценарий Twitter_data_download.py (продолжение)
# Имея в своем распоряжении токен доступа (Bearer Token), мы готовы
# выполнять запросы данных
# Создаем новую структуру dict, содержащую наш маркер доступа
search_headers = {
'Authorization': 'Bearer ' + access_token
}
Доступ к интернет-данным
|
205
# Это конечная точка API-интерфейса для поиска в Twitter для версии 1.1 API-интерфейса
search_url = 'https://api.twitter.com/1.1/search/tweets.json'
# Создаем новый объект dict, содержащий наши параметры поискового запроса
search_params = {
'q': 'Python',
'result_type': 'recent',
'count': 4
}
# Отправляем наш поисковый запрос и сохраняем полученные им результаты в переменной
# search_resp
search_resp = requests.get(search_url, headers=search_headers, params=search_params)
# Преобразовываем полученные данные в объект JSON
Twitter_data = search_resp.json()
# Открываем выходной файл для сохранения результатов
Twitter_output_file = open("Twitter_search_results.json", "w")
# Записываем полученные с Twitter данные в наш выходной файл
Twitter_output_file.write(str(Twitter_data))
# Закрываем выходной файл
Twitter_output_file.close()
# Обрабатываем в цикле полученные результаты и отображаем на экране текст состояния
# Twitter
for a_Tweet in Twitter_data['statuses']:
print(a_Tweet['text'] + '\n')
В данном случае наш запрос (q) направлен на поиск не более четырех последних
сообщений Twitter о Python. Напомню, что ключи и значения, которые может содержать этот объект, определяются поставщиком данных.
Поскольку в ответ на запрос Twitter возвращает объект JSON, мы использовали
функцию Python str(), чтобы преобразовать его в строку, прежде чем записывать
его в файл.
Поскольку каждый результат содержит большой объем информации, чтобы получить представление о содержимом результата, мы будем отображать только текст
каждого сообщения Twitter. Переменная statuses содержит список сообщений Twitter в объекте JSON, и сам текст сообщений можно получить при помощи ключа
text.
В зависимости от уровня активности пользователей Twitter по созданию новых сообщений о Python, результаты могут быть разными при повторном исполнении
206
|
Глава 5
сценария даже в течение нескольких минут после предыдущего13. Конечно же, этот
поисковый запрос можно модифицировать для поиска любого требуемого вами
термина, просто изменив должным образом значение переменной search_params.
Просмотреть список всех возможных параметров и их допустимых значений можно
в документации по API-интерфейсу для конкретной конечной точки APIинтерфейса Twitter (https://developer.twitter.com/en/docs/twitter-api/v1/tweets/search/
api-reference/get-search-tweets).
Вот и все! Платформа Twitter предоставляет несколько разных API-интерфейсов
(один из которых, например, позволяет публиковать сообщения в своей, или даже в
чьей-то другой, ленте сообщений). Но для целей извлечения и выпаса данных рассмотренного здесь материала должно быть достаточно, чтобы позволить вам начать
работать с другими подобными API-интерфейсами.
Этические нормы при работе
с API-интерфейсами
Теперь, когда вы знаете, как делать запросы к API-интерфейсам таких служб, как
Twitter (и других, использующих процесс OAuth), вы хорошо представляете себе
многообразие возможностей, которые можно делать с полученными данными. Но
прежде чем начинать создавать десятки сценариев для отслеживания онлайновых
разговоров на ваши любимые темы, будет уместным рассмотреть некоторые практические и этические аспекты подобной деятельности.
Во-первых, имейте в виду, что почти все API-интерфейсы ограничивают количество запросов, которые можно выполнять в течение данного интервала времени. Например, для конкретной конечной точки API-интерфейса, которую мы использовали в листинге 5.7, можно делать максимум 450 запросов в течение 15 минут, при
этом каждый запрос может возвращать максимум 100 сообщений Twitter. В случае
превышения этого количества, дальнейшие запросы, скорее всего, не будут возвращать никаких данных, пока платформа Twitter не решит, что начался следующий 15-минутный интервал.
Во-вторых, даже если не читали во всех подробностях Соглашение с разработчиком
(не волнуйтесь, вы не один такой 14; при необходимости, его всегда можно
найти в Интернете по адресу https://developer.twitter.com/en/developer-terms/
13
Не забывайте, что при каждом повторном исполнении этого сценария старое содержимое выходного файла заменяется новым.
14
См. статью The Cost of Reading Privacy Policies в журнале I/S: A Journal of Law and Policy for the
Information Society 4, авторы Aleecia M. McDonald and Lorrie Faith Cranor (2008): 543, https://kb.osu.edu/
bitstream/handle/1811/72839/ISJLP_V4N3_543.pdf, и статью The Biggest Lie on the Internet: Ignoring the
Privacy Policies and Terms of Service Policies of Social Networking Services, авторы Jonathan A. Obar и Anne
Oeldorf-Hirsch, Information, Communication & Society 23 №. 1 (2020), стр. 128147, https://papers.ssrn.com/
sol3/papers.cfm?abstract_id=2757465.
Доступ к интернет-данным
|
207
agreement-and-policy), оно содержит положения, имеющие важные практические и
этические последствия. Например, Соглашение с разработчиком системы Twitter
прямо запрещает "сопоставление данных вне рамок платформы Twitter", т. е. объединение данных Twitter с другой информацией о пользователе, за исключением
случаев, когда этот пользователь предоставил вам эту информацию, дав на это свое
согласие. Оно также содержит правила о том, как можно хранить и отображать содержимое платформы Twitter, получаемое посредством его API-интерфейса, а также целый ряд других правил и ограничений.
Независимо от того, имеют ли эти условия предоставления услуг обязательную
юридическую силу15 или являются ли они действительно этичными сами по себе,
помните, что, в конечном итоге, ответственность за сбор, анализ, хранение и обмен
любыми данными, которые вы используете, с соблюдением этических норм лежит
на вас. Это означает, что необходимо учитывать конфиденциальность и безопасность людей, данные о которых вы можете использовать, а также думать о последствиях их сбора и обмена.
Конечно же, если бы этические проблемы было легко идентифицировать и согласовать, то мы бы жили в совершенно другом цифровом мире. Но поскольку это не
так, многие организации используют четко определенные процессы проверки и комитеты по надзору, предназначенные для облегчения изучения и (если возможно)
решения этических проблем еще до начала исследований и сбора данных, оказывающих влияние на людей. Лично я считаю, что хорошей начальной точкой при
рассмотрении этических вопросов является Этический кодекс Общества профессиональных журналистов (Society of Professional Journalists' Code of Ethics,
https://spj.org/ethicscode.asp). Хотя в этом документе подробно не рассматриваются все возможные ситуации, затрагивающие этические вопросы, в нем излагаются
основные принципы, которые, по моему мнению, было бы полезно учитывать всем
пользователям данных при сборе, анализе и обмене информацией.
Учитывая все это, на мой взгляд, все же самым важным является то, что вы должны
быть готовы поддержать любой сделанный вами выбор. Одной из замечательных
возможностей извлечения и выпаса данных является возможность открытия новой
информации и генерирования новых идей. Точно так же, как навыки для этого находятся полностью в вашем распоряжении, так и ответственность за то, как вы их
используете, полностью лежит на вас.
Извлечение веб-данных:
источник данных последней надежды
Компании и организации разрабатывают API-интерфейсы с конкретной целью предоставления доступа к наборам данным (часто обширным и разнообразным) через
15
См. статью Data Journalism and the Law, автор Victoria D. Baranetsky, опубликованную Tow Center for
Digital Journalism в 2018 г., https:// doi.org/10.7916/d8-15sw-fy51.
208
|
Глава 5
Интернет. Но Интернет содержит большой объем информации, существующей в
виде веб-страниц на основе форм или с высоким уровнем форматирования, а не
предоставляемой посредством API-интерфейсов или в форматах CSV или PDF. В
таких ситуациях единственное решение для получения этой информации состоит в
применении веб-извлечения данных. Это процесс получения содержимого вебстраниц программным способом с последующим извлечением из этого содержимого некоторого объема структурированных данных.
Я называю веб-извлечение данных источником данных последней надежды по той
причине, что это как технически, так и этически сложный процесс. Для создания
сценария Python для веб-извлечения данных почти всегда нужно вручную исследовать запутанный HTML-код, чтобы найти требуемые данные, после чего обычно
следует значительный объем обработки методом проб и ошибок, чтобы отделить
требуемую информацию от всего ненужного. Это трудоемкий, хлопотный и часто
разочаровывающий процесс. К тому же, если страница изменится даже самую малость, то вам, возможно, придется начать все сначала, чтобы ваш сценарий работал
с обновленной страницей.
Веб-извлечение данных также представляет сложность в этическом аспекте, поскольку многие владельцы веб-сайтов по разным причинам не хотят, чтобы их сайты подвергались извлечению данных. Программы извлечения с некачественным
кодом могут перегрузить веб-сайт, делая его недоступным для других пользователей. Выполнение сценарием запросов с высокой частотой также может повысить
расходы владельца веб-сайта, так как ему нужно платить своему поставщику услуг
за возврат всех этих данных в течение короткого периода времени. Поэтому многие
веб-сайты в условиях обслуживания прямо прописывают запрет на веб-извлечение
данных.
В то же время, если важная информация — особенно о влиятельных организациях
или правительственных учреждениях — доступна только посредством вебстраницы, тогда веб-извлечение может быть вашим единственным вариантом, хотя
это и противоречит условиям обслуживания. И хотя предоставление псевдоюридического совета по этому вопросу выходит далеко за рамки этой книги, имейте в виду, что даже если ваши сценарии соответствуют стандартам ответственности, и для
вашей деятельности по веб-извлечению данных существует веский общественный
интерес, владелец веб-сайта может наложить на вас санкции (например, отправить
вам письмо с требованием "прекратить и воздерживаться впредь") или подать на
вас в суд.
Поэтому я настоятельно рекомендую, прежде чем приступать к разработке любого
сценария для веб-извлечения данных, пройтись по замечательному дереву решений
(автора Софи Чоу — Sophie Chou), показанному на рис. 5.1616.
Определив, что веб-извлечение является вашим единственным или наилучшим вариантом, можно приступать к работе.
16
Это дерево решений сопровождается таким же превосходным постом блога: https://storybench.org/
to-scrape-or-not-to-scrape-the-technical- and-ethical-challenges-of-collecting-data-off-the-web.
Доступ к интернет-данным
Рис. 5.16. Дерево решений для веб-извлечения данных
(создано Софи Чоу для Storybench.org, 2016)
|
209
210
|
Глава 5
Осторожно извлекаем данные с веб-сайта УГПТ
Для данного примера мы используем подход извлечения веб-данных для загрузки и
извлечения данных из веб-страницы Управления городского пассажирского транспорта (УГПТ) города Нью-Йорк, которая предоставляет ссылки на все данные с
турникетов городских станций метро, начиная с 2010 г. Чтобы гарантировать наивысший уровень ответственности в процессе веб-извлечения данных, мы обеспечим, что каждый создаваемый нами сценарий Python:
1. Идентифицирует нас и предоставляет контактную информацию (в данном примере мы вставим адрес электронной почты).
2. Делает паузы между запросами к веб-странице, чтобы не допустить перегрузки
сервера.
Кроме этого, мы структурируем наш сценарий, разбив его на отдельные части, чтобы обеспечить загрузку только необходимого материала определенной вебстраницы. Для этого мы начнем с загрузки и сохранения копии веб-страницы, содержащей ссылки на файлы с данными для отдельных турникетов. Затем мы создадим отдельный сценарий, который обрабатывает сохраненную версию загруженной
страницы и извлекает требуемые нам данные. Таким образом, всем экспериментам
в процессе извлечения данных из веб-страницы будет подвергаться наша версия
этой страницы, что предотвратит излишнюю нагрузку на сервер. Наконец, мы создадим третий сценарий, который выполняет парсинг нашего файла с извлеченными
ссылками и загружает данные для турникетов за последние четыре недели.
Прежде чем приступать к написанию какого-либо кода, давайте посмотрим на
страницу, из которой мы планируем извлекать данные (http://web.mta.info/
developers/turnstile.html). Как видно, эта страница не более чем заголовок и длинный список ссылок, каждая из которых ведет на текстовый файл (с расширением
.txt) с данными в формате CSV. Чтобы загрузить файлы данных за последние несколько недель, первым шагом загрузим и сохраним копию этой страницы с указателями. Соответствующий код приводится в листинге 5.8.
Листинг 5.8. Сценарий MTA_turnstiles_index.py
# Импортируем библиотеку requests для получения данных из Интернета
import requests
# Указываем URL-адрес загружаемой страницы
# Данная страница содержит список ссылок на все файлы данных для турникетов УГПТ
города
# Нью-Йорк вплоть до 2010 г.
mta_turnstiles_index_url = "http://web.mta.info/developers/turnstile.html"
# Создаем заголовочную информацию для нашего запроса веб-страницы
headers = {
'User-Agent': 'Mozilla/5.0 (X11; CrOS x86_64 13597.66.0) ' + \
'AppleWebKit/537.36 (KHTML, like Gecko) ' + \
Доступ к интернет-данным
|
211
'Chrome/88.0.4324.109 Safari/537.36',
'From': 'YOUR NAME HERE - youremailaddress@emailprovider.som'
}
# Вместе с нашими информационными заголовками отправляем запрос get() для данного
# URL-адреса
mta_web_page = requests.get(mta_turnstiles_index_url, headers=headers)
# Открываем локальный файл в режиме записи для сохранения содержимого загруженной
# веб-страницы
mta_turnstiles_output_file = open("MTA_turnstiles_index.html","w")
# Записываем текст веб-страницы в наш выходной файл
mta_turnstiles_output_file.write(mta_web_page.text)
# Закрываем выходной файл
mta_turnstiles_output_file.close()
Поскольку в данном случае мы не используем API-интерфейс, нам нужно предоставить владельцу веб-страницы упреждающую информацию о том, кто мы такие и как связаться с нами. В данном случае мы описываем браузер, который он
должен считать источником нашего трафика, а также предоставляем наше имя и
контактную информацию. Эти данные владелец веб-сайта сможет увидеть в журналах своего сервера.
В результате исполнения этого сценария мы получим файл MTA_turnstiles_index.html в
той же самой папке, что и файл сценария. Чтобы просмотреть его содержимое, выполните двойной щелчок мышью по его значку, и он должен открыться в вашем
браузере, установленном по умолчанию. Поскольку мы загрузили только основной
код страницы без никаких дополнительных файлов, изображений и других материалов, к которым эта страница обычно имела бы доступ в Интернете, то страница
будет иметь несколько неаккуратный вид, примерно как показанно на рис. 5.17.
К счастью, такой внешний вид не имеет особой важности для наших целей, поскольку нам нужны только ссылки, которые содержатся в коде HTML-страницы.
Но прежде чем начинать размышлять о том, как извлечь эти ссылки программным
способом, нам для начала нужно найти их в коде HTML-страницы. Для этого нам
понадобятся средства инспектирования.
Использование средств инспектирования браузера
Откройте в браузере локальную копию страницы данных турникетов УГПТ и прокрутите ее вниз, пока в поле зрения не появится заголовок Data Files (см. рис. 5.17).
Чтобы наш сценарий Python мог более точно определить в коде этой страницы
только требуемую нам информацию, нам нужно определить какую-либо уникаль-
212
|
Глава 5
ную характеристику кода HTML, заключающего эту информацию. Это облегчит
для нас задачу создания сценария, который будет быстро находить требуемое нам
содержимое. Самый легкий способ для этого — исследовать код страницы бок о
бок с ее отображением в браузере.
Рис. 5.17. Просмотр в браузере локальной копии веб-страницы УГПТ
Для этого щелкните правой кнопкой мыши по тексту Data Files. В открывшемся
контекстном меню выберите опцию Inspect (рис. 5.18).
Точное размещение и вид окна средств инспектирования будет разным для разных
браузеров, но его содержимое должно выглядеть наподобие показанного на рис.
5.19 (окно инспектирования браузера Chrome).
Независимо от местонахождения этого окно в вашем браузере (иногда оно будет
закреплено на боковой или нижней стороне окна браузера, как показано на рис. 5.19,
Доступ к интернет-данным
|
213
и часто содержит несколько панелей с дополнительной информацией), нам нужно
найти в его содержимом слова Data Files. Если вы потеряли эти слова из виду (или
не видели их с самого начала) после открытия окна, просто снова щелкните правой
кнопкой мыши по этим словам в веб-странице, чтобы еще раз открыть окно инспектирования.
Рис. 5.18. Контекстное меню для элемента Data Files
локальной копии веб-страницы УГПТ
Рис. 5.19. Пример окна средств инспектирования
214
|
Глава 5
Использование средств инспектирования
наиболее эффективным способом
Средства инспектирования браузера могут быть полезными во многих случаях,
но они особенно важны при веб-извлечении данных, когда нам требуется найти
уникальный способ для идентификации определенных фрагментов информации
на веб-странице. Например, при наведении курсора мыши на фрагмент кода подсвечивается соответствующая часть веб-страницы. Перемещаясь таким образом
по коду в окне средств инспектирования, можно подсвечивать соответствующие
части визуального интерфейса веб-страницы, что способствует созданию сценария для более точного захвата нужных вам частей веб-страницы.
В данном случае наведем курсор мыши на следующий код в окне средств инспектирования:
<div class="span-84 last">
В результате в окне браузера должен подсветиться раздел Data Files веб-страницы.
На основании подсвеченной области кажется, что данный фрагмент кода содержит
весь список интересующих нас ссылок, что можно подтвердить, прокручивая содержимое окна средств инспектирования. Здесь мы увидим, что все требующиеся
нам ссылки на файлы данных (которые заканчиваются расширением .txt) действительно находятся внутри раздела div. (Обратите внимание на то, как они все расположены, — с отступом под этим тегом. Это еще один пример использования вложенности.) Теперь, если мы можем подтвердить, что класс span-84 last встречается
только в одном месте веб-страницы, мы получим хорошую отправную точку для
создания нашего сценария Python для извлечения списка ссылок.
Решение Python для извлечения данных из веб-страницы:
библиотека Beautiful Soup
Прежде чем приступать к созданию следующего сценария Python, давайте удостоверимся, действительно ли класс span-84 last является уникальным на загруженной
странице. Опишем самый простой способ, как сделать это. Откройте сохраненную
веб-страницу в редакторе кода Atom (щелкнув правой кнопкой по его значку и в
опции Открыть с помощью открывшегося контекстного меню выберите вариант
Atom). В результате в редакторе отобразится код нашей веб-страницы. Затем выполните поиск в документа по строке "span-84 last". Как выясняется, подстрока
"span-84" встречается в нашем файле только один раз, поэтому в нашем сценарии
Python мы можем ограничиться поиском ссылочной информации, вложенной в этот
HTML-тег.
Теперь мы готовы начать создание сценария Python для извлечения требуемых
ссылок из сохраненной веб-страницы. Но для этого нам нужно для начала установить специальную библиотеку Beautiful Soup, предназначенную для парсинга кода
языка разметки веб-страниц. Хотя некоторая функциональность этой библиотеки
Доступ к интернет-данным
|
215
повторяет функциональность библиотеки lxml, которую мы использовали в сценарии в листинге 4.16, она может выполнять парсинг даже неидеально структурированного кода HTML и XML, с которым нам постоянно приходится сталкиваться в
Интернете. Кроме этого, библиотека Beautiful Soup позволяет захватывать фрагменты кода разметки по практически любой его характеристике — названию класса, типу тега или даже значению атрибута. Поэтому это, по большому счету, идеальная библиотека для извлечения данных из "супа" кода разметки, который часто
встречается в Интернете. Информацию по установке этой библиотеки можно найти
в ее документации (https://crummy.com/software/BeautifulSoup/bs4/doc), но легче
всего это сделать посредством того же самого процесса, который мы использовали
для установки других библиотек, в частности, при помощи команды pip:
pip install beautifulsoup4
Теперь мы можем создать сценарий Python, чтобы открыть локальную копию вебстраницы и извлечь из нее все требуемые нам ссылки и сохранить их в простом
файле CSV. Соответствующий код приводится в листинге 5.9.
Листинг 5.9. Сценарий MTA_turnstiles_parsing.py
# Импортируем метод Beautiful Soup из библиотеки bs4
from bs4 import BeautifulSoup
# Открываем локальную копию веб-страницы с данными турникетов УГПТ.
# (Сама веб-страница находится здесь: http://web.mta.info/developers/turnstile.html)
mta_web_page = open("MTA_turnstiles_index.html", "r")
# Определяем базовый URL-адрес для файлов данных
base_url = "http://web.mta.info/developers/"
# Метод BeautifulSoup принимает в качестве параметров содержимое нашей веб-страницы
# и значение типа кода, с которым нужно работать.
# В данном случае это код HTML
soup = BeautifulSoup(mta_web_page, "html.parser")
# Передаем методу find в качестве параметров тип тега и имя класса, чтобы найти
# требуемое нам содержимое
data_files_section = soup.find("div", class_="span-84 last")
# В данном разделе div мы можем просто искать все теги якоря (a)
all_data_links = data_files_section.find_all("a")
# Открываем файл в режиме записи для хранения извлеченных ссылок
mta_data_list = open("MTA_data_index.csv","w")
# Методом find_all() возвращаем список всех совпадающих ссылок
for a_link in all_data_links:
216
|
Глава 5
# Объединяем базовый URL-адрес с содержимым каждого свойства href (ссылка)
# и сохраняем результат в переменной comlete_link
complete_link = base_url+a_link["href"]
# Записываем эту полную ссылку в выходной файл, добавляя в конце символ новой строки
# (\n), чтобы каждая ссылка была в отдельной строке
mta_data_list.write(complete_link+"\n")
# Записав все ссылки, закрываем выходной файл
mta_data_list.close()
Если щелкнуть по какой-либо ссылке в онлайновой версии веб-страницы, мы
увидим, что первая часть URL-адреса, где хранятся настоящие данные, имеет вид
http://web.mta.info/developers/, но каждая ссылка содержит только вторую половину этого URL-адреса (в формате data/nyct/turnstile/turnstile_ГГММДД.txt). Чтобы
обеспечить нашему сценарию загрузки рабочие ссылки, нам нужно указать "базовый" URL-адрес.
Благодаря нашей работе со средствами инспектирования мы можем сразу же перейти к разделу div с классом span-84, чтобы начать поиск требующихся нам ссылок. Обратите внимание, что поскольку слово class имеет специальное значение в
Python, при его использовании с Beautiful Soup к нему в конце добавляется символ
подчеркивания (т. е. class_).
Ну, хорошо. Теперь у нас есть новый файл, содержащий список всех ссылок на
требующиеся нам файлы данных. Далее нам нужно создать еще один сценарий для
чтения этих ссылок и загрузки соответствующих файлов данных. Но мы не хотим
перегружать веб-сайт слишком быстрой загрузкой всех этих файлов, поэтому мы
разнесем их загрузку на секунду-две друг от друга, используя для этого встроенную
библиотеку Python time. Кроме этого, мы загрузим только четыре файла, которые
нам действительно нужны, а не все без разбора подряд файлы. Соответствующий
исходный код сценария приводится в листинге 5.10.
Листинг 5.10. Сценарий TA_turnstiles_data_download.py
# Импортируем библиотеку requests для получения данных из Интернета
import requests
# Импортируем библиотеку Python os для создания новой папки для хранения загруженных
# файлов данных
import os
# Импортируем библиотеку time
import time
# Открываем файл, содержащий список ссылок на файлы данных
mta_data_links = open("MTA_data_index.csv","r")
Доступ к интернет-данным
|
217
# Создаем новую папку, чтобы упорядочить сохраняемые данные
folder_name = "turnstile_data"
# Добавляем информационные заголовки
headers = {
'User-Agent': 'Mozilla/5.0 (X11; CrOS x86_64 13597.66.0) ' + \
'AppleWebKit/537.36 (KHTML, like Gecko) ' + \
'Chrome/88.0.4324.109 Safari/537.36',
'From': 'YOUR NAME HERE - youremailaddress@emailprovider.som'
}
# Используем встроенную функцию readlines() для преобразования каждой строки нашего
# файла данных в элемент списка
mta_links_list = mta_data_links.readlines()
# Удостовериваемся в отсутствии существующей папки с выбранным нами именем
# для создаваемой папки
if os.path.isdir(folder_name) == False:
# Создаем новую папку с таким именем
target_folder = os.mkdir(folder_name)
# Загружаем только требуемое количество файлов
for i in range(0,4):
# Удаляем символ новой строки в конце каждой ссылки, используя для этого встроенную
# функцию strip()
data_url = (mta_links_list[i]).strip()
# Создаем уникальный выходной файл с названием на основе нашего URL-адреса
data_filename = data_url.split("/")[-1]
# Запрашиваем данные
turnstile_data_file = requests.get(data_url, headers=headers)
# В созданной целевой папке открываем новый файл в режиме записи, используя
# соответствующее имя файла
local_data_file = open(os.path.join(folder_name,data_filename), "w")
# Сохраняем содержимое загруженного файла в этом новом файле
local_data_file.write(turnstile_data_file.text)
# Закрываем локальный файл
local_data_file.close()
# Прежде чем обрабатывать следующий элемент в цикле, выдерживаем паузу в две секунды,
# используя функцию sleep()
time.sleep(2)
218
|
Глава 5
Эта библиотека позволяет сделать паузу между запросами данных в сценарии
загрузки, чтобы не перегружать сервер, отправляя ему слишком много запросов в
течение слишком короткого интервала времени (что может создать нам проблемы с
владельцем веб-сайта).
Здесь мы разбиваем ссылку по косым чертам, а затем из получившегося списка
берем
последний
элемент,
используя
отрицательное
индексирование
(https://w3schools.com/python/gloss_python_string_negative_indexing.asp). Это означает, что элемент в позиции –1 будет последним, которым в данном случае будет
имя файла .txt. Это имя файла, который мы будем использовать для хранения локальной копии загруженных данных.
При успешном исполнении сценария у нас должна создаться новая папка turnstile_data,
содержащая четыре самых последних файла с данными турникетов. Довольно ловко, не так ли?
Заключение
Изучив многие способы получения требуемых нам данных и преобразования их в
практичные форматы, возникает вопрос: а что полезного можно сделать со всем
этим? Поскольку цель выпаса данных заключается в том, чтобы иметь возможность
отвечать на интересующие нас вопросы и генерировать определенные знания об
окружающем нас мире, теперь нам нужно переходить от процесса получения данные к процессу их оценки, улучшения и анализа. С этой целью в следующей главе
мы выполним оценку качества данных общедоступного набора данных с прицелом
на понимание как его возможностей, так и ограничений, а также, как выпас данных
может помочь нам использовать его наиболее эффективным образом.
ГЛАВА 6
Оценка качества данных
В последних двух главах мы концентрировали наши усилия на выявлении данных в
разных форматах из различных источников (от электронных таблиц до веб-сайтов)
и получения доступа к этим данным. Но получение доступа к (потенциально) интересным данным — это в действительности только первый шаг. Следующий шаг —
выполнить тщательную оценку качества этих данных, чтобы понять, представляют
ли они собой для нас что-либо ценное и полезное или же являются простым мусором.
Как вы уже убедились из материала главы 3, создание качественных данных представляет собой сложную и трудоемкую задачу. Этот процесс состоит из трех основных частей — исследования, экспериментирования и целеустремленной настойчивости. Приверженность цели получения качественных данных означает, что
вы должны быть готовы вложить в этот процесс значительные усилия и объем времени и при этом быть готовыми выбросить полученные результаты и начать заново. Потому как, несмотря на все вложенные усилия, некоторые данные бывает просто невозможно привести к требуемому уровню качества.
По сути, последний критерий, наверное, и является причиной, делающей действительно высококачественную, содержательную работу с данными по-настоящему
трудной. Овладение техническими навыками, как, я надеюсь, вы уже начинаете
осознавать, требует некоторого усилия, но при достаточной практике вполне достижимо. Исследовательские навыки немного сложнее задокументировать и передать, но проработка примеров в этой книге поможет вам выработать те навыки, которые связаны с обнаружением и упорядочением информации, требуемой для
оценки и улучшения полученных данных.
Что касается необходимости смириться с отказом от набора данных, имеющих существенные недостатки, на получение которого было потрачено десятки часов работы, то я могу посоветовать одно — попытайтесь вспомнить, что изучение значащих аспектов об окружающем нас мире является "игрой вдолгую". Вот почему я
всегда советую, что если вы заинтересованы научиться выполнять предварительную обработку и анализ данных, вам следует начинать с определения вопроса об
окружающем мире, представляющего для вас настоящий интерес и/или важность.
Для качественного выполнения этой работы вы должны позаботиться о том, чтобы
сделать ее правильно, а не просто так, по принципу: лишь бы она была сделана. Будет также полезным ценить знания, полученные в процессе выпаса данных, потому
что вы не только овладеете новой стратегией выпаса данных или программирова-
220
|
Глава 6
ния на языке Python, но и познакомитесь с экспертами или откроете новый информационный ресурс. В действительности, если вам по-настоящему небезразлична
исследуемая тема, усилия, потраченные на качественную работу с данными, никогда не будут потраченными напрасно. Просто в результате это может увести вас в
другое направление, нежели вы ожидали.
Часто случается так, что определенный набор данных хоть и не может предоставить ответ на исходный вопрос, но все равно может пролить свет на какой-либо из
его ключевых аспектов. А в других случаях данные по исследуемому предмету могут отсутствовать, и этот факт, возможно, может быть полезным — процесс обнаружения, какие именно данные отсутствуют, может побудить вас (и других) полностью изменить свою работу. Поскольку "идеальный" набор данных получить невозможно, в определенных случаях можно, с некоторой долей осторожности,
использовать данные с недостатками, так как даже частичные знания, предоставляемые такими данными, могут иметь общественную важность. В любом случае
самым важным остается ваша готовность нести личную ответственность за свои
решения — поскольку независимо от качества и происхождения исходных данных,
очищенный, дополненный, преобразованный и/или проанализированный набор
данных принадлежит уже вам.
И это не случайно, что все кажется достаточно трудным. Мы живем в такое время,
когда очень легко использовать мощные цифровые инструменты, не учитывая при
этом должным образом последствия этого использования на окружающий нас мир.
Надо всегда помнить, что разработчики "продвинутых" технологий создают алгоритмические правила, которые, по большому счету, основаны на их собственных
предпочтениях, даже если они не всегда осознают это. В конце концов, данные
могут быть механизмом как для информирования, так и для манипулирования,
как для просвещения, так и для эксплуатации. В конечном итоге, обеспечение направленности вашей работы на конкретную цель является уже вашей зоной ответственности.
Но что в действительности означает достижение качества данных на практике?
Чтобы получить представление об этом, мы посвятим последующий материал этой
главы оценке реальных данных с точки зрения аспектов целостности данных и соответствия данных, которые были представлены в главе 3. Для этого мы будем использовать набор данных в виде одного экземпляра данных по займам программы
PPP (Paycheck Protection Program — программа защиты зарплаты или бизнескредитование) правительства США. Этот набор данных содержит информацию о
миллионах кредитов, выданных малому бизнесу во время пандемии COVID-19.
Как мы сами увидим в последующем материале этой главы, данные программы PPP
иллюстрируют многие проблемы, присущие "найденным" данным, собранным как
государственными учреждениями, так и частными компаниями. В частности, неясные термины и необъяснимые переходы с одной версии данных на другую приводят к проблемам соответствия данных, для решения которых потребуются дополнительные исследования. Более простыми (хотя не обязательно поддающимися более быстрому решению) являются вопросы целостности данных, например,
подтверждение, что название данного банка пишется одинаково во всем наборе
Оценка качества данных
|
221
данных или что файлы данных содержат надлежащие значения и диапазоны данных. После решения большинства из этих проблем нашим достижением в итоге
будет высокая уверенность в наших данных, но, безусловно, она не станет несомненной. Как и вся работа с данными, полученные знания будут результатом принятия обоснованных решений, логического мышления и большого объема выпаса
данных. Для реализации первичной обработки мы будем полагаться на библиотеку
Python pandas, которая предоставляет популярный и мощный набор инструментов
для работы с табличными данными. Итак, приступим к работе.
Пандемия и программа PPP
Весной 2020 г. правительство США объявило о кредитной программе, предназначенной для стабилизации американской экономики, претерпевающей последствия
потери рабочих мест в связи с пандемией COVID-19. Объем финансирования программы составлял почти 1 триллион долларов1, а целью программы была заявлена
помощь малому бизнесу с уплатой арендной платы и выплатой зарплат, включая
принудительные закрытия и другие ограничения. И хотя фиксировалось, что после
выплаты первого транша произошло падение уровня безработицы, некоторые органы федерального правительства сопротивлялись предоставлению прозрачных данных по потраченным средствам2.
Так помогли ли кредиты по программе PPP спасти малый американский бизнес?
Теперь, по прошествии определенного времени, наверное, ответ на этот вопрос достаточно легко найти, но мы намерены выяснить это самостоятельно. Для этого мы
начнем с систематической оценки общего качества данных о кредитах PPP, в ходе
которой поочередно проверим все характеристики целостности данных и соответствия данных. Не менее важно и то, что мы тщательно задокументируем каждый
этап этого процесса, чтобы зафиксировать выполненные действия и логику принятых решений. Этот дневник данных будет важным для нас ресурсом в будущем,
особенно, когда нам потребуется объяснить или воспроизвести какую-либо часть
нашей работы. Подобный дневник может быть оформлен в любых предпочитаемых
вами форме и формате, но лично мне нравится простое форматирование и чтобы
дневник находился вблизи кода. Поэтому я буду документировать свою работу на
языке Markdown в файле, который можно легко сохранить и считать с GitHub3. Поскольку документирование, по сути, состоит в простой последовательной записи
всех выполняемых мною действий, я назову этот файл ppp_process_log.md.
1
Подробности о программе PPP см. здесь: https://en.wikipedia.org/wiki/Paycheck_Protection_Program.
См. статью Who Got Half a Trillion in COVID Loans? The Trump Administration Won't Say по адресу
https://marketplace.org/shows/make-me-smart-with-kai-and-molly/who-got-half-a-billion-in-covid-loansthe-trump-administration-wont-say.
3
Краткую сводку команд языка Markdown можно найти по адресу https://github.com/adam-p/
markdown-here/wiki/Markdown-Here-Cheatsheet.
2
222
|
Глава 6
Оценка целостности данных
С чего следует начинать процесс выпаса данных — с оценки целостности данных
или их соответствия? Ответ прост: понемногу с каждого. Как обсуждается в разд.
"Что такое выпас данных?" главы 1, процесс выпаса данных никогда не следует
начинать, если у вас нет какого-либо вопроса, на который вы ищете ответ, и некоторого представления о том, как определенный набор данных может помочь вам
получить этот ответ. Иными словами, у вас должно четко сформироваться представление о направлении вашего процесса выпаса данных и вы должны быть уверены, что ваши данные "подходят" для него. В то же самое время полностью оценить соответствие набора данных, пока не была исследована его целостность, часто
представляет сложную задачу. Например, если данные содержат разрывы, можно
ли их каким-либо образом заполнить? Или возможно ли найти отсутствующие метаданные? Если да, то тогда, возможно, мы сможем решить эти вопросы целостности и возвратиться к оценке соответствия данных, обладая более полной информацией. Если затем мы обнаружим, что соответствие данных можно улучшить, дополнив их (мы подробно рассмотрим вопрос дополнения данных в главе 7), то это,
конечно же, запустит другой раунд оценки целостности данных. А затем этот цикл
придется начать снова.
Однако это может привести к бесконечному циклу выпаса данных, поскольку зачастую одни вопросы порождают другие вопросы, и в результате мы узнаем что-то новое. Вместе с тем мы не можем посвятить задаче выпаса данных безграничное время
и тратить бесконечные усилия, а также прочие ресурсы, поэтому нам нужно принимать обоснованные решения и документировать их. Тщательная оценка качества наших данных поможет нам правильно принимать эти решения. Систематичная проверка данных на целостность и соответствие не только обеспечивает получение высококачественных данных, но также вынуждает нас принимать и документировать
решения, которые можно использовать для описания (а при необходимости и для защиты) любой создаваемой информации. Это не означает, что все согласятся с вашими заключениями. Но это помогает обеспечить возможность осмысленного их обсуждения, что действительно продвигает знания по данному вопросу.
Поэтому пора завершить разговоры и приступить к оценке целостности данных.
Бóльшая часть используемых в этой главе данных уже удалена из Интернета и/или заменена другими файлами; но в этом нет ничего необычного. Несмотря на это, я преднамеренно оставила материал этой главы в
его исходном виде, отражая, таким образом, реальные и типичные проблемы, возникающие при попытке осуществлять выпас данных в режиме, сколь-либо приближенном к режиму реального времени.
Это иллюстрирует, насколько быстро, и почти бесследно, в цифровом
мире могут эволюционировать и изменяться данные. И это обстоятельство следует помнить всегда. Все наборы данных, используемые в этой
главе, доступны на Google Drive по адресу https://drive.google.com/file/
d/1EtUB0nK9aQeWWWGUOiayO9Oe-avsKvXH/view?usp=sharing.
Оценка качества данных
|
223
Имеют ли данные известное происхождение?
На момент работы над материалом этой книги первым результатом поиска по фразе
"most recent PPP load data" (самые последние данные по кредитам программы PPP)
была страница на веб-сайте министерства финансов США, содержащая ссылку на
данные за август 2020 г4. Второй результат посика содержит ссылку на более свежие данные, предоставляемые Управлением по делам малого бизнеса — государственным учреждением, отвечающим за фактическое управление этими средствами5.
Хотя оба эти веб-сайта принадлежат государственным учреждениям, чья деятельность имеет отношение к программе PPP, для нас будет более целесообразным работать с данными, предоставляемыми Управлением по делам малого бизнеса. Но
отметим, что этот веб-сайт не был первым результатом моих поисков по данному
запросу.
Я хочу обратить ваше внимание на то обстоятельство, что Министерство финансов,
хоть и является надежным источником требуемых нам данных, однако оно не является наилучшим доступным источником этих данных. Вот поэтому важно учитывать не только источник данных, но и обстоятельства, при которых вы их нашли.
Актуальны ли данные?
Если мы хотим использовать данные для того, чтобы узнать что-либо о текущем
состоянии окружающего нас мира, первым делом необходимо установить возраст
наших данных, а также удостовериться в том, что это наиболее свежие из доступных данных.
Хоть это требование и выглядит легковыполнимым, однако напомню, что многие
веб-сайты не датируют автоматически каждое сообщение, поэтому зачастую становится весьма проблематично определить, когда что-то появилось в сети. При этом
веб-сайты могут менять дату датируемого содержимого при любом, даже незначительном, его обновлении. А некоторые веб-сайты вообще регулярно обновляют дату публикации, пытаясь таким образом перехитрить алгоритмы поисковых систем,
которые отдают предпочтение более свежему содержимому.
Иными словами, чтобы найти действительно самые свежие, релевантные данные
для конкретной задачи выпаса данных, придется немного потрудиться. Единственный способ — использовать несколько разных условий поиска, просмотреть несколько наборов результатов и, возможно, обратиться за советом к эксперту.
4
Первоначальная ссылка на содержимое — https://home.treasury.gov/policy-issues/cares-act/
assistance-for-small-businesses/sba-paycheck-protection-program-loan-level-data, но теперь оно находится по адресу https://home.treasury.gov/policy-issues/coronavirus/assistance-for-small-businesses/
paycheck-protection-program.
5
Следующая ссылка: https://sba.gov/funding-programs/loans/coronavirus-relief-options/paycheckprotection-program/ppp-data.
224
|
Глава 6
В процессе этой работы вы, скорее всего, найдете достаточное количество ссылок,
чтобы решить, какие доступные данные являются самыми свежими.
Полные ли данные?
На данном этапе мы знаем, что существует несколько публикаций данных по программе PPP. И даже если мы будем уверены в том, что успешно нашли самые свежие данные, возникает следующий вопрос: все ли это имеющиеся данные?
Поскольку нас в основном интересуют бизнесы, которые получили наиболее крупные кредиты, нам потребуется исследовать только один файл: public_150k_plus.csv6.
Но как мы можем знать, содержит ли этот файл данные для всех этапов программы
по текущую дату или же только данные по кредитам, выданным после первой публикации данных в августе 2020 г.? Поскольку у нас есть доступ к обоим наборам
данных7, мы можем использовать несколько разных стратегий:
1. Найти наиболее раннюю дату (или даты) в нашем "свежем" файле данных и удостовериться в том, что они предшествуют дате 8 августа 2020 г.
2. Сравнить размеры файлов и/или количество строк в двух наборах данных, чтобы
убедиться в том, что размер более позднего файла больше размера более раннего.
3. Сравнить содержащиеся в файлах данные, чтобы убедиться в том, что все записи в более старом файле содержатся в более новом файле.
Здесь вы можете начать задаваться вопросами: "Разве первой проверки, на подтверждение наиболее старой даты, не было бы достаточно? Зачем выполнять две
другие проверки, которые выглядят намного более трудными?" Ну, конечно, теоретически, подтверждение наличия данных из более ранних этапов программы
должно быть достаточным. Но в действительности это довольно поверхностная
проверка. Очевидно, что мы не можем убедиться в полноте опубликованных федеральным правительством данных, просто пытаясь собрать более охватывающие
данные самостоятельно. С другой стороны (как мы вскоре это увидим), процессы
сбора данных государственными учреждениями и крупными организациями (особенно банками) далеко не идеальны8. Поскольку, используя эти данные для того,
чтобы делать некие заключения, мы станем их владельцем, я полагаю, будет полезным выполнить тщательную проверку.
6
Этот файл можно загрузить по адресу
https://drive.google.eom/file/d/1EtUB0nK9aQeWWWGUOiayO9Oe-avsKyXH/ view?usp=sharing.
7
Данные за август 2020 г. можно загрузить по адресу https://drive.google.eom/file/d/
11wTOapbAzefeCQVVB-YJFIpsQVaZxJAm/view?usp=sharing; а данные за февраль 2021 г. — по
адресу https://drive.google.eom/file/d/1EtUB0nK9aQeWWWGUOiayO9Oe-avsKvXH/view?usp=sharing.
8
Например, после финансового кризиса 2008 г. выяснилось, что многие банки не вели должную
документацию при переоформлении и продаже (или секьюритизации) ипотек, в результате чего
некоторые суды отказали им в попытках лишить домовладельцев права выкупа собственности по
причине просрочки в платежах (https://nytimes.com/2009/10/25/business/economy/25gret.html).
Оценка качества данных
|
225
К счастью, выполнить первую проверку на полноту данных можно довольно легко:
просто открыв файл данных в каком-либо текстовом редакторе и увидев, что значение элемента DateApproved первой записи равно 05/01/2020, что подразумевает наличие в наиболее новом наборе данных данных из первого этапа выдачи кредитов
по программе PPP весной 2020 г. Этот факт иллюстрируется на рис. 6.1.
Рис. 6.1. Просмотр в текстовом редакторе файла данных
о новых кредитах по программе PPP
Как видим, ничего сложного. Если пойти немного дальше, то можно написать сценарий для поиска в файле свежих данных наиболее старых и наиболее новых значений (т. е., дат) элемента LoanStatus. Во избежание неразберихи я переименовала
более новый файл на public_150k_plus_recent.csv. В настоящее время9 при попытке
перейти по ссылке на данные по кредитам по программе PPP на странице
https://home.treasury.gov/policy-issues/cares-act/assistance-for-smallbusinesses/sba-paycheck-protection-program-loan-level-data открывается другая
страница, содержащая ссылку на папку с данными 150k plus 0808.zip, для которых указана дата загрузки 14 августа 2020 г. (https://sba.app.box.com/s/
ox4mwmvli4ndbp14401xr411m8sefx3i). Нужно загрузить всю эту папку, но мы можем извлечь данные в формате CSV и переименовать файл в
public_150k_plus_080820.csv.
Чтобы облегчить этот процесс, мы, как обычно, используем pandas — хорошо известную библиотеку Python для манипулирования табличными данными. Здесь мы
не будем подробно рассматривать эту библиотеку (желающие узнать о ней больше
могут воспользоваться другими ресурсами, например замечательной книгой Python
for Data Analysis (Анализ данных, используя язык Python), автор Уэс Маккинни
(Wes McKinney), издательство O'Reilly). Но мы воспользуемся ее полезными возможностями для проверки качества наших данных.
9
Следующие ссылки указываются только в архивных целях, так как они указывают первоначальное
местонахождение наборов данных, использованных в этой главе.
226
|
Глава 6
Первым делом установим эту библиотеку, выполнив следующую команду:
pip install pandas
Далее используем ее, чтобы извлечь из файла свежие данные по кредитам по программе PPP, и найдем самые старые и самые новые даты, которые содержит этот
файл. Соответствующий сценарий приводится в листинге 6.1.
Листинг 6.1. Сценарий ppp_date_range.py
# Простой сценарий для нахождения самой ранней и самой поздней дат в данных
# по кредитам по программе PPP
# Импортируем библиотеку pandas
import pandas as pd
# Считываем свежие данные в структуру DataFrame библиотеки pandas, используя ее
# метод read_csv()
ppp_data = pd.read_csv('public_150k_plus_recent.csv')
# Преобразовываем значения в столбце DateApproved в "настоящие" даты
ppp_data['DateApproved'] = pd.to_datetime(ppp_data['DateApproved'],
format='%m/%d/%Y')
# Отображаем минимальное и максимальное значения столбца DateApproved
print(ppp_data['DateApproved'].min())
print(ppp_data['DateApproved'].max())
Использование ключевого слова as позволяет нам создать короткий псевдоним
для библиотеки, чтобы к ней можно было более легко обращаться далее в нашем
коде.
Чтобы найти самую старую и самую новую даты, нам нужно преобразовать
строчные значения в нашем наборе данных в настоящий тип данных Date. В данном случае мы используем для этого функцию библиотеки pandas to_datetime(), передавая ей два параметра:
название столбца, чьи значения нужно преобразовать
(см. https://pandas.pydata.org/pandas-docs/stable/reference/api/
pandas.to_datetime.html);
формат дат (см. https://docs.python.org/3/library/datetime.html#strftime-and-
strptime-behavior), как они в настоящее время отображаются в нашем наборе
данных.
Как видно в следующем результате исполнения этого сценария, самый первый кредит в этом наборе данных был выдан 3 апреля 2020 г., а самый последний — 31 января 2021 г.:
2020-04-03 00:00:00
2021-01-31 00:00:00
Оценка качества данных
|
227
Обратите внимание на то, что хотя передача параметра формата функции
to_datetime() библиотеки pandas не является обязательной, это стоит сделать, поскольку без этой информации библиотека будет пытаться угадать, с каким форматом даты она работает, что может значительно увеличить время исполнения сценария. Учтите, что в данном случае задержка составит около секунды, но с набором
данных большого размера (или при обработке нескольких наборов данных) задержка может быть довольно значительной.
Теперь давайте сравним размер файла более новых данных с файлом данных за август 2020 г. Посмотрев на файлы public_150k_plus_recent.csv и public_150k_plus_
080820.csv в окне браузера, можно увидеть, что файл более свежих данных имеет
намного больший размер, чем файл более старых данных: несколько сотен мегабайт
по сравнению с около 124 МБ, соответственно. Пока что все хорошо.
Основываясь на методах, которые мы использовали в сценариях в листингах 4.9 и
5.10, создадим простой сценарий для определения количества строк в каждом из
этих файлов. Соответствующий код приводится в листинге 6.2.
Листинг 6.2. Сценарий ppp_numrows.py
# Простой сценарий для подсчета количества строк в каждом из файлов данных
# по кредитам по программе PPP.
# Это довольно простая задача, поэтому нам не нужно импортировать никаких
# дополнительных библиотек.
# Открываем файл данных по кредитам за август в режиме чтения
august_data = open("public_150k_plus_080820.csv","r")
# Используя метод readlines(), преобразовываем строки файла в список
print("August file has "+str(len(august_data.readlines()))+" rows.")
# Делаем то же самое и для файла со свежими данными по кредитам
recent_data = open("public_150k_plus_recent.csv","r")
# Опять отображаем количество строк в файле
print("Recent file has "+str(len(recent_data.readlines()))+" rows.")
После преобразования строк файла в список количество строк можно определить
при помощи встроенного метода Python len(). Для вывода этого результата на экран полученное число преобразовывается в строку, используя для этого встроенный метод str(). Если этого не сделать, то интерпретатор Python выдаст сообщение
об ошибке.
Исполнив этот сценарий, убеждаемся, что более старый файл данных (за август
2020 г.) содержит 662 516 строк, а более новый (датированный 1 февраля 2021 г.) —
766 500 строк.
228
|
Глава 6
Рассмотрим процесс сравнения содержимого этих двух файлов, чтобы убедиться в
том, что более новый файл содержит все содержимое более старого файла. Это будет многошаговый процесс, первым шагом которого будет просмотр более старого
файла данных (за август 2020 г.) в текстовом редакторе (рис. 6.2).
Рис. 6.2. Просмотр файла данных по кредитам по программе PPP за август 2020 г.
Сразу же можно увидеть некоторые отличия, это говорит нам о том, что данная
проверка будет еще более сложной, чем мы могли предполагать. Прежде всего мы
видим, что данные в более новом файле организованы в намного большее количество столбцов. Это означает, что нам нужно разработать стратегию для сопоставления записей более старого набора данных с записями более нового.
Сначала нужно разобраться, какие столбцы данных общие в обоих файлах. Для
этого мы создадим и сравним два небольших тестовых файла CSV, каждый из которых будет содержать несколько первых строк данных из каждого набора данных.
Затем создадим небольшой сценарий, который преобразовывает каждый из наших
исходных файлов в структуру DataFrame, а затем записывает первые несколько строк
в отдельный файл CSV. (Структура DataFrame — это специальный тип данных
библиотеки pandas для табличных данных.) Соответствующий код приводится в
листинге 6.3.
Листинг 6.3. Сценарий ppp_data_samples.py
# Небольшой сценарий для создания новых файлов CSV, содержащих первые несколько строк
# данных исходных файлов данных
# Импортируем библиотеку pandas
import pandas as pd
# Считываем данные из более старого файла (за август 2020 г.) в структуру DataFrame
# библиотеки pandas, используя ее метод read_csv()
august_ppp_data = pd.read_csv('public_150k_plus_080820.csv')
Оценка качества данных
|
229
# Метод head() возвращает заголовки столбцов структуры DataFrame вместе с первыми
# 5 строками данных
august_sample = august_ppp_data.head()
# Записываем эти первые строки данных в файл CSV august_sample.csv, используя метод
# библиотеки pandas to_cvs()
august_sample.to_csv('august_sample.csv', index=False)
# Считываем данные из более нового файла в структуру DataFrame библиотеки pandas,
# используя ее метод read_csv()
recent_ppp_data = pd.read_csv('public_150k_plus_recent.csv')
# Метод head() возвращает заголовки столбцов структуры DataFrame вместе с первыми
# 5 строками данных
recent_sample = recent_ppp_data.head()
# Записываем эти первые строки данных в файл CSV recent_sample.csv, используя метод
# библиотеки pandas to_cvs()
recent_sample.to_csv('recent_sample.csv', index=False)
Метод библиотеки pandas to_csv() инкапсулирует несколько шагов, которые мы
ранее выполняли посредством обычных операторов Python. В частности, он открывает файл в режиме записи и записывает в него данные. Поскольку библиотека
pandas по умолчанию добавляет столбец индексов (который, по сути, содержит номера строк) к каждой создаваемой ею структуре DataFrame, нам нужно передать методу to_csv() параметр index=False, чтобы эти номера строк не записывались в наш
выходной файл CSV.
Получив два тестовых файла небольшого размера, просмотрим их в редакторе
электронных таблиц, чтобы сравнить заголовки столбцов и их содержимое в обоих
файлах. Содержимое более старого файла показано на рис. 6.3, а более нового — на
рис. 6.4.
Рис. 6.3. Первые пять строк данных более старого файла (за август 2020 г.)
по кредитам по программе PPP
Похоже, что первые несколько строк обоих файлов содержат некоторые одинаковые столбцы данных. Это обстоятельство облегчит нашу задачу по сравниванию их
содержимого и (будем надеяться) определит, как столбцы можно использовать для
сопоставления строк.
230
|
Глава 6
Рис. 6.4. Первые пять строк данных более нового файла по кредитам по программе PPP
Начнем с выбора одной записи (строки), в идеале такой, которая содержит как
можно больше заполненных полей. Например, строка 6 более старого файла данных (рис. 6.3) содержит в столбце BusinessName значение SUMTER COATINGS, INC. А более новый файл (рис. 6.4) содержит это же значение в строке 2 в столбце
BorrowerName. Более старый файл содержит значение Synovus Bank в столбце Lender, и
это же значение находится в столбце ServicingLenderName более нового файла. Пока
что все выглядит хорошо. Или же нет?
Хотя многие данные в этих строках совпадают в обоих файлах данных, некоторые
данные, которые могут быть важными, отличаются. Например, в образце данных
более старого файла (за август 2020 г.) значение столбца DateApproved равно
05/03/2020, а в образце более нового файла оно равно 05/01/2020. Если посмотреть на
другую, казалось бы, общую запись, мы увидим, что для бизнеса/заемщика PLEASANT
PLACES, INC. (строка 5 в более старых данных и строка 3 в более новых) столбец CD в
обоих файлах (столбец AF электронной таблицы в более новом файле и столбец P в
более старом) содержит разные значения, а именно, SC-01 в старых данных и SC-06 в
новых. Что же здесь происходит?
На данном этапе нам нужно принять некоторые субъективные решения касательно
того, значения каких столбцов должны совпадать, чтобы определенную строку
можно было считать одинаковой для обоих файлов. Хорошей отправной точкой
может стать требование, чтобы совпадали название бизнеса и имя кредитора. Поскольку на данном этапе мы знаем, что было выдано несколько траншей кредитов,
будет, наверное, удобным, чтобы значения в столбце DateApproved обоих файлов
также совпадали (хотя выглядит маловероятным, что банк Synovus Bank выдал два
кредита заемщику Sumter Coatings, Inc. с интервалом друг от друга всего лишь в
два дня). Но как насчет несовпадающих значений столбца CD (congressional district —
избирательный округ для выборов в конгресс)? Посмотрев на карту этих избирательных округов для Южной Каролины10, можно увидеть, что их границы не меня10
Карты избирательных округов для выборов в Конгресс США можно посмотреть в Википедии по
адресу https://en.wikipedia.org/wiki/South_Carolina%27s_congressional_districts#Historical_and_
present_district_boundaries.
Оценка качества данных
|
231
лись с 2013 г., хотя действительно выглядит, что первый и шестой округа вроде бы
имеют общую границу. Получаем простой вывод: данное расхождение — это просто ошибка.
Как видно, мы уже нашли значительные проблемы с качеством данных, просмотрев
всего лишь пять строк данных! Очевидно, что мы не можем решить все эти расхождения одновременно, поэтому начнем с объединения строк, которые (мы надеемся)
действительно совпадают, одновременно отслеживая те, которые не совпадают.
Для этого нам нужно объединить (join), или слить (merge) эти два набора данных.
Но, поскольку мы уже знаем о расхождениях в этих наборах, то нам требуется найти
какой-то способ для отслеживания этих расхождений. Иными словами, файл, созданный объединением наших двух исходных файлов, должен содержать все строки
из обоих наборов данных, независимо от того, совпадают они или нет. Для этого
нам нужно выполнить с этим наборами данных операцию, называющуюся внешнее
соединение (outer join, https://pandas.pydata.org/pandas-docs/stable/user_guide/
merging.html#brief-primer-on-merge-methods-relational-algebra). Соответствующий
код приводится в листинге 6.4.
Обратите внимание на то обстоятельство, что поскольку результат операции внешнего соединения содержит все строки обоих исходных наборов данных, количество
строк в нем равно общему количеству строк в обоих наборах, что в данном случае
составит около 1,4 миллиона строк. Но не стоит волноваться из-за такого объема,
поскольку язык Python легко справится с этим. Вопрос только в том, справится ли
ваше устройство.
Для Macintosh или Windows работа с данными объемом около 500 МБ, что требуется для этих примеров, скорее всего, не представит никаких проблем. Но если вы,
подобно мне, используете устройство Chromebook или другое подобное устройство, то сейчас самое время перейти на облачную платформу.
Листинг 6.4. Сценарий ppp_data_join.py
# Простой сценарий для создания новых файлов CSV, содержащих первые несколько строк
# данных исходных файлов данных
# Импортируем библиотеку pandas
import pandas as pd
# Считываем данные из более старого файла (за август 2020 г.) в структуру DataFrame
# библиотеки pandas, используя ее метод read_csv()
august_ppp_data = pd.read_csv('public_150k_plus_080820.csv')
# Считываем данные из более нового файла в структуру DataFrame библиотеки pandas,
# используя ее метод read_csv()
recent_ppp_data = pd.read_csv('public_150k_plus_recent.csv')
# Загрузив оба файла в память, объединяем их
merged_data = pd.merge(august_ppp_data,recent_ppp_data,how='outer',
left_on=['BusinessName','Lender','DateApproved'],right_on=['BorrowerName',
'ServicingLenderName','DateApproved'],indicator=True)
232
|
Глава 6
# Отображаем значения в индикаторном столбце, по умолчанию обозначенном меткой
# по _merge
print(merged_data.value_counts('_merge'))
В данном случае передается параметр indicator=True, поскольку таким образом
будет создан новый столбец, содержащий информацию о том, какие строки содержатся в одном или обоих файлах.
Использование значения параметра indicator=True создает столбец merge, чье
значение для каждой строки показывает, с каким набором данных совпадает данная
строка. Как можно видеть в результатах исполнения этого сценария, это значение
может быть both (с обоими), left_only (только с левым) или right_only (только с
правым).
При отсутствии ошибок исполнение данного сценария должно дать следующий результат:
_merge
both
595866
right_only 171334
left_only 67333
dtype: int64
Что это все это означает? Похоже, что было успешно сопоставлено 393 866 записей
кредитов более старого файла (за август 2020 г.) с записями в более новом файле по
названию компании, названию обслуживающего кредитора и дате одобрения кредита. Метка right_only обозначает количество записей кредитов в более новом файле,
для которых не было совпадающих записей в более старом файле. (Этот набор данных считается правым в операции соединения, поскольку значение recent_ppp_data
было передано функции pd.merge() во втором аргументе.) Всего имеется 171 334
таких записей. Такое количество несовпадающих записей выглядит вполне возможным, поскольку за период с августа 2020 г. по февраль 2021 г. могло быть выдано большое количество новых кредитов.
Но значение 67 333 метки left_only представляет проблему. Это количество записей
о выданных кредитах в более старом наборе данных, для которых не было найдено
совпадений в более новом наборе. Это означает, что либо наш более новый набор
данных не является полным, либо у нас есть серьезные проблемы с качеством данных.
Из нашего предшествующего и достаточно поверхностного исследования наших
выборочных данных мы уже знаем, что со столбцами DateApproved могут быть некоторые проблемы. Поэтому давайте посмотрим, что будет, если устранить требования совпадения записей по этой дате. Для этого мы просто добавим фрагмент кода
из листинга 6.3 в код листинга 6.4, но опустив требование совпадения дат. Получившийся код приводится в листинге 6.5.
Оценка качества данных
|
233
Листинг 6.5. Сценарий ppp_data_join.py (продолжение)
# Опять соединяем данных двух наборов, но без требования совпадения по DateApproved
merged_data_no_date = pd.merge(august_ppp_data,recent_ppp_data,how='outer',
left_on=['BusinessName','Lender'],right_on=['BorrowerName',
'ServicingLenderName'],indicator=True)
# Отображаем значения в индикаторном столбце, по умолчанию обозначенном меткой _merge
print(merged_data_no_date.value_counts('_merge'))
Теперь результаты соединения этих двух наборов данных будут такими:
_merge
both
671942
right_only 96656
left_only 22634
dtype: int64
Иными словами, требуя совпадения записей только по названию компании и кредитора, мы "находим" еще 43 000 записей из более старого набора данных в более новом. Конечно же, мы не знаем, сколько из этих новых совпадений являются результатом ошибок при вводе данных (наподобие наблюдаемой ранее ошибки ввода
05/03/2020 вместо 05/01/2020), а также, сколько из них представляют многократную
выдачу кредитов11. Все, что нам известно, — так это то, что количество записей в более
старом наборе данных, которые мы не можем найти в новом, сократилось до 22 634.
Но что если мы просто проверим, присутствует ли данная компания в обоих наборах данных? Это кажется самой простой формой сравнения: теоретически, в течение многих месяцев банк или кредитор, обслуживающий кредит по программе PPP,
мог измениться или же могут (возможно) существовать дополнительные несовпадения из-за ввода данных с незначительными различиями. Помните следующее: на
данном этапе наша цель — просто оценить, насколько мы можем быть уверены, что
самый последний набор данных содержит все данные из более старого набора данных.
Поэтому давайте выполним последнее соединение с ослабленными требованиями и
посмотрим, что произойдет. Для этого мы добавим в наш сценарий фрагмент кода,
показанный в листинге 6.6, который сравнивает записи в обоих наборах данных
только по названию компании.
Листнинг 6.6. Сценарий ppp_data_join.py (продолжение)
# Снова соединяем оба набора данных, на этот раз только по столбцу
# BusinessName / BorrowerName
11
Управление по делам малого бизнеса начало принимать заявки на выдачу так называемых кредитов
второго раунда по программе PPP в январе 2021 г. (https://uschamber.com/co/run/business-financing/
second-draw-ppp-loans).
234
|
Глава 6
merged_data_biz_only = pd.merge(august_ppp_data,recent_ppp_data,how='outer',
left_on=['BusinessName'],right_on=['BorrowerName'],indicator=True)
# Отображаем значения в индикаторном столбце, по умолчанию обозначенном меткой _merge
print(merged_data_biz_only.value_counts('_merge'))
Исполнение этого сценария дает следующий результат:
_merge
both
706349
right_only 77064
left_only 7207
dtype: int64
Теперь ситуация выглядит немного лучше: мы "нашли" все 790 620 (706 349 + 77 064 +
+ 7 207) возможных кредитов, кроме 7 207, или чуть меньше 0,1% от общего количества. Это очень хорошо, и мы можем поддаться соблазну рассматривать это количество недостающих записей "ошибкой округления" и двигаться дальше. Но
прежде чем испытывать чувство удовлетворения за то, что мы нашли 99,9% всех
кредитов по программе PPP, остановимся на минутку и рассмотрим, что собой в
действительности представляет это "небольшое" количество недостающих данных.
Даже если предположить, что каждый их этих "недостающих" кредитов был на минимально возможную сумму (вспомним, что мы рассматриваем кредиты на сумму
от 150 000 долларов и выше), это все равно означает, что наш более новый набор
данных содержит неучтенных кредитов на сумму как минимум около $1 081 050
000 (т. е. свыше одного миллиарда долларов США). Учитывая, как многие тяжело
работают, чтобы заплатить налоги каждый год, хочется надеяться, что федеральное
правительство не собирается спокойно "потерять" один миллиард долларов налогоплательщиков. Но что можно сделать, чтобы найти эту сумму? Вот здесь мы подходим к той части работы с данными, которая может быть одновременно и сложной, и стимулирующей: настало время поговорить с людьми!
Хотя обращение к экспертам в предметной области всегда будет отличной отправной точкой для исследований качества данных, но есть нечто более интересное и
полезное: информация о самих кредиторах и получателях кредитов. Используя
комбинированную информацию с названиями компаний и их местонахождений,
мы, возможно, сможем найти контактную информацию по крайней мере для некоторого количества из наших 7 027 "утерянных" кредитов из более старого набора
данных (за август 2020 г.) и попытаться выяснить причину этой пропажи.
Прежде чем начинать звонить людям (и да, в большинстве случаев нужно именно
звонить), нужно определиться с вопросами, которые вы хотите им задать. Первоначально можно поддаться соблазну и подозревать какие-либо нечестные мотивы.
Например, кредиторы утаивают средства или компании предоставляют о себе неверную информацию. Но старая (и очень легко модифицируемая под разные ситуации) пословица гласит: никогда не следует усматривать злой умысел в том, что
Оценка качества данных
|
235
можно объяснить некомпетентностью, глупостью или небрежностью12. Иными словами, информация об этих кредитах отсутствует в более новом наборе данных, скорее всего, по причине каких-либо ошибок при вводе данных или, возможно, потому
что эти кредиты просто никогда не были одобрены.
Действительно, как выяснилось после нескольких звонков разным мастерским по
ремонту автомобильных кузовов и парикмахерским по всей стране, это была наиболее распространенная история. Многие люди, с которыми я разговаривала, описали похожую картину: они подавали заявку на получение кредита, им говорили,
что заявка была одобрена, а затем деньги просто не приходили. Пытаться подтвердить, что это было причиной в каждом отдельном случае "недостающего" кредита,
было бы непрактичным, но множественные компании, расположенные на расстояние тысяч километров друг от друга, излагали, по сути, одну и ту же историю. Это
вселяет в меня уверенность, что эти кредиты не отображаются в конечном наборе
данных по причине того, что деньги никогда не были отправлены13.
Несколько советов, как вести телефонный разговор
Если идея "холодных звонков" частным лицам или компаниям с запросом информации заставляет вас нервничать, поверьте, вы не одни такие. И хотя практика будет самым полезным подходом к преодолению этого состояния, наличие
краткого сценария для первых звонков может быть очень полезным. Далее приводится краткое изложение подхода к обзваниванию компаний (которое также
применимо и с частными лицами) для запроса информации. Отбросьте свою неуверенность, но помните, что вы просите занятых людей уделить вам свое время.
Будучи вежливым и тактичным, вы добьетесь намного больших результатов.
1. Представьтесь. Назовите свое имя и компанию. Изложите (вкратце), о чем вы
хотите говорить.
2. Уточните, может ли собеседник уделить вам время, и не начинайте излагать
причину своего звонка, не выяснив сначала, не занят ли ваш собеседник.
3. Если вы не уверены, с кем вы разговариваете, попросите позвать к телефону
владельца или менеджера компании. Если они заняты, спросите, когда можно
будет им перезвонить.
4. Вежливо спросите, с кем вы разговариваете, и подтвердите имя своего собеседника. Поблагодарите собеседника за уделенное вам время и завершите телефонный разговор.
12
Попытки атрибутировать пословицы всегда сопряжены с проблемами. И хотя я предпочитаю
использование терминов некомпетентность и небрежность, мне нравится пояснение этой пословицы в "Файле жаргона" (The Jargon file, https://jargon-file.org/archive/jargon-4.4.7.dos.txt) как "Бритва
Хенлона" (Hanlon's Razor), главным образом потому, что этот документ объясняет происхождение
многих терминов компьютерного и программистского сленга.
13
Безуловно, почему они никогда не были отправлены, само по себе является интересным вопросом.
236
|
Глава 6
Всегда записывайте основные пункты своего телефонного разговора. И когда вы
будете перезванивать, вы сможете позвать владельца или менеджера уже по имени (так как вы его записали ранее). Скажите, с кем вы разговаривали первый раз
и что он/она сказал, что вы могли перезвонить ему в это время. Если владелец
снова не сможет поговорить с вами, уточните, когда можно перезвонить, или попробуйте договориться о встрече, если это возможно.
Четко и откровенно объясните цель своего звонка, а при запросе информации
изложите подробно, что вы намерены делать с полученной информацией. В публикуемых работах никогда не указывайте личность кого бы то ни было, не получив на это их предварительное разрешение.
На данном этапе ясно, что наши данные по кредитам по программе PPP являются
достаточно полными для наших целей. Может показаться, что проверка нашего набора данных на соответствие всего лишь одному из почти дюжины требований
требует больших усилий, но имейте в виду, что в процессе мы узнали ценную информацию, благодаря которой сможем выполнить следующие проверки намного
быстрее или, возможно, не выполнять их вообще. Итак, перейдем к нашему следующему критерию.
Хорошо ли данные аннотированы?
Удовлетворившись, что наши данные являются актуальными и полными, нужно
начинать подробно разбираться с тем, какую информацию в действительности содержат столбцы более нового набора данных по кредитам по программе PPP14. Как
и в случае с задачей оценки полноты данных, решение задачи оценки качества их
аннотирования можно начинать с самих данных. Исследуя названия столбцов и содержащиеся в них значения, можно получить начальное представление о том, какая
дополнительная информация нам требуется. Если название столбца выглядит описательным и значения этого столбца соответствуют нашей интерпретации его названия, тогда у нас есть хорошая отправная точка.
Хотя названия столбцов и содержащиеся в них значения можно исследовать несколькими способами, давайте воспользуемся созданными ранее файлами с выборочными данными. Ширина экрана обычно недостаточна для отображения большого количества столбцов данных, а вот дополнительные строки можно легко про14
Возможно, вы заметили, что в этой главе я не рассматриваю критерии целостности данных точно в
том порядке, в котором они перечислены в главе 3. Поскольку программа PPP была смоделирована по
уже существующей кредитной программе 7(a) (https://journalofaccountancy.com/news/2020/apr/
paycheck-protection-program-ppp-loans-sba-details-coronavirus.html), я сделала предположение (сомнительное), что эти данные будут хорошо аннотированными. Конечно же, это были единственные
доступные наборы данных о программе PPP, поэтому у меня был ограниченный выбор (каким он
часто бывает при работе с реальными данными).
Оценка качества данных
|
237
сматривать, прокручивая экран вверх. Поэтому первым делом мы транспонируем
наши выборочные данные, т. е. преобразуем столбцы в строки, а строки в столбцы15. Кроме этого мы выполним дополнительную фильтрацию по типам данным,
чтобы облегчить определение, какие данные отсутствуют. В листинге 6.7 приводится код первого сценария для выполнения этой задачи.
Листинг 6.7. Сценарий ppp_columns_review.py
# Простой сценарий для просмотра всех названий столбцов таблицы с данными по программе
# PPP, чтобы увидеть, что можно предположить по ним об их данных
# Импортируем библиотеку pandas
import pandas as pd
# Считываем данные из более нового файла в структуру DataFrame библиотеки pandas,
# используя ее метод read_csv()
ppp_data_sample = pd.read_csv('recent_sample.csv')
# Преобразовываем все недостающие записи в значение <NA>, используя метод
# convertdtypes()
converted_data_sample = ppp_data_sample.convert_dtypes()
# Транспонируем все выборочные данные
transposed_ppp_data_sample = converted_data_sample.transpose()
# Отображаем полученные результаты
print(transposed_ppp_data_sample)
В целях ускорения процесса метод библиотеки pandas read_csv() преобразовывает все недостающие записи в значения типа NaN (Not a Number — не число), как
описывается в разделе https://pandas.pydata.org/pandas-docs/stable/user_guide/
missing_data.html. Эти значения преобразовываются в более общую (и интуитивно понятную) метку <NA> (Not Available — нет данных), обрабатывая всю структуру DataFrame методом сcovertdtypes(), как описывается в разделе
https://pandas.pydata.org/pandas-docs/stable/user_guide/missing_data.html#missingdata-na-conversion.
В результате исполнения этого сценария становится возможным одновременный
просмотр всех исходных названий столбцов как строк, вместе с парой исходных
строк данных в виде столбцов (рис. 6.5). Просматривая эти названия столбцов и
соответствующие данные, мы можем начинать получать представление о том, что
мы знаем, а что нет.
15
Для меня лично процесс транспонирования таблицы данных легче представить как "переворачивание" ее набок.
238
|
Глава 6
Рис. 6.5. Таблица выборочных данных в транспонированном виде
Благодаря сравнительно описательным названиям столбцов, мы можем предполагать, какие данные они, скорее всего, могут содержать. Например, значения столбцов с названиями DateApproved (дата одобрения), ProcessingMethod (метод обработки),
BorrowerName (имя заемщика), BorrowerAddress (адрес заемщика), BorrowerCity (город
заемщика), BorrowerState (штат заемщика) и BorrowerZip (почтовый индекс заемщи-
Оценка качества данных
|
239
ка) довольно легко понять. Но в некоторых случаях название столбца может быть
описательным, но не предоставлять всю требуемую нам информацию. Например,
название столбца SBAGuarantyPercentage предоставляет нам информацию как о содержимом этого столбца, так и о его единицах измерения (предположительно это
доля кредита в процентах, гарантируемая Управлением по делам малого бизнеса).
А вот название столбца Term не говорит нам ничего о его значении — то ли это 24
дня, недели, месяца или года. Аналогично, значения столбца BusinessAgeDescripion
описывают сами себя (например, Existing or more than 2 years old — существует
или возрастом свыше 2 лет), а значение Exemption 4 столбца LoanStatus не очень
способствует пониманию, какова была судьба данного кредита. Наконец, такие названия столбцов, как, например, LMIIndicator могут легко интерпретироваться экспертами, но лица, не владеющие терминологией в кредитной области, будут иметь
с ними трудности.
Таким образом, на данном этапе нам важно иметь в своем распоряжении словарь
данных, т. е. документ, который описывает содержимое данных табличного типа.
Эта необходимость объясняется тем, что хотя табличные данные очень удобны для
выполнения анализов, они по своей сути не предоставляют способа включения того
типа метаданных (т. е., данных о данных), которые нам требуются для получения
ответов на вопросы типа "Какие единицы измерения должны использоваться?" или
"Что означают закодированные категории?". Это именно те вопросы, которые часто
возникают при работе со сложными наборами данных.
Наиболее вероятным местонахождением словаря данных должно быть то же самое
место, где мы получили исходные данные. (Вспомните, что в главе 4 мы нашли
описание данных файла ghcnd-stations.txt в файле readme.txt, находящегося в той же
самой папке, что и данные.) В данном случае это означает, что нам нужно снова
посетить веб-сайт Управления по делам малого бизнеса (https://sba.gov/fundingprograms/loans/coronavirus-relief-options/paycheck-protection-program/ppp-data)
и посмотреть, не сможем ли мы найти на нем требуемые нам данные.
Поначалу ситуация выглядит довольно многообещающей. В разделе All Data на
этой странице находится ссылка, обещающая краткое изложение "ключевых аспектов данных", как показано на рис. 6.616.
Переход по этой ссылке (https://sba.gov/document/report-paycheck-protectionprogram-ppp-loan-data-key-aspects) приводит нас (на момент работы над данным
материалом) на страницу, содержащую ссылки на два документа PDF, датированные летом 2020 г. К сожалению, ни один из этих документов не содержит требуемую нам информацию. Они в основном заполнены текстом наподобие отказа от
ответственности о подробностях обработки кредитов по программе PPP. Но они все
же подтверждают наше открытие, что аннулированные кредиты не будут отображаться в базе данных (рис. 6.7).
16
Со времени написания этой главы эта страница претерпела значительные изменения. В частности,
главная страница расположения данных теперь содержит и словарь данных, но он был помещен туда
только несколько месяцев спустя после первой публикации данных.
240
|
Глава 6
Рис. 6.6. Целевая страница для данных по кредитам по программе PPP
на веб-сайте Управления по делам малого бизнеса США
Рис. 6.7. Фрагмент информации, которую следуем иметь в виду
при исследовании данных по программе PPP
Что же теперь делать? У нас есть несколько вариантов. Можно воспользоваться
более общей стратегией исследования, чтобы получить больше информации о программе PPP и, будем надеяться, заполнить некоторые из пробелов в наших знаниях.
Например, прочитав достаточное количество статей на веб-сайтах, направленных
на потенциальных кандидатов на участие в программе PPP, мы сможем выяснить,
что правильной единицей измерения для значений столбца Term, с большой долей
вероятности, являются недели. Аналогично, выполнив достаточное количество поисковых запросов в Интернете по терминам LMIIndicator, LMI Indicator и LMI
Indicator loans, можно, в конце концов, натолкнуться на страницу в Wikipedia, из
Оценка качества данных
|
241
которой можно будет предположить, что сокращение LMI может расшифровываться как Loan Mortgage Insurance. Но, тем не менее, мы никогда не сможем быть до
конца уверенными в правильности нашего предположения.
Иными словами, снова настало время обратиться за помощью к людям. Но к кому
именно необходимо обратиться? Можно начать со специалистов, которые уже изучали данные по программе PPP, но их будет достаточно трудно найти в связи с относительно недавней публикацией данных. Поэтому, точно так же, как и когда мы
пытались выяснить, что случилось со всеми теми недостающими кредитами из более старого набора данных, мы обратимся непосредственно к источнику данных:
Управлению по делам малого бизнеса. К счастью, веб-страница, с которой мы загрузили наши фактические файлы данных, содержит имя, связанное с этими загрузками: Stephen Morris (Стивен Моррис) (рис. 6.8)17.
Рис. 6.8. Веб-страница для загрузки данных по программе PPP
Я позвонила ему и отправила сообщение по электронной почте. На момент моего
запроса в Управление по делам малого бизнеса я еще не создала словаря данных
для данных по кредитам по программе PPP. Но Моррис предоставил мне ссылки
как на исходный файл PDF, так и на словарь данных для программы кредитования 7a
(Управления по делам малого бизнеса), на которой была основана структура программы кредитования PPP. Хотя файл более новых данных (расположенный по адресу https://data.sba.gov/dataset/7-a-504-foia) все равно значительно отличался от
столбцов данных по кредитам по программе PPP, тем не менее он содержал определенную информацию о некоторых ключевых элементах. Например, тот файл со17
Стоит обратить внимание на тот факт, что вскоре после того, как я связалась с Моррисом, эта атрибуция была изменена на SM.
242
|
Глава 6
держал описание столбца, который также назывался LoanStatus и чьи возможные
значения выглядели похожими на некоторые значения, которые мы на данном этапе обнаружили в данных по кредитам по программе PPP:
LoanStatus (Текущий статус кредита):
• NOT FUNDED = Не выдан
• PIF = Paid In Full (Полностью оплачен)
• CHGOFF = Charged Off (Списан)
• CANCLD = Canceled (Отменен)
• EXEMPT = Статус кредитов, которые были выданы, но не были отменены,
выплачены полностью или списаны, не подлежит разглашению
согласно Исключению 4 Закона о свободе информации
Так являются ли эти данные хорошо аннотированными? В некоторой степени — да.
Они не так хорошо аннотированы, как, скажем, данные Управления NOAA, с которыми мы работали в разд. "Данные фиксированной ширины" главы 4. Но, приложив
некоторые усилия, мы постараемся создать наш собственный словарь данных для
данных по кредитам по программе PPP, которому мы сможем доверять.
Являются ли данные крупномасштабными?
Поскольку мы завершили проверку наших данных на полноту, то мы уже практически ответили на вопрос, являются ли используемые нами данные в целом крупномасштабными. Потому как наличие более 750 тысяч строк данных будет вполне
достаточным для выполнения множества полезных анализов.
В то же самое время мы еще не можем сказать, какие именно виды анализов мы
сможем выполнять, поскольку количество строк имеющихся у нас данных не будет
иметь значения, если большинство из этих строк пустые. Так как же можно решить
эту проблему? Для столбцов, в которых мы ожидаем найти только несколько возможных значений, таких как LoanStatus, можно использовать метод библиотеки
pandas value_counts(), чтобы подсчитать их значения. А вот столбцы, которые могут
содержать очень разнообразные значения (такие как BorrowerName или BorrowerAddres),
нам нужно специально проверять на отсутствие значений, а затем сравнить полученный результат с общим количеством строк, чтобы получить представление о
возможном объеме отсутствующих данных. Для начала мы выполним подсчет в тех
столбцах, в которых, как мы думаем, будет содержаться только небольшое количество отдельных значений. Соответствующий код приводится в листинге 6.8. Мы
также подсчитаем количество значений NA в столбце с большим количеством возможных значений — BorrowerAddress.
Листинг 6.8. Сценарий ppp_columns_summary.py
# Простой сценарий для просмотра всех названий столбцов таблицы с данными по программе
# PPP, чтобы увидеть, что можно предположить о них по самим данным
Оценка качества данных
|
243
# Импортируем библиотеку pandas
import pandas as pd
# Считываем данные из более нового файла в структуру DataFrame библиотеки pandas,
# используя ее метод read_csv()
ppp_data = pd.read_csv('public_150k_plus_recent.csv')
# Отображаем количество отдельных значений, содержащихся в столбце LoanStatus
print(ppp_data.value_counts('LoanStatus'))
# Отображаем общее количество значений, содержащихся в столбце LoanStatus
print(sum(ppp_data.value_counts('LoanStatus')))
# Отображаем количество отдельных значений, содержащихся в столбце Gender
print(ppp_data.value_counts('Gender'))
# Отображаем общее количество значений, содержащихся в столбце Gender
print(sum(ppp_data.value_counts('Gender')))
# Отображаем количество строк с отсутствующими значениями для столбца BorrowerAddress
print(ppp_data['BorrowerAddress'].isna().sum())
В листинге 6.9 приводится результат исполнения сценария в листинге 6.8, по которому можно получить начальную картину о типе данных, фактически содержащихся в приблизительно 750 тысячах строк нашего набора данных.
Листинг 6.9. Количество отдельных значений столбцов более нового набора данных
LoanStatus
Exemption 4
549011
Paid in Full
110120
Active Un-Disbursed 107368
dtype: int64
766499
Gender
Unanswered
563074
Male Owned
168969
Female Owned 34456
dtype: int64
766499
17
Например, большинство кредитов имеют статус Exemption 4, который, как мы знаем из нашего исследования аннотирования, означает, что кредит "освобожден от
244
|
Глава 6
раскрытия согласно Исключению 4 Закона о свободе информации"18. Там можно
увидеть, что свыше двух третей претендентов на получение кредита при подаче
заявки не указали свой пол, а в 17 кредитах даже не указан адрес заемщика!
Так являются ли эти данные достаточно крупномасштабными? Да. Потому что
несмотря на то, что отсутствует довольно большое количество данных, полезная
информация содержится в достаточно большом количестве имеющихся у нас
строк и столбцов. Имейте также в виду, что в некоторых случаях можно получить
полезную информацию о чем-либо при помощи всего лишь нескольких десятков
строк данных, при условии, что они содержат действительно значащую информацию. Вот почему информации о размере файла с данными или даже о количестве
строк в нем будет недостаточно для того, чтобы подтвердить, что он действительно содержит крупномасштабные данные. Для этого необходимо изучить эти
данные подробнее.
Непротиворечивы ли данные?
Из нашей проверки на полноту данных мы узнали, что формат данных по кредитам
по программе PPP был абсолютно неоднородным между первой и последующими
публикациями. В частности, данные, опубликованные, начиная с декабря 2020 г.,
были намного более подробными, чем в предыдущих публикациях
(https://washingtonpost.com/business/2020/06/11/trump-administration-wont-say-whogot-511-billion-taxpayer-backed-coronavirus-loans). Кроме этого, даже названия
большинства столбцов в этих двух файлах были разными.
Поскольку теперь мы полагаемся на файл с более новыми данными, то нам нужно
думать о другом типе непротиворечивости, а именно, насколько непротиворечивыми являются значения в самом наборе данных. Например, мы можем надеяться, что
в столбцах типа InitialApprovalAmmount и CurrentApprovalAmount будут использоваться
одинаковые единицы измерения. Тем не менее лучше все-таки проверить, действительно ли это так. В листинге 6.10 приводится код сценария для быстрой проверки,
находятся ли в пределах ожидаемого значения минимального и максимального выданных кредитов.
Листинг 6.10. Сценарий pp_min_max_loan.py
# Сценарий для быстрого нахождения сумм минимального и максимального одобренного
# кредита, содержащихся в наборе данных по программе PPP
# Импортируем библиотеку pandas
import pandas as pd
18
Дополнительную информацию по запросам и исключениям по Закону о свободе информации см. в
разд. "Запросы FOIA/L" приложения В, а также в полном тексте данного исключения на веб-сайте
министерства юстиции США (https://justice.gov/oip/exemption-4-after-supreme-courts-ruling-foodmarketing-institute-v-argus-leader-media).
Оценка качества данных
|
245
# Считываем данные из более нового файла в структуру DataFrame библиотеки pandas,
# используя ее метод read_csv()
ppp_data = pd.read_csv('public_150k_plus_recent.csv')
# Извлекаем минимальное и максимальное значения, используя методы библиотеки pandas
# min() и max()
print(ppp_data['CurrentApprovalAmount'].min())
print(ppp_data['CurrentApprovalAmount'].max())
Судя по названию нашего файла данных — 150k_plus — можно было бы ожидать,
что минимальная сумма одобренного кредита будет $150 тысяч. Быстрый поиск в
Интернете по фразе "maximum loan under ppp" (максимальная сумма кредита по
программе PPP) ведет к документу19 на веб-сайте Управления по делам малого бизнеса, в котором указывается, что для большинства бизнесов максимальная сумма
кредита составляет $10 миллионов. Действительно, результаты исполнения нашего
сценария из листинга 6.10 подтверждают, что суммы кредитов в наших данных находятся в этих пределах:
150000.0
10000000.0
На данном этапе также следует выполнить проверку на одну из наиболее распространенных (и коварных) форм противоречивости: различия в правописании. Каждый раз при работе с данными, которые были введены людьми, будут встречаться
проблемы с орфографией: как минимум лишние пробелы, опечатки и разные знаки
препинания. Это представляет проблему, потому что если мы хотим быть в состоянии ответить на простой с виду вопрос типа "Сколько кредитов было выдано заемщиком X?", название этого банка должно писаться единообразно во всех записях
данных.
Поскольку это достаточно распространенная проблема с данными, то существует
несколько качественных подходов к ее решению. В нашем случае для проверки
единообразности написания названия банка мы воспользуемся так называемым методом "отпечатков пальцев" (fingerprinting). При поиске вариантов написания названий этих компаний их можно было бы кластеризовать разными способами (например, фонетически). Но в данном случае мы воспользуемся методом "отпечатков
пальцев", поскольку он следует простому, но эффективному алгоритму, который
сводит к минимуму риск сопоставления двух названий, которые в действительности должны быть разными.
19
Документ называется Paycheck Protection Program: How to Calculate Maximum Loan Amounts for
First Draw PPP Loans and What Documentation to Provide by Business Type. Загрузить его можно по
следующему адресу: https://sba.gov/sites/default/files/2021-01/PPP%20%20How%20to%20Calculate
%20Maximum%20Loan%20Amounts%20for%20First%20Draw%20PPP%20Loans%20%281.17.2021
%29-508.pdf.
246
|
Глава 6
Алгоритм метода "отпечатков пальцев", который мы будем использовать, выполняет следующие операции20:
1. Удаляет ведущие и замыкающие непечатные пробелы.
2. Заменяет все символы их представлением в нижнем регистре.
3. Удаляет все символы пунктуации и управления.
4. Нормализует расширенные символы латинского алфавита к их представлению
ASCII (например, gödel --> godel).
5. Разбивает строки на токены, разделенные пробелами.
6. Сортирует токены и удаляет дубликаты.
7. Соединяет токены обратно вместе.
Мы могли бы сами создать требуемый для этого код, но нам повезло, и один из членов сообщества Python уже проделал эту работу и создал библиотеку fingerprints,
которая устанавливается стандартным способом при помощи установщика pip:
pip install fingerprints
На данный момент главной задачей является выяснение, присутствуют ли в наших
данных различные написания названий компаний. Вопрос преобразования данных
для устранения этих различий рассматривается в главе 7. Так что сейчас мы просто
пересчитаем все однозначные названия банков в нашем наборе данных, а затем посмотрим, сколько однозначных отпечатков пальцев содержит полученный список.
Если все названия банков в файле действительно различны, то тогда теоретически
оба списка должны быть одинакового размера. Если же некоторые названия банков в
нашем наборе данных "однозначны" только вследствие, например, незначительных
различий в пунктуации и непечатных символов, тогда список отпечатков пальцев
будет меньшего размера, чем список "однозначных" названий банков. Это позволяет
предположить, что нам потребуется выполнить некоторые преобразования данных,
чтобы согласовать разные варианты названий одного и того же банка, чтобы гарантировать, например, возможность извлечения в случае надобности всех кредитов,
связанных с одним банком. Соответствующий сценарий приводится в листинге 6.11.
Листинг 6.11. Сценарий ppp_lender_names.py
# Сценарий для быстрого определения наличия опечаток в названиях банков, участвующих
# в программе PPP
# Импортируем библиотеку pandas
import pandas as pd
# Импортируем библиотеку fingerprints для создания унифицированных меток для всех
# названий банков в нашем наборе данных
import fingerprints
20
Чтобы получить дополнительную информацию по предмету кластеризации, посетите веб-страницу
https://github.com/OpenRefine/OpenRefine/wiki/Clustering-In-Depth.
Оценка качества данных
|
247
# Считываем данные из более нового файла в структуру DataFrame библиотеки pandas,
# используя ее метод read_csv()
ppp_data = pd.read_csv('public_150k_plus_recent.csv')
# Создаем список однозначных названий банков из столбца OriginatingLended нашего
# набора данных, используя для этого метод библиотеки pandas unique()
unique_names = ppp_data['OriginatingLender'].unique()
# Отображаем полученное количество однозначных названий банков
print(len(unique_names))
# Создаем пустой список для хранения отпечатка пальцев каждого однозначного названия
# банка
fingerprint_list = []
# Обрабатываем в цикле каждое название банка в полученном списке
for name in unique_names:
# Для каждого названия банка создаем его отпечаток пальцев и добавляем его в конец
# списка
fingerprint_list.append(fingerprints.generate(name))
# Удаляем дубликаты и сортируем список отпечатков пальцев fingerprint_list, используя
# для этого функцию set()
fingerprint_set = set(fingerprint_list)
# Отображаем количество элементов в списке отпечатков пальцев
print(len(fingerprint_set))
Исполнение этого сценария дает следующий результат21:
4337
4242
Как видно, "сырые" данные содержат 4 337 однозначных названий банков, но количество однозначных отпечатков пальцев для этих названий составляет всего
лишь 4 242. Разное количество элементов двух списков определенно предполагает
наличие в нашем наборе данных разных вариантов названий банков. Хотя в данном
случае разница составляет "всего лишь" около 100 элементов, эти различия могут
затрагивать тысячи строк данных, поскольку каждый банк выдал в среднем сотни
кредитов (750 000 / 4 337 = 173). В результате мы не можем сказать, сколько строк
нашего исходного набора данных содержат определенное "неправильное" название
21
При исполнении этого сценария может выводиться предупреждение, рекомендующее установить
библиотеку pyICU. Но установка этой библиотеки сопряжена с некоторыми сложностями, и ее
наличие не изменит результатов исполнения этого сценария. Если вы планируете широко использовать этот сценарий, то будет полезно потратить немного времени на установку этой библиотеки.
Дополнительную информацию по ее установке можно найти здесь: https://pypi.org/project/PyICU.
248
|
Глава 6
(также мы не можем быть уверенными в том, что это количество исчерпывающее).
В разд. "Исправление разнообразности написаний" главы 7 мы рассмотрим процесс
преобразования наших данных при помощи отпечатков пальцев, чтобы улучшить
идентификацию конкретных кредиторов и заемщиков.
Многомерны ли наши данные?
Во многом подобно тому, как набор данных, содержащий большое количество
строк, может с большой вероятностью быть крупномасштабным, если он содержит
большое количество столбцов, он так же может с большой вероятностью быть и
многомерным. Но, так же как и определение крупномасштабности, определение,
делают ли имеющиеся столбцы наши данные действительно многомерными, также
требует выполнения проверки качества содержащихся в них данных.
Например, хотя наш набор данных по кредитам по программе PPP содержит 50
столбцов данных, около десяти из этих столбцов содержат расширенные адресные
данные, поскольку адреса заемщика, первоначального кредитора и обслуживающего кредитора разбиты на отдельные столбцы для названия улицы, города, штата и
почтового индекса. При этом остальные столбцы могут содержать однозначные
данные и нам нужно получить представление об их содержимом, чтобы понимать, с
каким количеством характеристик или особенностей данных нам в действительности нужно работать.
Например, для скольких кредитов в нашем наборе данных указано иное назначение, нежели выплата зарплаты? Хотя эта структура данных (и сама кредитная программа) позволяет заемщикам использовать полученный кредит в иных целях
(например, расходы на здравоохранение и выплату арендной платы, см.
https://sba.gov/funding-programs/loans/covid-19-relief-options/paycheck-protectionprogram/first-draw-ppp-loan), в каком объеме эти цели отражены в данных?
Точно так же, как и при оценке крупномасштабности наших данных, мы исследуем
содержимое нескольких дополнительных столбцов, чтобы определить, действительно ли предполагаемая их названиями подробность подтверждается содержащимися в них данными. Соответствующий сценарий приводится в листинге 6.12.
В нем мы исследуем, сколько строк столбца PROCEED не содержат значений.
Листинг 6.12. Сценарий ppp_loan_uses.py
# Сценарий для быстрой проверки указанного заемщиками целевого назначения кредита
# по программе PPP
# Импортируем библиотеку pandas
import pandas as pd
# Считываем данные из более нового файла в структуру DataFrame библиотеки pandas,
# используя ее метод read_csv()
ppp_data = pd.read_csv('public_150k_plus_recent.csv')
Оценка качества данных
|
249
# Отображаем количество строк с отсутствующими значениями для столбца UTILITIES_PROCEED
print(ppp_data['UTILITIES_PROCEED'].isna().sum())
# Отображаем количество строк с отсутствующими значениями для столбца PAYROLL_PROCEED
print(ppp_data['PAYROLL_PROCEED'].isna().sum())
# Отображаем количество строк с отсутствующими значениями для столбца
# MORTGAGE_INTEREST_PROCEED
print(ppp_data['MORTGAGE_INTEREST_PROCEED'].isna().sum())
# Отображаем количество строк с отсутствующими значениями для столбца RENT_PROCEED
print(ppp_data['RENT_PROCEED'].isna().sum())
# Отображаем количество строк с отсутствующими значениями для столбца
# REFINANCE_EIDL_PROCEED
print(ppp_data['REFINANCE_EIDL_PROCEED'].isna().sum())
# Отображаем количество строк с отсутствующими значениями для столбца
# HEALTH_CARE_PROCEED
print(ppp_data['HEALTH_CARE_PROCEED'].isna().sum())
# Отображаем количество строк с отсутствующими значениями для столбца
# DEBT_INTEREST_PROCEED
print(ppp_data['DEBT_INTEREST_PROCEED'].isna().sum())
# Создаем новую структуру DataFrame, которая содержит все строки, в которых
# назначением кредита указана выплата зарплаты, а все другие расходы указываются
# как NA (нет данных)
payroll_only = ppp_data[(ppp_data['UTILITIES_PROCEED'].isna()) & (ppp_data
['MORTGAGE_INTEREST_PROCEED'].isna()) & (ppp_data
['MORTGAGE_INTEREST_PROCEED'].isna()) & (ppp_data['RENT_PROCEED'].isna()) &
(ppp_data['REFINANCE_EIDL_PROCEED'].isna()) & (ppp_data
['HEALTH_CARE_PROCEED'].isna()) & (ppp_data['DEBT_INTEREST_PROCEED'].isna())
]
# Отображаем количество строк с назначением только на зарплату
print(len(payroll_only.index))
Результат исполнения этого сценария показан в листинге 6.13.
Листинг 6.13. Количество каждого отдельного указанного целевого назначения кредитов
по программе PPP
570995
1828
719946
666788
743125
708892
734456
538905
250
|
Глава 6
Как видно по результатам исполнения этого сценария, большинство заемщиков (по
сути, все, кроме 1 828) указали целевым назначением кредита выплату зарплаты.
При этом меньше одной трети заемщиков указали, что они (возможно) также планировали использовать полученные по кредиту средства для оплаты коммунальных
услуг. Еще одна часть заемщиков сообщили, что они собирались использовать кредитные средства для оплаты аренды. При этом наш последний тест показывает, что
свыше двух третей всех заемщиков указали только выплату зарплаты как целевое
назначение кредитных средств.
Что все это означает в контексте многомерности наших данных? Даже если мы решим не учитывать дополнительные столбцы, содержащие подробности адреса, или
с виду малоиспользуемые столбцы PROCEED, этот набор данных все равно содержит
довольно большой объем информации. Поэтому будем считать, что мы можем использовать эти данные по крайней мере для того, чтобы начать делать выводы о
том, кто получил кредиты по программе PPP и как они использовали эти средства.
Но, как всегда, мы должны самостоятельно проверить и убедиться, что столбцы
или строки нашего набора данных содержат значения.
Атомарны ли данные?
Это еще один случай, когда результаты нашей предыдущей работы с данными позволяют нам довольно быстро дать положительный ответ касательно этого показателя целостности данных. Хотя более старая версия данных (за август 2020 г.) содержит только диапазоны сумм кредитов, мы знаем, что этот набор данных содержит один кредит в каждой строке, включая точную сумму кредита в долларах.
Поскольку мы располагаем конкретными числами, а не сводными или агрегированными значениями, мы можем быть уверенными в том, что наши данные достаточно
детализированные (или атомарные) для поддержания широкого спектра анализов
данных, которые нам потребуется выполнить в будущем.
Понятны ли данные?
Хотя этот набор данных не оказался особенно хорошо аннотированным, мы, тем не
менее, смогли выполнить для него многие проверки на целостность данных. Это
было возможным благодаря тому, что названия его столбцов и их значения довольно понятны. Например, если мы не были уверены, что означает название столбца
CD, увидев некоторые из его значений (например, CS-05), можно довольно легко
предположить, что это сокращение расшифровывается как congressional district
(округ для голосования в Конгресс США). По мере накопления нами опыта работы
с общедоступными и государственными наборами данных (или работы в определенной предметной области) мы сможем все быстрее понимать коды, термины и
жаргонные слова.
В случае столбцов, чьи названия были не сразу понятны, они стали для нас более
ясными после обмена несколькими сообщениями со Стивеном Моррисом и Управ-
Оценка качества данных
|
251
лением по делам малого бизнеса. Например, Моррис подтвердил, что для столбца
Term правильными единицами измерения были месяцы (а не недели, как казалось
более вероятным сначала), и что значения столбцов PROCEED описывают целевое назначение кредитных средств согласно "информации, предоставленной заемщиком
Управлению по делам малого бизнеса (как сообщено их заемщиком в форме заявки
на кредит)".
Моя переписка с Моррисом также проиллюстрировала, почему обращение к эксперту по первоисточнику, если таковое вообще возможно, является важным шагом
в выполнении проверок на целостность данных. Вспомните, что вначале мне не
было понятно значение названия столбца LMIIndicator. Поскольку мои строки выборочных данных не содержали значений в этом столбце, я выполнила ряд поисков в
Интернете и получила результат, включающий такое возможное сокращение, как
lenders mortgage insurance — страхование ипотеки для кредиторов (рис. 6.9).
Какая же с этим проблема? Дело в том, что это неправильное значение этого сокращения. В своем сообщении (по электронной почте) Моррис объяснил мне, что
правильное значение будет Low/Moderate Income (низкий/средний уровень доходов), а
столбец LMI Indicator содержит значения, указывающие, находится ли заемщик географически в зоне с низким или средним уровнем доходов.
Рис. 6.9. Результаты поиска в Интернете значения термина LMI
252
|
Глава 6
Урок, который можно извлечь из этого, состоит в том, что если у вас нет официального словаря данных, нужно всегда быть осторожным с интерпретацией названий и значений столбцов. Даже те из них, которые могут казаться вам абсолютно
понятными, могут означать совсем не то, что вы думаете, что они означают. Если у
вас есть хоть минимальные сомнения, непременно свяжитесь с каким-либо специалистом или, в идеале, с теми людьми, которые собрали эти данные, чтобы подтвердить свое предположение.
Размерностно структурированы ли данные?
Если понятие размерностно структурированных данных казалось немного абстрактным при его рассмотрении в главе 3, будем надеяться, что теперь, когда в нашем распоряжении есть настоящий набор данных, оно немного более понятно.
Размерностно структурированные данные включают информацию о полезных категориях или классах, которые можно использовать для группирования данных, совместно с более атомарными свойствами, облегчающими выполнение более гранулярных анализов.
В случае данных по кредитам по программе PPP я бы сказала, что в число этих полезных "размерностных" столбцов данных входят столбцы RuralUrbanIndicator,
HubzoneIndicator, LMIIndicator (чье значение мы недавно выяснили), NAICSCode и в некоторой степени столбец SBAOffice Code. Столбцы RaceEthnicity, Gender и Veteran
также могли бы быть размерностно полезными, но поскольку они содержат много
значений Unanswered, то это ограничивает объем полезной информации, которую
можно извлечь из них. При этом другие столбцы могут дать ответы на вопросы касательно местонахождения и типа компаний-заемщиков, которые на данном этапе
воспользовались программой PPP.
Более того, такие столбцы, как, например, NAICSCode, предоставляют нам возможность дополнить наш набор данных информацией о том, к каким отраслям относятся компании, воспользовавшиеся программой PPP. Эту информацию можно было
бы сравнить с набором данных Бюро трудовой статистики США или данными о
секторах занятости других государственных учреждений. Мы рассмотрим этот
процесс более подробно в разд. "Дополнение данных" главы 7.
Пока что мы смогли дать положительный ответ лишь на некоторые вопросы касательно целостности данных, при этом ответы на другие вопросы идут с определенными оговорками, предполагая необходимость в дополнительных преобразованиях
и оценках, прежде чем можно будет переходить непосредственно к фазе анализа
данных. Но прежде чем заниматься этими преобразованиями, нам нужно обратиться к критическому вопросу (или вопросам) по соответствию данных: демонстрируют ли наши данные достоверность, надежность и репрезентативность, которая требуется для того, чтобы делать осмысленные выводы о влиянии программы PPP на
малый бизнес в Соединенных Штатах.
Оценка качества данных
|
253
Оценка соответствия данных
Оценив целостность нашего набора данных почти по дюжине различных показателей, можно приступать к оценке уровня его соответствия нашим целям, т. е. действительно ли эти данные могут дать нам ответы на задаваемые нами вопросы. Для
этого мы используем три основных критерия: достоверность, надежность и репрезентативность данных. Теперь настало время изучить наш набор данных с учетом
нашего первоначального вопроса: помогла ли программа PPP спасти малый американский бизнес?
Достоверность данных
Вспомним, что наше рабочее определение достоверности — это "степень, в которой нечто измеряет то, что оно должно измерять". Даже если бы наши данные по
кредитам по программе PPP были идеальными, нам нужно как-то определить, помогут ли они найти ответ на наш вопрос. На данном этапе мы знаем, что наш набор
данных предоставляет важную часть этого ответа, поскольку мы уже достаточно
уверены в том, что он описывает, для каких компаний был одобрен кредит по программе PPP. Посредством наших исследований его целостности (особенно касательно его полноты) и вследствие нашего поиска аннотационной информации или
метаданных мы также достаточно уверены в том, что он не содержит аннулированных кредитов. Мы убедились в этом как при помощи опубликованной информации
Управления по делам малого бизнеса о программе PPP, так и посредством прямых
контактов с компаниями-заемщиками.
Мы также можем использовать элементы нашего набора данных (особенно значения столбцов LoanStatus и LoanStatus Date), чтобы получить представление о том,
какие компании из свыше 750 тысяч компаний, одобренных для получения кредита, фактически получили средства. Для этого мы подсчитаем количество отдельных
значений столбца LoanStatus, используя метод value_counts(), как мы уже делали это
ранее. Соответствующий сценарий приводится в листинге 6.4.
Листинг 6.14. Сценарий ppp_loan_status.py
# Сценарий для определения количества выданных кредитов
# Импортируем библиотеку pandas
import pandas as pd
# Считываем данные из более нового файла в структуру DataFrame библиотеки pandas,
используя
# ее метод read_csv()
ppp_data = pd.read_csv('public_150k_plus_recent.csv')
# Отображаем количество отдельных значений, содержащихся в столбце LoanStatus
print(ppp_data['LoanStatus'].value_counts())
print(sum(ppp_data['LoanStatus'].value_counts()))
254
|
Глава 6
Обратите внимание на то, что поскольку метод value_counts() игнорирует значения NA, я также суммирую все записи, чтобы обеспечить подсчет всех строк.
Результат исполнения этого сценария приводится в листинге 6.15.
Листинг 6.15. Подсчет отдельных значений столбца LoanStatus
Exemption 4
549011
Paid in Full
110120
Active Un-Disbursed 107368
Name: LoanStatus, dtype: int64
766499
Как мы видим, из 766 499 кредитов свыше 100 тысячам заемщиков средства еще не
были отправлены, а другие 100 тысяч заемщиков уже выплатили свою задолженность.
Таким образом, чтобы оценить судьбу малых бизнесов, которые получили кредиты
по программе PPP, нам нужно работать только с теми из них, которые фактически
получили средства. Иными словами, наш запрос должен ограничиваться теми заемщиками, для которых значение столбца LoanStatus равно Exemption 4 или Paid in
Full.
Теоретически, когда бизнесы подавали заявки на кредит по программе PPP, они
запрашивали достаточную сумму, чтобы удерживаться на плаву, поэтому мы можем поддаться соблазну и предположить, что если бизнес получил кредитные
средства, то дела у него должны идти хорошо. Но точно так же, как потенциально
слишком ослабленные критерии могли позволить многим бизнесам мошеннически получить кредиты по программе PPP (https://papers.ssrn.com/sol3/papers.cfm?
abstract_id=3906395), сам факт получения бизнесом кредитных средств не является
гарантией того, что он продолжает успешно функционировать. Эта реальность иллюстрируется статьей в журнале Wall Street Journal о компании, которая объявила о
банкротстве, несмотря на то что получила кредит по программе PPP
(https://wsj.com/articles/hundreds-of-companies-that-got-stimulus-aid-have-failed11605609180). Поскольку мы уже знаем, что данный бизнес объявил о банкротстве,
если мы найдем его запись в нашем наборе данных, то мы сможем получить представление о том, как такие записи могут потенциально выглядеть. Сценарий для
поиска этой записи приводится в листинге 6.16.
Листинг 6.16. Сценарий ppp_find_waterford.py
# Сценарий для нахождения определенного бизнеса в нашем наборе данных
# по его (частичному) названию
# Импортируем библиотеку pandas
import pandas as pd
Оценка качества данных
|
255
# Считываем данные из более нового файла в структуру DataFrame библиотеки pandas,
# используя ее метод read_csv()
ppp_data = pd.read_csv('public_150k_plus_recent.csv')
# Создаем и заполняем структуру DataFrame, не содержащую строк с отсутствующими
# значениями столбца BorrowerName
ppp_data_named_borrowers = ppp_data[ppp_data['BorrowerName'].notna()]
# Поскольку точное сопоставление может быть сложной задачей, воспользуемся методом
# библиотеки pandas str.contains()
bankruptcy_example = ppp_data_named_borrowers[ \
ppp_data_named_borrowers['BorrowerName']
.str.contains('WATERFORD RECEPTIONS')]
# Транспонируем результат, чтобы его было легче читать
print(bankruptcy_example.transpose())
Библиотека pandas не может выполнять поиск строк в столбцах, содержащих
значение NA (нет данных), поэтому для целей проверки наша структура DataFrame не
должна содержать строк с такими значениями в целевом столбце. (Очевидно, что в
других отношениях кредиты без названий заемщика могут представлять интерес
для более подробного исследования.)
Хотя метод str.contains() и может выполнять успешный поиск только по части
строки, однако стоит учесть, что он чувствителен к регистру. Поэтому тот факт, что
названия компаний-заемщиков полностью написаны ЗАГЛАВНЫМИ БУКВАМИ,
имеет значение!
Результат исполнения этого сценария приводится в листинге 6.17. Он предоставляет нам интересную информацию: данный кредит имеет статус Exemption 4, а также,
что, возможно, еще более интересно, значение его столбца LoanStatusDate равно NA.
Но во всех остальных отношениях нет никаких признаков, что этот бизнес больше
не работает.
Листинг 6.17. Результат исполнения сценария ppp_find_waterford.py
LoanNumber
DateApproved
SBAOfficeCode
ProcessingMethod
BorrowerName
BorrowerAddress
BorrowerCity
BorrowerState
BorrowerZip
LoanStatusDate
7560217107
04/14/2020
353
PPP
WATERFORD RECEPTIONS, LLC
6715 COMMERCE STREET
SPRINGFIELD
VA
22150
NaN
256
|
Глава 6
LoanStatus
Term
SBAGuarantyPercentage
InitialApprovalAmount
CurrentApprovalAmount
UndisbursedAmount
FranchiseName
ServicingLenderLocationID
ServicingLenderName
ServicingLenderAddress
ServicingLenderCity
ServicingLenderState
ServicingLenderZip
RuralUrbanIndicator
HubzoneIndicator
LMIIndicator
BusinessAgeDescription
ProjectCity
ProjectCountyName
ProjectState
ProjectZip
CD
JobsReported
NAICSCode
RaceEthnicity
UTILITIES_PROCEED
PAYROLL_PROCEED
MORTGAGE_INTEREST_PROCEED
RENT_PROCEED
REFINANCE_EIDL_PROCEED
HEALTH_CARE_PROCEED
DEBT_INTEREST_PROCEED
BusinessType
OriginatingLenderLocationID
OriginatingLender
OriginatingLenderCity
OriginatingLenderState
Gender
Veteran
NonProfit
Exemption 4
24
100
413345.0
413345.0
0.0
NaN
122873
EagleBank
7815 Woodmont Ave
BETHESDA
MD
20814
U
N
NaN
New Business or 2 years or less
SPRINGFIELD
FAIRFAX
VA
22150-0001
VA-08
45.0
722320.0
Unanswered
NaN
413345.0
NaN
NaN
NaN
NaN
NaN
Limited Liability Company(LLC)
122873
EagleBank
BETHESDA
MD
Male Owned
Non-Veteran
NaN
Более того, добавив следующую строку кода в конец нашего сценария, можно проверить, сколько кредитов имеют значение NA в столбце LoanStatusDate. Мы увидим,
что результат будет идеально совпадать с теми кредитами, чье значение LoanStatus
равно Exemption 4.
print(sum(ppp_data['LoanStatusDate'].isna()))
Оценка качества данных
|
257
Так измеряют ли эти данные кредитов по программе PPP то, что они должны измерять? Я бы сказала, что да, измеряют, но не полностью. Как мы видели по результатам подсчета значений столбца LoanStatus, не все компании в нашем наборе данных
фактически получили кредитные средства. Они были одобрены и все еще имеют
право получить эти деньги (мы знаем, что их кредиты не были аннулированы), но
107 368 этих компаний все еще не получили эти деньги, и мы не можем знать, получат ли они их когда-либо.
Также по этому набору данных нельзя получить информацию о дальнейшей судьбе
компаний, получивших кредитные средства. Некоторые из них могут продолжать
работать, а другие, возможно, обанкротились. А третьи могли быть ликвидированы,
не объявляя банкротство. Иными словами, данные программы PPP имеют высокую
достоверность при ответе на определенные части нашего вопроса, но чтобы получить ответ на весь вопрос целиком, нам потребуется намного больше, чем только
этот один набор данных.
Надежность данных
Что касается надежности данных, мы в первую очередь заинтересованы в таких
критериях, как точность и стабильность. Или, иными словами, насколько хорошо
данные программы PPP отражают, кто получил кредиты по этой программе, а также, насколько вероятно, что картина того, кто получил кредиты, изменится со временем.
Благодаря нашим предшествующим исследованиям, на данный момент мы знаем,
что стабильность этого набора данных далека от идеальной. Несколько тысяч бизнесов, которые получили одобрение на получение кредита и содержатся в более
старом наборе данных (за август 2020 г.), отсутствуют в более новом наборе данных (мы рассмотрим вызываемые этим обстоятельством последствия на репрезентативность в следующем разделе). Это соответствует информации в документации Управления по делам малого бизнеса22, что аннулированные кредиты не включаются в этот набор данных23. Также неясно, будут ли предыдущие версии данных
доступны по мере обновлений набора данных. Это затрудняет задачу определения,
какие были выполнены в нем изменения, если только не загружать и архивировать
каждый его выпуск самостоятельно.
Даже числа в самом наборе данных могут быть не особенно стабильными с течением времени. Например, мы знаем, что 538 905 компаний-заемщиков указали, что
они будут использовать кредит по программе PPP для выплаты зарплат. Но, как
пояснил представитель Управления по делам малого бизнеса Стивен Моррис в од22
Информацию можно загрузить по этому адресу: https://sba.gov/document/report-paycheck-protectionprogram-ppp-loan-data- key-aspects.
23
Опять же, понимание, почему и как эти кредиты были аннулированы, может быть полезным, но мы
не найдем эту информацию в этом наборе данных, здесь есть только некоторые детали, указывающие
на то, где начинать ее искать.
258
|
Глава 6
ном из своих сообщений (по электронной почте): "Эти данные в некоторой степени
обманчивы, поскольку заемщик не обязан использовать средства на указанные в
заявке цели". Иными словами, если только в какой-либо части процесса прощения
или погашения кредита не требуется, чтобы заемщик предоставил подробную информацию о том, как были потрачены эти деньги (и эта информация впоследствии
не обновится в этом наборе данных), мы не можем быть уверены в точности или
стабильности чисел в разных столбцах PROCEED.
Репрезентативность данных
Представляют ли данные по кредитам по программе PPP всех, кто фактически получили такие кредиты? Скорее всего, да. После того как публичное негодование
вынудило многие крупные и/или публичные компании возвратить кредиты по программе PPP, выданные на ранних этапах этой программы (https://nbcnews.com/
business/business-news/which-companies-are-returning-their-ppp-loan-here-s-listn1194566), Управление по делам малого бизнеса дало понять, что они внимательно
изучат кредиты на сумму свыше $2 миллиона. В результате к началу июля 2020 г.
было возвращено или аннулировано кредитов на почти $30 миллиардов
(https://cnbc.com/2020/07/06/companies-returned-30-billion-in-small-business-loansfrom-ppp.html). После наших сравнительно всесторонних сравнений более старого
(август 2020 г.) и более нового (февраль 2021 г.) наборов данных мы можем быть
довольно уверенными в том, что имеющийся у нас набор данных репрезентативен
по крайней мере для тех, кто получил кредит по программе PPP на сегодняшний
день.
В то же самое время это не предоставляет нам столько полезной информации,
сколько можно показаться. Мы уже знаем, что подавляющее большинство получателей кредитов по программе PPP в этом наборе данных не указали свой пол, расу,
этничность или статус прохождения военной службы (veteran). Это означает, что у
нас нет действенного способа узнать, насколько хорошо (если вообще) демографические показатели получателей кредитов по программе PPP отражают численность
малых бизнесов в Соединенных Штатах. Более того, как мы увидим в главе 9, очень
маловероятно, что мы вообще сможем сделать какие-либо выводы касательно демографии получателей кредитов по программе PPP, по причине очень небольшого
количества заявителей, которые предоставили эту информацию.
Но вопрос репрезентативности в действительности намного глубже, чем этот аспект, — к тем 7 207 кредитам (и дальше), которые исчезли из набора данных между
его первой и последней публикацией. Эти пропавшие записи кредитов отражают
компании, которые подали заявку на кредит и получили одобрение на его получение, но, по словам одного сотрудника: "Деньги просто так и не поступили". Это
означает, что хотя мы знаем, сколько компаний получили кредит по программе PPP,
у нас нет никакой возможности узнать, сколько компаний подали заявку на получение кредита, поскольку эти аннулированные кредиты были удалены из набора данных. Таким образом, у нас теперь есть проблема, которую я люблю называть проблемой "знаменателя".
Оценка качества данных
|
259
Проблема знаменателя
Проблема знаменателя известна почти во всех областях исследований на основе
данных, хотя она может называться и по-другому, например эталонной тестовой
(benchmarking) или базисной (baseline) проблемой24. Эта проблема обобщает сложность попытки извлечь смысл из данных, когда у вас нет достаточной сравнительной информации, чтобы поместить ее в контекст. В большинстве случаев причиной
этому является то, что требуемые для сравнения данные никогда не были собраны.
В наших исследованиях данных программы PPP нам уже встречалась одна версия
этой проблемы: мы знаем, какие компании получили кредиты, но мы не знаем,
какие компании подали заявку, но им было отказано, или причину отказа (по
крайней мере в некоторых случаях получатели не знают причину). Это представляет проблему при оценке процесса выдачи кредитов по программе PPP, поскольку мы не знаем, было ли отказано в получении кредита законным заявителям, в то
время как некоторым компаниям было выдано по несколько кредитов. Если мы
хотим знать, была ли выдача кредитов справедливой или даже эффективной, информация о том, кто не был включен в программу, представляет такую же важность, как и о том, кто был.
Некоторые из проблем знаменателя, с которыми нам пришлось столкнуться до сих
пор, можно попоробовать решить, используя какие-либо дополнительные данные.
Именно это сделали в журнале Wall Street Journal при сравнении данных программы PPP с данными по объявлениям о банкротстве. Если вместе с обновленными
данными поставщик данных не предоставляет более старые данные, что, похоже,
происходит в случае с данными по программе PPP, то для решения таких проблем
нам придется создать свой собственный архив.
Заключение
Так помогли ли кредиты по программе PPP спасти малые американские бизнесы?
Возможно. С одной стороны, трудно представить, что можно было бы потратить
столько денег и не получить какого-либо положительного результата. Но, с другой
стороны, как реальный мир, так и имеющиеся у нас данные о нем, представляют
собой сложный и взаимозависимый беспорядок. Если получатель кредита по программе PPP изменил модель своего бизнеса и нашел новые потоки дохода, поместили бы мы его в категорию бизнесов, "спасенных" этой программой, или нет?
Аналогично, если бизнес, который не получил кредита, потерпел неудачу, так это
потому, что он не получил кредита или же он бы потерпел неудачу в любом случае?
Чем больше таких "что если", тем выше вероятность следующих двух развитий:
1. У нас начнется ужасная головная боль, мы решим, что по-настоящему ничего
невозможно узнать и начнем искать оправдание, чтобы отложить работу над задачей на потом.
24
Термин "проблема знаменателя" (denominator problem), по-видимому, имеет специфическое значение при
использовании в области права собственности США, но я использую его здесь не совсем в этом значении.
260
|
Глава 6
2. Некоторое время вы играете в разные игры, бродите по Интернету или жалуетесь ничего не понимающим друзьям и родственникам на то, что вы потратили
массу времени на выпас набора данных, который, возможно, абсолютно бесполезен и вы с трудом можете вспомнить, почему вообще начали работать над
ним. И тут вам на ум приходит что-то, что наталкивает вас на другой подход,
возможно, более узкого вопроса для исследований, и вы возбужденно возвращаетесь к своему набору данных, горя нетерпением узнать, можно ли на этот
вопрос дать лучший ответ, чем на предыдущий.
Является ли этот процесс трудоемким и извилистым? Да, является. Он также напоминает наше рассмотрение темы конструктивной достоверности во врезке "Как? И
для кого?" в главе 3. В конечном итоге, именно так в действительности формируются новые знания. Неопределенная, раздражающая и трудновыполнимая работа
по рассмотрению, обдумыванию и тестированию новых вариантов, с последующим
(потенциально) запуском всего процесса заново с небольшим объемом дополнительной информации и понимания — вот так добываются по-настоящему оригинальные знания. Это то, что каждая алгоритмическая система в мире отчаянно пытается аппроксимировать или имитировать. Но если вы готовы приложить усилия,
то ваши усилия действительно могут увенчаться успехом.
На данный момент я чувствую, что узнала довольно много об этом наборе данных.
И прихожу к выводу, что его, вероятнее всего, будет недостаточно, чтобы получить
ответ на мой исходный вопрос, но я все равно могу извлечь некоторые интересные
выводы, которые можно сделать на его основе. Например, я уверена в том, что более новый набор данных точно отражает состояние кредитов, одобренных в настоящее время, поскольку я убедилась в том, что отсутствующие в более новом наборе данных кредиты никогда не были выданы (по той или иной причине). В то же
самое время, хотя Управление по делам малого бизнеса объявило, что по состоянию на начало января 2021 г. было прощено кредитов по программе PPP на сумму
свыше 100 миллиардов долларов (https://sba.gov/article/2021/jan/12/11-millionpaycheck-protection-program-loans-forgiven-so-far-totaling-over-100-billion), даже
спустя шесть недель столбец LoanStatus не содержит отдельного значения для обозначения таких кредитов. В начале марта Стивен Моррис из Управления по делам
малого бизнеса перестал отвечать на мои запросы по электронной почте, но по состоянию на май 2021 г. для данного набора данных стал доступным словарь данных25, даже если ни он, ни обновленные данные не содержат эту информацию.
Конечно же, здесь еще много чего можно узнать: какие компании получили кредиты и где они находятся, на какую сумму были эти кредиты одобрены, а также, кто
их выдавал. И хотя эти данные далеко не идеальные, я могу хранить их старые копии, чтобы в случае каких-либо значительных изменений в будущем у меня под
рукой были архивы предыдущих данных. Учитывая все это, настало время перейти
от этапа оценки данных и приступить к задаче их очистки и преобразования, чем
мы и займемся в главе 7.
25
Этот словарь можно загрузить по адресу https://data.sba.gov/dataset/ppp-foia/resource/aab8e9f936d1-42e1-b3ba-e59c79f1d7f0.
ГЛАВА 7
Очистка, преобразование
и дополнение данных
В большинстве случаев данные, которые мы находим, собираем или получаем, не
совсем отвечают нашим требованиям. Например, они могут иметь неудобный формат, неправильную структуру или неподходящие единицы измерения. Сами данные
могут содержать ошибки или разрывы в последовательностях, быть неоднородными. Они могут содержать непонятные ссылки или намеки на дополнительные нереализованные возможности. Какими бы недостатками ни обладали исходные данные,
в нашем стремлении использовать эти данные как источник знаний нам неизбежно
придется их тем или иным образом очищать, преобразовывать и/или дополнять,
чтобы извлечь из них максимальную пользу.
До сих пор мы откладывали бóльшую часть этой работы, поскольку у нас были более насущные задачи, требующие решения. В главе 4 мы концентрировались на извлечении данных из файлов сложных форматов и сохранении их в каком-либо более удобном для нас формате. А в главе 6 наш приоритет состоял в тщательной
оценке качества наших данных, чтобы можно было принять обоснованное решение,
стоит ли вообще вкладывать усилия в их дополнение и анализ.
Но сейчас настало время закатать рукава и приступить к выполнению задачи, которую я рассматриваю как второй этап выпаса данных и обеспечения их качества:
подготовка имеющихся у нас данных для выполнения над ними требующихся нам
анализов. Наши данные находятся в требуемом нам табличноподобном формате, и
мы определили, что они имеют достаточно высокое качество, чтобы предоставить
некоторые полезные знания, — даже если они не точно такие, какими мы их изначально представляли.
Очевидно, что определить и рассмотреть каждую возможную проблему или метод,
связанный с очисткой, преобразованием и/или дополнением данных, невозможно.
Поэтому мы рассмотрим подход посредством проработки примеров, с которыми
мы уже повстречались ранее, для которых требуется выполнение одной или нескольких из этих задач. Например, мы рассмотрим различные способы, которые
могут нам потребоваться для преобразования информации о дате, используя наборы данных, с которыми мы работали в главах 2 и 4. Мы также рассмотрим способы
удаления бесполезных данных из файлов, содержащих как структурированные
данные, так и метаданные. Мы даже исследуем регулярные выражения, которые
262
|
Глава 7
предоставляют нам мощное средство для выбора определенных частей поля данных или для поиска определенных терминов или образцов независимо от используемого в них регистра и/или знаков препинания. В процессе работы мы также охватим большой круг средств и стратегий, которые могут вам потребоваться при
очистке и преобразовании большинства наборов данных. Как минимум, изложенные в этой главе подходы дадут вам полезную отправную точку в случае необходимости решить по-настоящему сложную или уникальную задачу.
Выбор подмножества данных системы Citi Bike
В разд. "Отправляемся в путь с данными Citi Bike" главы 2 мы использовали данные системы аренды велосипедов Citi Bike для тестирования некоторых из наших
только что изученных концептов Python, например циклов for…in и операторов
условия if … else. Ради удобства мы начали работу с выборочного набора данных, извлеченного из файла данных этой системы за сентябрь 2020 г.
(https://s3.amazonaws.com/tripdata/index.html).
Существует множество ситуаций, когда нам нужно разбить на части большие наборы данных для анализа. Например, мы не располагаем достаточными временны́ми
или вычислительными ресурсами для обработки всех данных за один раз или нас
вообще интересует только определенное подмножество из всего набора данных.
Если нам нужно выбрать только конкретное количество строк, это можно было бы
сделать при помощи цикла for…in, используя функцию range(), которую мы рассмотрели во врезке "Добавляем итераторы: функция range()" в главе 4. Но нам
также может потребоваться извлечь из всего набора только некоторые данные в
зависимости от их значений. В моем случае я выбрала все аренды велосипедов, начиная с 1 сентября 2020 г., но нам может потребоваться получить более специфичную информацию, например, отдельно количество аренд велосипедов по субботам
и воскресеньям, а также по рабочим дням и праздникам.
Давайте начнем с первой задачи, выбора из всего набора данных аренд велосипедов
в системе Citi Ride, начиная с 1 сентября 2020 г.1. Концептуально это довольно простая задача: нам просто нужны все строки из нашего набора данных, содержащие
аренду, начавшуюся в первый день сентября. Но, снова взглянув на наш набор
данных, станет ясно, что даже эта задача не такая и простая.
В частности, как можно увидеть на рис. 7.1, столбец starttime содержит не просто
дату в виде месяца, дня и года, но также часы, минуты и секунды (с точностью до
четырех знаков после запятой!).
Например, значение starttime в первой записи этого файла данных выглядит следующим образом:
2020-09-01 00:00:01.0430
1
Хотя формат данных файлов системы Citi Bike изменился в начале 2021 года, более ранние файлы
имеют тот же самый формат, который используется в этих примерах.
Очистка, преобразование и дополнение данных
|
263
Рис. 7.1. Первые несколько строк набора данных аренды велосипедов системы Citi Bike
Очевидно, что если мы хотим проанализировать только первый день аренд (или
только аренды в утренние часы пик, или только аренды по рабочим дням недели),
нам нужен способ эффективной фильтрации данных на основе только части информации, хранящейся в этом столбце. Но какие у нас имеются опции для решения
этой задачи? В следующих нескольких разделах мы рассмотрим каждую из этих
задач, последовательно выполняя поиск аренд в определенную дату, в определенный период времени или в определенный "тип" дня. В процессе мы изучим некоторые из инструментов Python для решения подобных задач, а также когда и почему
использовать каждый из этих инструментов.
Простое разбиение
Решить первую задачу — извлечь записи аренды велосипедов за период, начинающийся с 1 сентября 2020 г., — сравнительно легко, используя совместно некоторые
инструменты, с которыми мы уже работали в предыдущих примерах. Но первым
делом нам нужно осознать, что при считывании файлов CSV средствами Python
большинство данных считаются строковыми2. Это означает, что хотя для людей
вполне понятно, что значение 2020-09-01 00:00:01.0430 должно интерпретироваться как дата и время, для Python же оно выглядит просто как набор цифр и прочих символов.
Рассматривая поле starttime, вопрос выделения всех аренд, заключенных начиная с
1 сентября 2020 г., немного упрощается, поскольку часть данных, содержащая информацию о дате, всегда отделяется от информации о времени одним пробелом.
Это означает, что если мы сможем найти способ выделить только часть поля перед
этим пробелом, то мы сможем легко организовать оператор условия if … else, чтобы сравнить эту часть с нашей целевой строкой даты (в данном случае 2020-09-01) и
использовать это сравнение, чтобы оставить только требуемые нам записи (строки)
набора данных.
Эта задача решается довольно обыденным способом — используя встроенный метод split(). Мы уже использовали его на второстепенных ролях в предыдущих
2
И даже если бы это было не так, то мы всегда можем преобразовать их в строки.
264
|
Глава 7
примерах, когда нам нужно было разделить имена файлов или URL-адреса. В частности, с помощью этого метода в разд. "Глаголы ≈ функциям" главы 2 мы проиллюстрировали разницу между функциями и методами. Напомним, что этот метод позволяет указать один символ, по которому строку следует разделить на две части. В
результате исполнения этого метода создается список, содержащий оставшиеся
части строки в исходном порядке, но без символа, по которому было выполнено
разделение. Таким образом, результатом разделения строки 2020-09-01 00:00:01.0430
по символу пробела будет список ['2020-09-01', '00:00:01.0430'].
Чтобы продемонстрировать простоту и эффективность этого метода, модифицируем наш сценарий из разд. "Отправляемся в путь с данными Citi Bike" главы 2. В
листинге 7.1 я сократила комментарии к некоторым задачам, поскольку теперь эти
задачи должны быть вам знакомы, но полагаю, что будет не лишним предоставить
краткое описание назначения сценария в его начале.
Листинг 7.1. Сценарий citibike_september1_rides.py
# Цели: отфильтровать все аренды велосипедов системы Citi Bike за сентябрь
# 2020 года и сохранить в новый файл только аренды за 1 сентября 2020 года
#
#
#
#
#
#
#
Краткое описание программы:
1. Считываем файл данных: 202009-citibike-tripdata.csv
2. Создаем новый выходной файл и записываем в него строку заголовка
3. Для каждой строки входного файла разделяем значение starttime по символу пробела:
a. Если первый элемент получившегося списка равен "2020-09-01", записываем эту
строку в наш выходной файл
4. Закрываем выходной файл
# Импортируем библиотеку csv
import csv
# Открываем файл данных в режиме чтения
source_file = open("202009-citibike-tripdata.csv","r")
# Открываем выходной файл в режиме записи
output_file = open("2020-09-01-citibike-tripdata.csv","w")
# Передаем исходный файл source_file методу DictReader и сохраняем результат
# в переменной citibike_reader
citibike_reader = csv.DictReader(source_file)
# Создаем соответствующий метод DictWriter и указываем, что заголовок должен быть
# таким же, как и названия полей в citibike_reader
output_writer = csv.DictWriter(output_file, fieldnames=citibike_reader.fieldnames)
Очистка, преобразование и дополнение данных
|
265
# Записываем строку заголовка в выходной файл
output_writer.writeheader()
# Обрабатываем в цикле for...in список строк citibike_reader
for a_row in citibike_reader:
# Получаем значение столбца starttime
start_timestamp = a_row["starttime"]
# Разделяем это значение по символу пробела
timelist = start_timestamp.split(" ")
# Первым получившимся элементом с позицией 0 будет часть даты исходной строки
the_date = timelist[0]
# Если значение the_date совпадает с требуемой датой
if the_date == "2020-09-01":
# Записываем эту строку данных в наш выходной файл
output_writer.writerow(a_row)
# Закрываем выходной файл
output_file.close()
Довольно просто, не так ли? Конечно же, этот сценарий можно с легкостью модифицировать для выборки другой даты или даже нескольких дат. Например, оператор условия if можно модифицировать следующим образом:
if the_date == "2020-09-01" or the_date == "2020-09-02":
Хотя для выбора двух или трех конкретных дат будет вполне достаточно части or
этого оператора, код для большего количества дат при таком подходе будет запутанным. (Вспомните, например, наш неуклюжий оператор условия в листинге 6.12.)
Поэтому, чтобы отфильтровать требуемые нам даты, не создавая при этом чрезмерно сложного, неуклюжего и чреватого ошибками кода, нам лучше подойдет
другое средство: регулярные выражения.
Регулярные выражения:
супермощное средство сопоставления строк
Регулярные выражения (regular expressions, часто сокращается до regex) позволяют
выполнять быстрый и эффективный поиск подстрок в строках большего размера
или фрагментах текста. В большинстве случаев, если обнаруживается, что для решения задачи сопоставления или фильтрации строк в операторе условия необходи-
266
|
Глава 7
мо использовать большое количество and или or, то это будет первым признаком
необходимости использовать регулярное выражение.
Регулярные выражения используются в большинстве языков программирования.
Это лаконичные и мощные средства, но иногда чрезвычайно сложные в работе. Хотя одно регулярное выражение может выразить очень сложный шаблон поиска,
процесс разработки регулярных выражений, работающих ожидаемым образом, может быть чрезвычайно трудоемким и часто требующим большого количества проб
и ошибок, чтобы получить правильное выражение. Поскольку наша цель состоит в
том, чтобы обеспечить как эффективную, так и понятную работу по выпасу данных, здесь мы будем концентрироваться на коротких регулярных выражениях, предоставляющих уникальную функциональность, которую нельзя с легкостью получить иными способами. Хотя регулярные выражения незаменимы для некоторых
задач, они не являются универсальным средством для решения любых задач и
обычно лучше всего работают при использовании их совместно с другими методами.
Для начала используем регулярное выражение для решения задачи выборки аренд
велосипедов, осуществленных в типичные часы "утренних поездок на работу", которые для наших целей мы определим как период с 7 до 9 часов утра. Процесс создания любого регулярного выражения начинается с получения представления о
том, что мы хотим сопоставить, а что не хотим. В данном случае мы начнем с рассмотрения примера записи starttime, находящейся вне указанного периода времени
(следовательно, это не то, что мы хотим сопоставить):
2020-09-01 00:00:01.0430
А теперь рассмотрим запись starttime, входящую в требуемый период:
2020-09-01 00:08:17.5150
Следующий этап заключается в признании, что эту задачу можно решить, используя рассмотренный ранее метод разбиения строки. В частности, можно разбить
строку по символу двоеточия (:), получив следующий результат:
['2020-09-01 00', '08', '17.5150']
Затем извлечем из этого списка средний элемент и, используя составной условный
оператор (т. е. оператор if, содержащий два или больше условий), проверим, совпадает ли он со строками 07, 08 или 09.
Это определенно рабочий подход, но он несколько неудобный. В частности, его
реализация требует нескольких шагов, а оператор c тремя условиями трудно понимать. С другой стороны, условное выражение позволит нам сконцентрироваться на
этих значениях часа в один прием, при этом оставаясь довольно легким для понимания. Но прежде чем приступать к созданию самого регулярного выражения, выполним быстрый обзор словаря регулярных выражений языка Python.
Поскольку для описания шаблонов символов и строк регулярные выражения также
используют символы и строки, для упрощения описания искомой последовательности в системе регулярных выражений в Python применяются метасимволы и специальные последовательности символов. В табл. 7.1 приводится несколько наиболее
Очистка, преобразование и дополнение данных
|
267
полезных символов, взятых из более обширного списка на веб-сайте W3Schools
(https://w3schools.com/python/python_regex.asp).
Таблица 7.1. Наиболее употребляемые компоненты регулярных выражений
Выражение
Описание
[]
Набор символов
"\"
Указывает специальную последовательность символов
(может также применяться для экранирования специальных символов)
.
Любой символ (за исключением символа новой строки)
*
Отсутствие или несколько вхождений символа
+
Одно или несколько вхождений символа
{}
В точности указанное количество вхождений символа
|
Или, или
()
Выборка и группирование
\d
Возвращает строки, содержащие цифры 09
\D
Возвращает строки, НЕ содержащие цифры
\s
Возвращает строки, содержащие символ пробела
\S
Возвращает строки, НЕ содержащие символа пробела
\w
Возвращает строки, содержащие любые символы, используемые в словах
(буквы от a до Z, цифры от 0 до 9 и символ подчеркивания _)
\W
Возвращает строки, НЕ содержащие символов, используемых в словах
Как всегда в процессе создания письменных работ, регулярные выражения предоставляют несколько способов для описания требуемого нам шаблона. В большинстве случаев наша цель состоит в том, чтобы определить шаблон, который будет совпадать с требуемыми нам строками и при этом не будет допускать случайного совпадения с какими бы то ни было другими строками. Для нашей задачи "утреннего
часа пик" мы можем воспользоваться тем обстоятельством, что в столбце starttime
только цифры значения часов, и никакие другие значения, заключены в двоеточия.
Это означает, что этот шаблон "заключение в двоеточия" можно использовать как
часть нашего регулярного выражения с уверенностью, что мы случайно не выберем
какую-либо другую часть строки. Чтобы проверить работу этого подхода на практике, создадим несколько образцов регулярных выражений, работу которых мы
протестируем на настоящих и искусственно созданных данных. Соответствующий
код приводится в листинге 7.2.
268
|
Глава 7
Листинг 7.2. Сценарий regex_tests.py
# Цель этого сценария — проверить работу нескольких регулярных выражений
# с некоторыми тестовыми данными.
# Импортируем библиотеку для работы с регулярными выражениями
import re
# Преобразуем шаблоны регулярных выражений в объекты шаблонов, используя метод
re.compile()
bookend_regex = re.compile("\s0[7-9]:")
# Всегда пытаемся использовать описательные названия переменных
one_sided_regex = re.compile("0[7-9]:")
# Этот пример не должен найти совпадений
sample1 = "2020-09-01 00:00:01.0430"
# Этот пример должен найти совпадения
sample2 = "2020-09-01 09:04:23.7930"
# Этот пример не должен найти совпадений
sample3 = "2020-09-01 10:07:02.0510"
# Проверяем работу примеров на практике
print("bookend_regex:")
print(bookend_regex.search(sample1))
print(bookend_regex.search(sample2))
print(bookend_regex.search(sample3))
print("one_sided_regex:")
print(one_sided_regex.search(sample1))
print(one_sided_regex.search(sample2))
print(one_sided_regex.search(sample3))
Кроме данного примера, потренироваться в использовании регулярных выражений Python можно с помощью демонстрационного сценария на веб-сайте W3Schools
(https://w3schools.com/python/trypython.asp?filename=demo_regex).
Даже если регулярное выражение используется в сценарии только один раз, я
настоятельно рекомендую определять его в начале сценария и присваивать его переменной соответствующее название. Это наиболее простой и эффективный способ
для отслеживания его работы, особенно в случае использования нескольких выражений.
Очистка, преобразование и дополнение данных
|
269
Исполнение сценария в листинге 7.2 должно дать результат наподобие следующего:
bookend_regex:
None
<re.Match object; span=(10, 14), match=' 09:'>
None
one_sided_regex:
None
<re.Match object; span=(11, 14), match='09:'>
<re.Match object; span=(14, 17), match='07:'>
Как можно увидеть, регулярное выражение bookended, в котором указаны оба двое-
точия, правильно находит (и не находит) совпадения во всех трех случаях; а вот с
другой стороны, выражение one_sided неправильно находит совпадение в значении
seconds образца sample3. Именно по этой причине важно определять искомую строку
наиболее точно. В выводе объекта Match можно видеть, что он содержит информацию о совпадающей подстроке (например, match='07:'), а также о ее положении в
исходной строке (например, начиная с позиций указателя 1417 в строке).
Пока что все выглядит достаточно просто. Однако в случае изменения структуры
объекта дело может несколько усложниться. Например, что если мы захотим расширить требуемый временной период, чтобы он охватывал часы с 7 до 10 утра? В
его текущем состоянии регулярное выражение не справится с этой задачей, поскольку в нем указывается, что первым символом после двоеточия должен быть 0.
Его можно попробовать исправить, просто добавив к диапазону цифр цифры 1 и 0,
как показано ниже:
plus_ten = re.compile("\s[01][0789]:")
print("plus_ten")
print(plus_ten.search("2020-09-01 18:09:11.0980"))
Это даст следующий результат:
plus_ten
<re.Match object; span=(10, 14), match=' 18:'>
Как можно увидеть в выводе результатов, проблема заключается в том, что в наших
данных используется 24-часовый формат, в результате чего будет возвращаться
целый диапазон нежелательных совпадений. Это объясняется тем, что регулярные
выражения не "видят" числа таким образом, как их видят люди, для них это просто
последовательность символов. Вот поэтому число 18 возвращается как совпадение
— наше регулярное выражение допускает выбор любой строки, начинающейся с
цифры 0 или 1, за которой следует цифра 0, 7, 8 или 9. Тогда как мы, очевидно, создавали это выражение для выбора подстрок 07, 08, 09 и 10, наш код позволяет осуществить выбор и других подстрок.
Решением в данном случае будет использование символа конвейеризации
"или/или" (|), чтобы объединить отдельные регулярные выражения. Полученное
регулярное выражение будет выглядеть, как показано в листинге 7.3.
270
|
Глава 7
Листинг 7.3. Возвращение чисел от 7 по 10
seven_to_ten = re.compile("\s0[7.9]:|\s10:")
Испытайте это регулярное выражение сами с несколькими образцами данных, чтобы убедиться в том, что оно выбирает требуемые нам подстроки (и ничего больше).
Этим я и ограничу рассмотрение предмета регулярных выражений. Подобно методам веб-извлечение данных, которые рассматриваются в разд. "Извлечение вебданных: источник данных последней надежды" главы 5, каждое регулярное выражение (или реализуемое им решение) уникально. Но я надеюсь, что вы сможете
оценить предоставляемую ими возможность для сопоставления с образцом, реализация которого посредством одних составных условных операторов и основных
строковых функций была бы очень неуклюжей.
Создание дат
Одна из причин, по которой данные, подобные датам, удобно рассматривать как
строки, состоит в том (как можно было видеть в нашей работе с разными исходными форматами данных по уровню безработицы в главе 4), что их можно интерпретировать по-разному в разных источниках данных и даже разными библиотеками
Python. Тем не менее в некоторых задачах очень полезно преобразовывать подобные датам данные в фактические данные типа datetime. Например, выделить данные
по арендам велосипедов только в рабочие дни из всех данных системы Citi Bike
можно было бы, применив подход "грубой силы". Для этого нужно определить все
даты рабочих дней (например, посмотрев их в календаре), а затем создать гигантский список условий или регулярное выражение, чтобы найти требуемые совпадения. Для данных аренд за сентябрь 2020 года такое регулярное выражение может
выглядеть наподобие показанного в листинге 7.4.
Листинг 7.4. Регулярное выражение для выбора аренд в рабочие дни сентября 2020 г.
september2020_weekday = re.compile("-0[123489]-|-1[0145678]-|-2[1234589]-|-30-")
Это выражение, безусловно, рабочее, но его почти невозможно читать. Кроме этого, это все такой же, по сути, один гигантский составной условный оператор, несмотря на то, что будучи регулярным выражением, он реализован меньшим количеством символов. Кроме этого, это решение не очень хорошо поддается масштабированию. Например, чтобы расширить наш анализ на любой другой месяц, нам
снова пришлось бы определять требуемые даты вручную по календарю.
К счастью, в состав Python входит объект datetime, обладающий несколькими
встроенными методами, который может быть полезным для решения как раз такого
рода задач. Более того, его простой метод weekday() возвращает число от 0 до 6 (0
означает понедельник, а 6 — воскресенье, см. https://docs.python.org/3/library/
datetime.html#datetime.date.weekday), в зависимости от дня недели, на который
Очистка, преобразование и дополнение данных
|
271
выпадает определенная дата. Это означает, что если содержимое нашего столбца
starttime преобразовать в дату, то посредством этого метода можно быстро определить день недели, соответствующий любой дате. Подобный код приводится в листинге 7.5. Этот подход позволит нам использовать наш код с другими источниками
данных (например, с данными аренд для другого месяца или года), не требуя никаких изменений первоначального кода.
Листинг 7.5. Сценарий weekday_rides.py
# Цели: отфильтровать все аренды велосипедов системы Citi Bike за рабочие дни сентября
# 2020 года и сохранить в новый файл.
#
#
#
#
#
#
Краткое описание программы:
1. Считываем файл данных: 202009-citibike-tripdata.csv
2. Создаем новый выходной файл и записываем в него строку заголовка
3. Для каждой строки в файле создаем из строки starttime значение даты
a. Если дата выпадает на рабочий день, записываем строку в выходной файл
4. Закрываем выходной файл
# Импортируем библиотеку csvimport csv
# Импортируем datetime
from datetime import datetime
# Открываем файл данных в режиме чтения
source_file = open("202009-citibike-tripdata.csv","r")
# Открываем выходной файл в режиме записи
output_file = open("202009-citibike-weekday-tripdata.csv","w")
# Преобразовываем исходный файл в структуру DictReader; сохраняем результат
# в переменной citibike_reader
citibike_reader = csv.DictReader(source_file)
# Создаем соответствующую структуру DictWrited и задаем ее название полей
output_writer = csv.DictWriter(output_file, fieldnames=citibike_reader.fieldnames)
# Записываем строку заголовка в выходной файл
output_writer.writeheader()
# Обрабатываем в цикле for...in список строк citibike_reader
for a_row in citibike_reader:
# Преобразовываем значение столбца starttime в объект даты
the_date = datetime.strptime(a_row['starttime'], '%Y-%m-%d %H:%M:%S.%f')
# Если дата the_date — рабочий день
if the_date.weekday() <= 4:
272
|
Глава 7
# Записываем эту строку данных в наш выходной файл
output_writer.writerow(a_row)
# Закрываем выходной файл
output_file.close()
Как упоминалось в листинге 6.1, предоставление формата наших исходных данных
(https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior)
будет способствовать более быстрому и надежному исполнению нашего сценария.
Метод weekday() присваивает понедельнику позицию 0 (https://docs.python.org/3/
library/datetime.html#datetime.date.weekday), поэтому поиск любых значений до 4
включительно выберет все значения с понедельника по пятницу включительно.
В зависимости от используемого устройства исполнение сценария в листинге 7.5
может занять некоторое время. Например, на моем, не очень мощном устройстве,
исполнение занимает свыше 85 секунд. А вот сценарий для решения этой же задачи
с использованием регулярного выражения из листинга 7.4 исполняется всего лишь
за 45 секунд. Регулярное выражение можно также легко настроить, чтобы пропускать рабочие дни недели, которые являются выходными (например, День труда,
который празднуется в США и Канаде в первый понедельник сентября).
Так какой же из этих подходов лучше? Как обычно, это зависит от разных факторов. Наилучший рабочий подход для решения вашей конкретной задачи по первичной обработке, очистке или преобразованию данных будет зависеть от ваших конкретных требований и имеющихся ресурсов. Если для ответа на ваш вопрос потребуется исследовать закономерности поездок в рабочие дни в данных системы Citi
Bike за десятилетие, лучше использовать метод weekday(), чтобы не изменять код, в
случае работы с разными месяцами или годами. С другой стороны, если не требуется работать с большим количеством месяцев и вашим приоритетом является скорость исполнения (и абсолютная точность), то предпочтительным может стать подход с использованием регулярного выражения. Вы также можете обнаружить, что
работа с регулярными выражениями вызывает у вас желание рвать на себе волосы
или что необходимость использования нескольких этапов для получения идеальных результатов сводит вас с ума. Как рассматривается подробнее в главе 8, все эти
причины могут обосновывать выбор определенного подхода, однако этот выбор
непременно должен быть вашим собственным3.
Удаление хлама из файлов данных
В главе 4 нам пришлось несколько раз "очищать" наборы данных, которые в противном случае были бы непонятными или неудобными для работы. Например, в
процессе парсинга файла old-school-style.xls в листинге 4.6 мы столкнулись с парой
3
Исключением может быть работа в команде. В таком случае необходимо учитывать мнение каждого
ее члена.
Очистка, преобразование и дополнение данных
|
273
отдельных проблем. Первое — электронная таблица содержала как табличные данные, так и описательную информацию в заголовке. Эту информацию, несмотря на
ее, в принципе, полезность, придется переместить в другое место, чтобы иметь
возможность анализировать всю прочую информацию. Второе — формат .xls не
поддерживает "настоящие" даты, означая, что после первоначального преобразования данных из формата .xls в формат .csv вместо ожидаемых дат создавался набор
бессмысленных чисел. Изначально мы отложили решение этих проблем на потом,
но сейчас настало время заняться ими.
Касательно первой проблемы, я хочу подчеркнуть, что мы определенно не хотим
просто "выбросить" информацию, хранящуюся изначально вверху файла fredgraph.xls.
Как мы, я надеюсь, выяснили в результате нашей работы в главе 6, метаданные являются ценным ресурсом, который никогда не следует выбрасывать из основного
источника данных (https://loc.gov/programs/teachers/getting-started-with-primarysources). В таких случаях я предпочитаю разделить один файл на два. Метаданные
извлекаются из файла и сохраняются в отдельном текстовом файле, которому присваивается соответствующее название. А табличные данные сохраняются в файле
.csv, чтобы их было легко анализировать.
Соглашение прежде чем конфигурация
Даже наиболее простые примеры в этой книге требуют принятия большого
количества решений, например, при именовании переменных, файлов сценариев или вывода данных и т. п. Хотя концепт "соглашение прежде чем конфигурация" может не совсем соответствовать принципам языка Python
(https://en.wikipedia.org/wiki/Convention_over_configuration), я считаю, что
этот концепт может многое предложить в области сбора, создания и реорганизации файлов в процессе выпаса данных. В частности, применяя общее соглашение по именованию для нашего файла метаданных и файла табличных данных, мы можем избежать проблем как при создании этих файлов, так и при
поиске их в будущем. Вам необязательно точно следовать предлагаемому
здесь образцу, но в долгосрочной перспективе, выбрав последовательный метод именования этих файлов во всех своих проектах, вы сэкономите себе время и усилия, а также избежите напрасного беспокойства.
При просмотре нашего исходного файла .xls в программе электронной таблицы
можно достаточно легко увидеть, где заканчиваются метаданные и начинаются
табличные данные. Но вопрос заключается в том, как определить этот переход в
сценарии. Как часто бывает в случае очистки данных, наиболее эффективное решение этой задачи не всегда будет опрятным. Метаданные заканчиваются там, где начинаются табличные данные, — в строке, содержащей заголовки таблиц. Исследуя
первое значение каждой строки при обработке файла, можно прекратить запись в
файл метаданных и начать запись в файл данных .csv, как только мы обнаружим
первый заголовок столбца. Поскольку в данном наборе данных первым заголовком
274
|
Глава 7
столбца является значение observation_date, мы выполним этот переход, как только
обнаружим это значение в начале текущей строки.
Прежде чем приступать к работе, внимательно исследуйте исходный
файл, чтобы найти в нем местонахождение метаданных. Метаданные могут находиться как до, так и после табличных данных, особенно в случаях,
когда данные содержат оценочные значения или другие спецификаторы.
В листинге 7.6 приводится сценарий для создания наших двух специальных файлов
из одного исходного файла. (Если некоторые фрагменты кода этого сценария вызывают у вас вопросы, вы можете освежить свою память, исследовав сценарий в
листинге 4.6.)
Листинг 7.6. Сценарий xls_meta_parsing.py
# Преобразовываем табличные данные в файле .xls в формат csv и сохраняем их
# в соответствующем файле. Также отделяем метаданные и сохраняем их в отдельном файле.
# Используем для всего этого библиотеку xrld.
# Сначала установим библиотеку xlrd (https://pypi.org/project/xlrd/2.0.1/), используя
# установщик pip
# Импортируем библиотеку xlrd
import xlrd
# Импортируем библиотеку csv, чтобы создать выходной файл
import csv
# Передаем имя файла в качестве аргумента методу open_workbook() библиотеки xlrd
# Сохраняем результат в переменной source_workbook
source_workbook = xlrd.open_workbook("fredgraph.xls")
# Открываем и именуем простой файл для хранения метаданных
source_workbook_metadata = open("fredgraph_metadata.txt","w")
# Рабочая книга .xls может содержать несколько листов
for sheet_name in source_workbook.sheet_names():
# Создаем переменную, указывающую на текущий лист, передавая текущее значение
# переменной sheet_name метода sheet_by_name
current_sheet = source_workbook.sheet_by_name(sheet_name)
# Создаем выходной файл xls_"+sheet_name+".csv для хранения текущего листа
output_file = open("xls_"+sheet_name+".csv","w")
# Используем метод для записи writer() библиотеки csv для записи строк данных
# в выходной файл output_file
output_writer = csv.writer(output_file)
Очистка, преобразование и дополнение данных
|
275
# Создаем булеву переменную для определения достижения табличных данных
is_table_data = False
# Обрабатываем в цикле for каждую строку в листе
for row_num, row in enumerate(current_sheet.get_rows()):
# Извлекаем значение первого столбца текущей строки
first_entry = current_sheet.row_values(row_num)[0]
# Если строка заголовков таблицы данных
if first_entry == 'observation_date':
# Присваиваем переменной флага is_table_data значение True
is_table_data = True
# Если значение переменной is_table_data равно True
if is_table_data:
# Записываем эту строку в наш выходной файлn данных
output_writer.writerow(current_sheet.row_values(row_num))
# В противном случае эта строка должна быть метаданными
else:
# Для опрятного форматирования файла метаданных обрабатываем отдельные ячейки
# каждой строки метаданных
for item in current_sheet.row(row_num):
# Записываем значение ячейки
source_workbook_metadata.write(item.value)
# Отделяем его от значения следующей ячейки символом табуляции
source_workbook_metadata.write('\t')
# В конце каждой строки метаданных добавляем символ новой строки
source_workbook_metadata.write('\n')
# На всякий случай закроем наши выходные файлы
output_file.close()
source_workbook_metadata.close()
Здесь мы создаем только один файл метаданных, но при необходимости мы могли бы с легкостью поместить этот процесс в цикл for и создавать отдельный файл
метаданных для каждого рабочего листа.
Данный тип булевой (True/False — истина/ложь) переменной часто называется
логической, или флаговой переменной. Идея заключается в том, чтобы установить
эту переменную вне цикла, а затем менять ее значение на обратное, при определен-
276
|
Глава 7
ном событии. Таким образом мы избегаем двойной обработки в цикле всех данных.
В данном случае мы используем эту переменную, чтобы определить, когда прекратить запись в файл метаданных и начать запись в файл данных.
Прежде чем переходить к работе с (все еще непонятными) датами в этом файле, я
хочу обратить ваше внимание на новый метод, представленный в листинге 7.6: использование флаговой переменной. Этот термин обычно означает любую булеву
переменную, используемую для отслеживания происхождения определенного события или выполнения условия, особенно внутри цикла. Например, в листинге 7.6
мы используем флаговую переменную is_table_data, чтобы определить нахождение
строки данных, обозначающей начало табличных данных. Поскольку в нашем цикле for…in эта строка данных, по сути, "забывается" после считывания новой строки,
эту переменную нужно создать до входа в цикл. Таким образом переменная
is_table_data оказывается вне области видимости цикла. (Предмет области видимости рассматривается более подробно в главе 8.)
Декодирование дат Excel
Мы больше не можем избегать вопроса дат в таблицах Excel. Хотя, я надеюсь, вам
не придется часто сталкиваться с этой ситуаций, я решила рассмотреть ее здесь для
полноты материала, а также по причине того, что она иллюстрирует несколько различных путей развития кода при расширении его даже, казалось бы, незначительными возможностями. Обычно в таких случаях код становится более сложным и
менее читаемым. Например, в сценарии в листинге 7.7 нам нужно проверить, содержит ли переменная число или нет. Хотите верьте, хотите нет, но для этого нам
потребуется отдельная библиотека, которая соответственно называется numbers.
Хотя это довольно просто, в листинге 7.7 мы увидим, как для преобразования этих
значений дат требуется изменять наш подход для записи табличных данных в выходной файл.
Листинг 7.7. Сценарий xls_meta_and_date_parsing.py
# Преобразовываем табличные данные в файле .xls в формат csv и сохраняем их
# в соответствующем файле. Также отделяем метаданные и сохраняем их в отдельном файле.
# Для работы с данными дат используем библиотеку xrld
# Сначала установим библиотеку xlrd (https://pypi.org/project/xlrd/2.0.1/), используя
# установщик pip
# Импортируем библиотеку xlrd
import xlrd
# Импортируем библиотеку csv
import csv
Очистка, преобразование и дополнение данных
|
# Нужен для проверки, является ли данное значение каким-либо типом числа
from numbers import Number
# Требуется для парсинга и форматирования наших дат Excel
from datetime import datetime
# Передаем имя файла в качестве аргумента методу open_workbook() библиотеки xlrd.
# Сохраняем результат в переменной source_workbook
source_workbook = xlrd.open_workbook("fredgraph.xls")
# Открываем и именуем простой файл для хранения метаданных
source_workbook_metadata = open("fredgraph_metadata.txt","w")
# an `.xls` workbook can have multiple sheets
for sheet_name in source_workbook.sheet_names():
# Создаем переменную, указывающую на текущий лист, передавая текущее значение
# переменной sheet_name метода sheet_by_name
current_sheet = source_workbook.sheet_by_name(sheet_name)
# Создаем выходной файл xls_"+sheet_name+".csv для хранения текущего листа
output_file = open("xls_"+sheet_name+"_dates.csv","w")
# Используем метод для записи writer() библиотеки csv для записи строк данных
# в выходной файл output_file
output_writer = csv.writer(output_file)
# Создаем булеву переменную для определения достижения табличных данных
is_table_data = False
# Обрабатываем в цикле for каждую строку в листе
for row_num, row in enumerate(current_sheet.get_rows()):
# Извлекаем значение первого столбца текущей строки
first_entry = current_sheet.row_values(row_num)[0]
# Если строка заголовков таблицы данных
if first_entry == 'observation_date':
# Присваиваем переменной флага is_table_data значение True
is_table_data = True
# Если значение переменной is_table_data равно True
if is_table_data:
# Извлекает значения табличных данных в отдельные переменные
the_date_num = current_sheet.row_values(row_num)[0]
U6_value = current_sheet.row_values(row_num)[1]
277
278
|
Глава 7
# Создаем новый объект строки, содержащий все значения
new_row = [the_date_num, U6_value]
# Если the_date_num содержит число, тогда текущая строка не является строкой
# заголовка. Нужно выполнить преобразование даты
if isinstance(the_date_num, Number):
# Создаем объект Python datetime, используя метод xldate_as_datetime()
# библиотеки xlrd
the_date_num = xlrd.xldate.xldate_as_datetime(
the_date_num, source_workbook.datemode)
# Записываем переформатированную дату, заменяя им первое значение в новой
# строке
new_row[0] = the_date_num.strftime('%m/%d/%Y')
# Записываем эту новую строку в наш выходной файл данных
output_writer.writerow(new_row)
# В противном случае эта строка должна быть метаданными
else:
# Для опрятного форматирования файла метаданных обрабатываем отдельные ячейки
# каждой строки метаданных
for item in current_sheet.row(row_num):
# Записываем значение ячейки
source_workbook_metadata.write(item.value)
# Отделяем его от значения следующей ячейки символом табуляции
source_workbook_metadata.write('\t')
# В конце каждой строки метаданных добавляем символ новой строки
source_workbook_metadata.write('\n')
# На всякий случай закроем наши выходные файлы
output_file.close()
source_workbook_metadata.close()
При
преобразовании значений дат формата xls, используя метод
xldate_as_datetime() библиотеки xlrd, для правильного создания объекта Python
datetime требуется наличие как значения числа, так и значения datemode
(https://xlrd.readthedocs.io/en/latest/api.html#xlrd.book.Book.datemode) рабочей
книги4.
4
В системах Mac и PC используются разные базовые даты, поскольку… на это есть важные причины.
Очистка, преобразование и дополнение данных
|
279
Здесь я решила записывать даты в файл табличных данных в формате ММ/ДД/ГГГГ,
используя соответствующий метод strftime(), но вы можете использовать любой
другой предпочитаемый вами формат.
Как мы увидели, библиотека xlrd делает процесс преобразования дат из странного
формата Excel в нечто более понимаемое сравнительно просто. Но код в листинге
7.7 демонстрирует, как особенности обработки конкретного набора данных могут
быстро повысить сложность (особенно в виде дополнительных вложенных условных операторов if) изначально очень простой программы. Это лишь одна из причин, по которой мы посвятим главу 8 исследованию стратегий и методов для эффективной и рациональной оптимизации нашего кода: нам нужно быть уверенными
в том, что он делает все, что нам требуется, но также достаточно удобочитаем и
пригоден для повторного использования в будущем.
Создание настоящих данных CSV
из данных фиксированной ширины
У нас частично получилось преобразование наших данных в листинге 4.7, где мы
преобразовывали исходные данные фиксированной ширины в данные, разделенные
запятыми (формата csv). Технически нам удалось записать в выходной файл значения, разделенные запятыми, однако этот результат был довольно разочаровывающим: в данных сохранились многие артефакты из исходного файла, которые с легкостью могли бы препятствовать нашим усилиям по анализу данных в будущем.
К счастью, данная проблема, в частности наличие ведущих или концевых непечатаемых символов, хорошо известна, поскольку технологии, которые обычно создают ее, существуют уже длительное время. Поэтому эта проблема решается довольно легко — с помощью встроенной функции Python strip(), использование которой
демонстрируется в листинге 7.8.
Листинг 7.8. Сценарий fixed_width_strip_parsing.py
#
#
#
#
#
Пример чтения данных из файла данных фиксированной ширины средствами Python.
Исходный файл данных для этого примера предоставлен управлением NOAA и доступен
здесь: https://www1.ncdc.noaa.gov/pub/data/ghcn/daily/ghcnd-stations.txt
Метаданные для файла доступны здесь:
https://www1.ncdc.noaa.gov/pub/data/ghcn/daily/readme.txt
# Импортируем библиотеку csv, чтобы создать выходной файл
import csv
filename = "ghcnd-stations"
# Для чтения обычного текстового файла не требуются никакие специальные библиотеке,
# поэтому просто открываем его в обычном режиме для чтения "r".
source_file = open(filename+".txt", "r")
280
|
Глава 7
# Встроенный метод readlines() делает то, что говорит его название:
# считывает содержимое входного текстового файла и преобразовывает его в список строк
stations_list = source_file.readlines()
# Создаем выходной файл для преобразованных данных
output_file = open(filename+".csv","w")
# Используем метод для записи writer() библиотеки csv для записи строк данных
# в выходной файл output_file
output_writer = csv.writer(output_file)
# Создаем список заголовков
headers = ["ID","LATITUDE","LONGITUDE","ELEVATION","STATE","NAME","GSN_FLAG",
"HCNCRN_FLAG","WMO_ID"]
# Записываем заголовки в выходной файл
output_writer.writerow(headers)
# Обрабатываем каждую строку в нашем файле (множественные листы невозможны)
for line in stations_list:
# Создаем пустой список, в который будем добавлять каждый набор символов,
# составляющий "столбец" данных
new_row = []
# ID: позиции 1-11
new_row.append((line[0:11]).strip())
# ШИРОТА: позиции 13-20
new_row.append((line[12:20]).strip())
# ДОЛГОТА: позиции 22-30
new_row.append((line[21:30]).strip())
# ВОЗВЫШЕНИЕ: позиции 32-37
new_row.append((line[31:37]).strip())
# ШТАТ: позиции 39-40
new_row.append((line[38:40]).strip())
# НАЗВАНИЕ: позиции 42-71
new_row.append((line[41:71]).strip())
# GSN_FLAG: positions 73-75
new_row.append((line[72:75]).strip())
# HCNCRN_FLAG: positions 77-79
new_row.append((line[76:79]).strip())
Очистка, преобразование и дополнение данных
|
281
# WMO_ID: positions 81-85
new_row.append((line[80:85]).strip())
# Теперь осталось только записать new_row в выходной файл, используя метод
# writerow()
output_writer.writerow(new_row)
# На всякий случай закроем только что созданный выходной файл .csv
output_file.close()
Если сравнить этот код с кодом в листинге 4.7, мы увидим, что они идентичны,
за исключением метода strip(), применяемого в текущем сценарии к каждой строке перед тем, как она добавляется к строке данных.
Как мы видим, модифицировать исходный код для решения проблемы форматирования или "очистки" данных не всегда сложно, но получившийся в результате сценарий все же не совсем элегантный. Удалить пробелы из выходных данных при
помощи метода strip() просто, но в процессе нам нужно было добавить в код
большое количество скобок, в результате чего наш код получился намного менее
удобочитаемым, чем нам бы хотелось.
Это иллюстрирует еще одно сходство создания хорошего, качественного кода Python с типичным процессом создания литературных сочинений. Наши усилия в главе 4, где мы решаем выкоуровневую задачу преобразования исходного формата
данных, по крайней мере, в требуемую нам табличную структуру, можно рассматривать как своего рода наметку конечной программы. Таким образом, это предоставляет нам возможность возвратиться к ней позже и откорректировать эту программу, добавив к ней возможности, позволяющие более точно обрабатывать текущий набор данных.
В главе 8 мы перейдем к следующему этапу процесса редактирования. В частности,
мы займемся дальнейшим усовершенствованием этих программ (которые уже выполняют все, что от них требуется), чтобы они были более краткими и понимаемыми, точно так же, как мы могли бы, отредактировав, усовершенствовать свою письменную работу. Такой итеративный (пошаговый) подход к программированию означает, что в конечном итоге мы получим лучший и более полезный код, а также
разобъем решение больших и сложных задач программирования в последовательность менее трудных задач. Не менее важно и то, что независимо от текущего этапа
процесса, у нас есть работающая программа, к которой мы можем прибегнуть в
случае необходимости. Такой пошаговый подход особенно полезен при работе с
более сложными задачами по очистке данных, подобных той, которую мы рассмотрим следующей: исправление непреднамеренных разных вариантов написания одного и того же слова.
282
|
Глава 7
Исправление разнообразности написаний
В главе 6 мы применили метод "отпечатков пальцев" для решения проблемы возможного наличия разных вариантов написаний названий компаний в данных программы PPP. Это распространенная проблема во всех наборах данных, полагающихся на ручной ввод данных. Код в листинге 6.11 дал только приблизительную
оценку действительно однозначных записей в столбце OriginatingLender, подсчитав,
сколько из этих записей имели однозначный "отпечаток пальца". Мы обнаружили,
что наш набор данных содержит 4 337 однозначных названий банков, но только 4
242 однозначных "отпечатков пальцев". Это указывает на то, что около 95 банков в
действительности могут быть одними и теми же, но использовать разные варианты
названий, поскольку их названия создали одинаковые "отпечатки пальцев".
Поскольку эти 95 возможных разных написаний могли затронуть тысячи строк
данных, нам требуется какой-либо способ для преобразования нашего набора данных таким образом, чтобы мы могли с уверенностью агрегировать (объединить) его
по кредитодателю. В то же самое время мы также хотим избежать избыточного
корректирования, сгруппировав вместе записи, которые в действительности не относятся друг к другу.
Это тот случай, для которого полезно преобразование данных: мы не хотим рисковать потерей каких бы то ни было исходных данных (сохранение их необходимо
для проверки достоверности и выборочной проверки данных), но нам также необходимо преобразовать их, чтобы иметь возможность анализировать их в будущем.
Вследствие большого размера нашего набора данных, группирование и фильтрация
его данных для удовлетворения наших требований, скорее всего, займут много
времени. Поэтому нам нужно сохранить результаты этой работы, фактически добавив в набор данных новые столбцы. Это позволит нам сохранить как наши исходные данные, так и полезные результаты процесса преобразования, в одном файле.
К счастью, у нас есть несколько готовых библиотек, которые значительно упростят этот процесс. Поскольку мы уже знаем, как агрегировать названия, используя
процесс "отпечатков пальцев" из листинга 6.11, более сложной задачей будет определить, когда банки с одинаковыми "отпечатками пальцев" в действительности
следует рассматривать как разные организации. Рассмотрим для примера вывод
из листинга 6.7, который для удобства воспроизводится в листинге 7.9. Можно
увидеть, что лишь небольшое количество полей содержит информацию о первоначальном кредитодателе (originating lender). Поэтому наилучшим способом определить, являются ли два банка со всеми одинаковыми словами в их названиях
(и поэтому имеющие одинаковые "отпечатки пальцев", например First Bank Texas
и Texas First Bank) одним и тем же банком, можно будет по их значению
OriginatingLenderLocationID.
Листинг 7.9. Таблица выборочных данных в транспонированном виде
LoanNumber
DateApproved
SBAOfficeCode
9547507704
05/01/2020
464
Очистка, преобразование и дополнение данных
ProcessingMethod
BorrowerName
BorrowerAddress
BorrowerCity
BorrowerState
BorrowerZip
LoanStatusDate
LoanStatus
Term
SBAGuarantyPercentage
InitialApprovalAmount
CurrentApprovalAmount
UndisbursedAmount
FranchiseName
ServicingLenderLocationID
ServicingLenderName
ServicingLenderAddress
ServicingLenderCity
ServicingLenderState
ServicingLenderZip
RuralUrbanIndicator
HubzoneIndicator
LMIIndicator
BusinessAgeDescription
ProjectCity
ProjectCountyName
ProjectState
ProjectZip
CD
JobsReported
NAICSCode
RaceEthnicity
UTILITIES_PROCEED
PAYROLL_PROCEED
MORTGAGE_INTEREST_PROCEED
RENT_PROCEED
REFINANCE_EIDL_PROCEED
HEALTH_CARE_PROCEED
DEBT_INTEREST_PROCEED
BusinessType
OriginatingLenderLocationID
OriginatingLender
OriginatingLenderCity
OriginatingLenderState
Gender
Veteran
NonProfit
PPP
SUMTER COATINGS, INC.
2410 Highway 15 South
Sumter
<NA>
29150-9662
12/18/2020
Paid in Full
24
100
769358.78
769358.78
0
<NA>
19248
Synovus Bank
1148 Broadway
COLUMBUS
GA
31901-2429
U
N
<NA>
Existing or more than 2 years old
Sumter
SUMTER
SC
29150-9662
SC-05
62
325510
Unanswered
<NA>
769358.78
<NA>
<NA>
<NA>
<NA>
<NA>
Corporation
19248
Synovus Bank
COLUMBUS
GA
Unanswered
Unanswered
<NA>
|
283
284
|
Глава 7
Прежде чем идти дальше, нам нужно разобраться, что в действительности означает
значение OriginatingLenderLocationID. Поиск в Интернете по фразе "originating lender
location id" первым результатом выдает еще один документ на веб-сайте Управления по делам малого бизнеса (https://sba.gov/sites/default/files/articles/
ETran_Origination_01_2014.pdf). Поиск в этом PDF-документе по слову "location"
приводит нас на страницу, показанную на рис. 7.2, где видно, что значение
"Location ID" обозначает главное отделение данного банка и должно быть одинаковым для разных отделений одного и того же банка.
Рис. 7.2. Информация о значении "Location ID" кредитодателя
Имея эту дополнительную информацию, мы можем создать свою версию данных
по кредитам по программе PPP, которая будет содержать новый столбец OriginatingLenderFingerprint, значением которого будут значения "отпечатка пальцев" OriginatingLender и OriginatingLenderLocationID. Соответствующий код приводится в листинге 7.10. Позже мы сможем использовать это значение для того, чтобы быстро
агрегировать наши данные по первоначальному кредитодателю, при этом будучи
(достаточно) уверенными в том, что мы ни пропускаем совпадающих записей по
причине орфографических ошибок в названии, ни рассматриваем два отдельных
банка как один и тот же.
Листинг 7.10. Сценарий ppp_add_fingerprints.py
# Простой сценарий для добавления столбца "отпечатков пальцев" к нашим данным
# по кредитам, который поможет нам подтвердить и/или исправить возможные
# орфографические ошибки или разные варианты написаний, например, названий банков
# Импортируем библиотеку csv
import csv
Очистка, преобразование и дополнение данных
|
285
# Импортируем библиотеку fingerprints
import fingerprints
# Считываем последнюю выборку данных в переменную
ppp_data = open('public_150k_plus_recent.csv','r')
# Выполняем предварительную обработку данных при помощи функции DictReader
ppp_data_reader = csv.DictReader(ppp_data)
# Создаем выходной файл для записи модифицированного набора данных
augmented_ppp_data = open('public_150k_plus_fingerprints.csv','w')
# Создаем объект для записи данных writer, чтобы можно было выводить сразу всю строку
augmented_data_writer = csv.writer(augmented_ppp_data)
# Поскольку добавляется столбец, нужно также создать новую строку заголовка
header_row = []
# Для заголовка каждого столбца
for item in ppp_data_reader.fieldnames:
# Добавляем существующий заголовок столбца
header_row.append(item)
# Если столбец OriginatingLender
if item == 'OriginatingLender':
# Нужно добавить новый столбец
header_row.append('OriginatingLenderFingerprint')
# Теперь можно записать расширенную строку заголовка в выходной файл
augmented_data_writer.writerow(header_row)
# Обрабатываем в цикле каждую строку данных
for row in ppp_data_reader:
# Создаем пустой лист для хранения новой строки данных
new_row = []
# Обрабатываем в цикле for каждый столбец данных исходного набора данных
for column_name in ppp_data_reader.fieldnames:
# Сначала добавляем значение этой строки для данного столбца
new_row.append(row[column_name])
# Если название столбца OriginatingLended, добавляем наше новое значение
# fingerprint
if column_name == 'OriginatingLender':
286
|
Глава 7
# Наше значение fingerprint будет состоять из сгенерированного
# "отпечатка пальцев" ПЛЮС значения столбца OriginatingLenderLocationIS
the_fingerprint = fingerprints.generate(row[column_name]) + \
" " + row['OriginatingLenderLocationID']
# Добавляем составное значение "отпечатка пальцев" к нашей строке
new_row.append(the_fingerprint)
# Завершив составление строки, записываем ее в выходной файл
augmented_data_writer.writerow(new_row)
# Закрываем оба файла
augmented_ppp_data.close()
ppp_data.close()
Хотя это может выглядеть излишним, первый цикл for используется только для
создания нашей строки заголовков. Как всегда, мы хотим избежать внедрения орфографических ошибок всегда и всюду, где это возможно, поэтому в данном случае
дополнительный цикл оправдывает себя (это намного лучше, чем вводить значения
этого списка вручную).
Поскольку мы добавляем столбец данных, нам нужно поэлементно создавать
новую строку данных в виде списка, точно так же, как мы создали заголовочную
строку.
Структура получившегося файла будет такой же, как и исходного файла, за исключением добавления в него нового столбца OriginatingLenderFingerprint между столбцами OriginatingLender и OriginatingLenderCity исходного файла. Соответствующий
результат показан в листинге 7.11.
Листинг 7.11. Данные программы PPP с добавленным столбцом "отпечатков пальцев"
LoanNumber
DateApproved
SBAOfficeCode
ProcessingMethod
BorrowerName
BorrowerAddress
BorrowerCity
BorrowerState
BorrowerZip
LoanStatusDate
LoanStatus
Term
SBAGuarantyPercentage
InitialApprovalAmount
9547507704
05/01/2020
464
PPP
SUMTER COATINGS, INC.
2410 Highway 15 South
Sumter
NaN
29150-9662
12/18/2020
Paid in Full
24
100
769358.78
Очистка, преобразование и дополнение данных
CurrentApprovalAmount
UndisbursedAmount
FranchiseName
ServicingLenderLocationID
ServicingLenderName
ServicingLenderAddress
ServicingLenderCity
ServicingLenderState
ServicingLenderZip
RuralUrbanIndicator
HubzoneIndicator
LMIIndicator
BusinessAgeDescription
ProjectCity
ProjectCountyName
ProjectState
ProjectZip
CD
JobsReported
NAICSCode
RaceEthnicity
UTILITIES_PROCEED
PAYROLL_PROCEED
MORTGAGE_INTEREST_PROCEED
RENT_PROCEED
REFINANCE_EIDL_PROCEED
HEALTH_CARE_PROCEED
DEBT_INTEREST_PROCEED
BusinessType
OriginatingLenderLocationID
OriginatingLender
OriginatingLenderFingerprint
OriginatingLenderCity
OriginatingLenderState
Gender
Veteran
NonProfit
|
287
769358.78
0.0
NaN
19248
Synovus Bank
1148 Broadway
COLUMBUS
GA
31901-2429
U
N
NaN
Existing or more than 2 years old
Sumter
SUMTER
SC
29150-9662
SC-05
62.0
325510.0
Unanswered
NaN
769358.78
NaN
NaN
мNaN
мNaN
NaN
Corporation
19248
Synovus Bank
bank synovus 19248
COLUMBUS
GA
Unanswered
Unanswered
NaN
Кроме того, что это преобразование поможет нам с легкостью агрегировать наши
данные по конкретному первоначальному кредитодателю (originating lender), мы
также сможем продублировать это и с обслуживающим кредитодателем (servicing
lender). Можно даже написать сценарий, сравнивающий значения этих двух полученных "отпечатков пальцев", чтобы создать столбец (флаг), указывающий, кредит
выдал и обслуживает один банк или разные.
288
|
Глава 7
Тернистый путь к "простым" решениям
Я надеюсь, что у вас не возникло никаких трудностей с выполнением сценария в
листинге 7.10. Но я хочу уведомить вас, что это было не первое решение, которое я
попыталась применить. Более того, это было даже не второе и не третье решение.
В действительности я провела около дюжины часов, думая, кодируя, обрабатывая
данные и терпя неудачу, прежде чем я, наконец, осознала, что мой конечный подход к решению этой задачи был наиболее быстрым, простым и эффективным способом найти баланс между тем, чтобы кредиты одного банка были сгруппированы
вместе и при этом случайно не объединить две разные организации.
Я предоставлю здесь описание этого процесса, поскольку (как я надеюсь, становится понятным) выпас данных (и программирование в целом) заключается не столько
в написании кода, сколько в логическом мышлении и решении проблем. Это означает, что обдумывание поставленной задачи, пробование разных решений и, возможно, самое главное, готовность изменить курс, даже когда понимаешь, что это
выбрасывание "на ветер" большого объема работы, являются намного более важными факторами в выпасе данных, чем способность написать больше чем две строки кода Python по памяти5. Поэтому, с целью проиллюстрировать, что влечет за собой лишь один из этих типов усилий по решению проблем, я предоставлю (сравнительно) краткий обзор разных предпринятых мною подходов, прежде чем я
остановилась на решении, изложенном в разд. "Исправление разнообразности написаний" этой главы.
Я начала с минимальной адаптации сценария из листинга 6.11 для создания новой
строки, содержащей только "отпечатки пальцев", и нового файла CSV, в который
добавлялась эта строка. Но я быстро осознала, что существует большая вероятность, что некоторые банки со схожими названиями могут иметь одинаковые "отпечатки пальцев". Поэтому я написала сценарий, который выполнял следующее:
1. Создавал список уникальных "отпечатков пальцев".
2. Для каждого уникального "отпечатка пальцев" создавал новый список (фактически, структуру библиотеки pandas DataFrame) из всех уникальных значений OriginatingLenderLocationID.
3. При наличии нескольких разных значений OriginatingLenderLocationID столбец
"отпечатков пальцев" обновлялся, чтобы включать в него значение OriginatingLenderLocationID, подобно тому, как мы это делали для всех записей в сценарии
в листинге 7.10.
Но даже создание этого сценария было намного более сложным, чем можно было
бы подумать по только что изложенному общему обзору. Конечно, первый шаг был
легким — мы уже практически его осуществили. Но при попытке обработки этого
нового файла средствами библиотеки pandas оказалось, что мой жалкий маленький
5
Поверьте мне, большинство профессиональных программистов каждые пять минут ищут в Интернете информацию по разным аспектам, а не хранят информацию в памяти.
Очистка, преобразование и дополнение данных
|
289
планшет Chromebook не имел достаточного объема памяти, поэтому пришлось перенести мою работу на платформу Google Colab. Здесь у меня было больше памяти
для работы, но при каждом отвлечении от работы на более чем несколько минут
мне приходилось выполнять повторную аутентификацию и снова загружать данные
с моего файла Google Drive, что каждый раз занимало дополнительное время. Кроме этого, хотя я была довольно уверена в том, что я разобралась с тем, как правильно обновлять значения в структуре DataFrame, были проблемы с поиском нового
"отпечатка пальцев", в наличии которого я была полностью уверена: иногда я находила совпадения, а иногда получала пустую структуру DataFrame! Вдобавок, исполнение шага 3 занимало три или больше минут, так что можно представить, сколько
часов (и разочарования) я потратила на то, чтобы убедиться в том, что мой код действительно работает должным образом.
Конечно же, когда я создала (и проверила) код для этого трехшагового решения, я
осознала, что полученный результат не очень-то и отличался от начального. Более
того, он был немного менее удовлетворительным, поскольку новый формат моего
нового столбца OriginatingLenderFingerprint был непоследовательным: в одни ячейки этого столбца значение OriginatingLenderLocationID добавлялось, а в другие нет.
Но поскольку фактическое значение "отпечатка пальцев" не имело значения (только чтобы его можно было использовать для точного агрегирования и различения
банков), зачем я усложняла себе задачу, добавляя значение LocationID только к тем
из них, для которых было несколько записей? Нельзя ли было просто добавлять
значение LocationID к ним всем?
Ну, конечно же, только на этом этапе я удосужилась прочитать документацию, показанную на рис. 7.2, которая подтвердила, что добавление значения LocationID не
должно нарушить целостности "отпечатков пальцев", которые должны быть одинаковыми6. Вот так я и прошла весь круг: вместо того, чтобы присваивать потенциально совпадающие "отпечатки пальцев", а затем пытаться устранить проблемные
значения посредством неуклюжего и трудоемкого поиска, наилучшим решением
было сделать значение OriginatingLenderLocationID частью нового столбца "отпечатков пальцев" с самого начала.
Я потратила множество часов на то, чтобы "исправить" исходные "отпечатки пальцев", в процессе преодолевая ограниченные возможности своего устройства, капризы платформы Google Colab и утомительность внесения небольших модификаций в
сценарий с последующим многоминутным ожиданием завершения его исполнения.
Поэтому я не буду притворяться, что я не ощутила некоего разочарования, осознав,
что наилучшее решение в действительности состояло лишь в незначительной корректировке моего исходного сценария (хотя и не того, с которого я начала работу).
Но если выделить что-то одно, чему я научилась после всех этих лет выпаса данных, так это то, что умение прекратить дальнейшую работу и начать ее сначала яв6
Поначалу я беспокоилась, что столбец OriginatingLenderLocationID может обозначать, например,
отдельные банковские отделения.
290
|
Глава 7
ляется одним из важнейших навыков. Иногда нужно просто найти в себе силы и
отказаться от набора данных, даже если вы потратили на его исследование, оценку
и очистку много времени. Так же как иногда нужно отказаться от определенного
программного подхода, даже если вы потратили многие часы на чтение документации и экспериментирование с новыми методами, чтобы получить требуемые результаты. Поскольку в итоге цель заключается не в том, чтобы использовать какойлибо конкретный набор данных или библиотеку или программный метод. Цель состоит в использовании данных, чтобы понять что-либо об окружающем нас мире.
И если вы можете сконцентрироваться именно на этом, то будет намного легче отказаться от какого-либо неудачного аспекта разрабатываемого решения, когда в
этом возникнет необходимость.
Вам также, вероятно, будет легче принять этот процесс — будь то отказ от набора
данных или сценария решения, на разработку которого вы потратили много времени — когда вы начнете испытывать на собственном опыте, что, даже отказавшись
от чего-либо, вы научились чему-то ценному. Например, прежде чем отклониться
на "исправление" моих первоначальных, исключительно текстовых, "отпечатков
пальцев", я, по сути, не знала, как обновлять значения в структуре библиотеки pandas DataFrame. Теперь же я знаю, как делать это (я действительно знаю). Я также узнала кое-что о сильных сторонах платформы Google Colab и ее особенностях, а
также освежила знания о некоторых ключевых "подводных камнях" при работе с
разнообразными наборами данных (этот момент рассматривается более подробно в
следующем разделе).
То же самое относится и к наборам данных, которые могут оказаться непригодными для получения ответа на определенный вопрос. Лишь то, что они не подходят
для вашего текущего проекта, не означает, что они не сгодятся для другого. Но независимо от того, увидите ли вы их когда-либо снова или нет, работая с этими наборами данных, вы многое узнаете: о предмете данных, о подводных камнях и возможностях определенных типов данных, об экспертах в данной области и о многом
другом. Иными словами, отказ от набора данных или подхода к кодированию никогда не является "напрасной тратой времени": приобретенный в процессе соответствующей работы опыт только улучшит вашу следующую попытку.
Опасные подводные камни
Одна из причин, по которой важно документировать свою работу, заключается в
том, что очень часто человеком, для которого вы составляете эту документацию, в
действительности являетесь просто "будущий вы", возвращающийся к определенному набору данных или сценарию (или вообще платформе Python) после перерыва
в несколько дней, недель или даже месяцев. К тому времени вещи, которые однажды были очевидными, будут казаться запутанными и неясными, если только вы
всесторонне не задокументировали их. Можно не разобраться даже с обычными
"уроками", особенно в спешке или когда вы сосредоточены на чем-либо другом.
Очистка, преобразование и дополнение данных
|
291
Я сама это испытала, когда я прорабатывала упражнения в нескольких последних
главах, и это при том, что я прилагала особые усилия при проверке своей работы.
Для меня этот опыт стал еще одним напоминанием того, что если с вашим сценарием что-либо не так, то это совершенно не обязательно что-то сложное, а скорее наоборот — что-то очень простое.
Далее приводятся два распространенных "подводных камня", которые следует
иметь в виду.
Проверьте используемый регистр
При каждой проверке, совпадают ли две строки, не забывайте о важности используемого регистра! При работе над сценарием в листинге 6.16 я поначалу не
обратила внимания на то, что названия всех предприятий (но не названия банков) были написаны заглавными буквами. Несколько минут я была в фрустрации, думая, что мой набор данных не содержит записи для значения WATERFORD
RECEPTIONS, пока я наконец не посмотрела на данные опять и не осознала свою
ошибку.
Всегда указывайте тип данных
В процессе разработки решения для сценария в листинге 7.10, который описан в
разд. "Тернистый путь к «простым» решениям" ранее в этой главе, у меня
опять возникли проблемы с нахождением совпадений для значений, которые, я
была уверена, должны были содержаться в моем наборе данных. Но я забыла,
что библиотека pandas (в отличие от библиотеки csv) пытается применять типы
данных к столбцам данных, считываемых в ее структуру DataFrame. В данном
случае это означало, что значение столбца OriginatingLenderLocationID стало числом (а не строкой). В результате мои усилия по поиску определенных значений в
этом столбце терпели неудачу, поскольку я пыталась сравнить, например, число
71453 со строкой "71453", что определенно было обречено на провал.
В данном случае наиболее простым решением было добавление параметра в вызове функции read_csv(), указывающего, что все данные должны считываться
как строки (например, fingerprinted_data1 = pd.read_csv('public_150k_plus_fin
gerprints.csv', dtype=' string' ))7. Это также предотвратило преобразование некоторых больших сумм в данных в экспоненциальное представление (например,
числа 1210681 в 1,21068e+06).
После простых орфографических ошибок этот тип "подводных камней" является
наиболее распространенным типом проблем, с которыми вам придется столкнуться
в процессе выпаса данных. Поэтому, если вы обнаружите, что в какой-то момент
вы допустили подобную оплошность, постарайтесь не слишком расстраиваться.
В действительности это просто признак того, что вы обладаете хорошей программной логикой, но некоторые элементы форматирования нуждаются в исправлении.
7
Я не применила этот подход в конечном коде сценария в листинге 7.10, но использовала его в
листинге 7.12.
292
|
Глава 7
Дополнение данных
Добавление столбца OriginatingLenderFingerprint (см. листинг 7.10) было хорошим
способом повысить полезность и удобство данных по кредитам по программе PPP,
но другим способом добавления значения в набор данных будет использование
других наборов данных для дополнения первоначального. Это легче всего реализовать для размерностно структурированного набора данных, поскольку он уже основан на каком-либо широко используемом стандарте. В нашем случае с данными
кредитов по программе PPP пример такой ситуации можно видеть в столбце NAICSCode. Быстрый поиск в Интернете8 по ключевым словам "NAICS code" предоставляет информацию об этом коде:
...North American Industry Classification System (Система классификации отраслей Северной Америки). Система NAICS была разработана для удовлетворения
нужд федеральных статистических агентств при сборе, анализе и публикации
статистических данных, связанных с экономикой США.
С учетом этого мы, вероятно, сможем найти способ дополнить наши данные, добавив в них информацию о коде NAICS для каждой записи. Это может помочь нам
расширить наше понимание о том, бизнесы каких типов и в каких отраслях участвуют в кредитной программе PPP. Мы, скорее всего, смогли извлечь полный список
кодов NAICS из основного веб-сайта, но поиск в Интернете по словам "naics sba"
предоставит нам более интересные возможности. В частности, Управление по делам малого бизнеса предлагает PDF-документ, содержащий информацию о рекомендациях этого управления для бизнесов по коду NAICS касательно размера бизнеса в миллионах долларов или в количестве сотрудников (https://sba.gov/sites/
default/files/2019-08/SBA%20Table%20of%20Size%20Standards_Effective%20Aug%
2019%2C%202019.pdf). Кроме предоставления более понятного описания самих
кодов NAICS, этой информацией можно расширить наши данные о кредитах по
программе PPP, что может помочь нам получить ответы на более общие вопросы о
том, какие бизнесы в действительности можно квалифицировать как "малые".
Наш процесс для этого не будет значительно отличаться от процесса слияния данных, которое мы осуществляли несколько раз ранее, как в выполняемых операциях,
так и в представляемых им вопросах. Начнем с рассмотрения веб-версии рекомендаций Управления по делам малого бизнеса по размеру бизнесов. Щелчок по ссылке BA's Size Standards Webpage на первой странице PDF-документа открывает
более общую страницу веб-сайта Управления по делам малого бизнеса
(https://sba.gov/federal-contracting/contracting-guide/size-standards), в разделе
Numerical Requirements которого находится ссылка table of small business size
standards — таблица стандартов размеров малых предприятий (https://sba.gov/
document/support-object-object-table-size-standards). Внизу этой страницы можно
найти ссылку (https://www.sba.gov/sites/default/files/2022-05/Table%20of%20Size%
20Standards_Effective%20May%202%202022_Final.xlsx) для загрузки версии XLSX
8
Один из результатов которого ведет нас к https://naics.com/what-is-a-naics-code-why-do-i-need-one.
Очистка, преобразование и дополнение данных
|
293
предыдущего PDF-документа. Из полученного XLSX-документа можно экспортировать второй рабочий лист (который содержит фактические коды и описания) в
файл CSV, после чего эти данные можно будет импортировать и сравнивать с нашими данными о кредитах по программе PPP.
Как мы увидим в сценарии в листинге 7.12, при интегрировании любого нового источника данных данные этого источника нужно оценить, очистить и преобразовать
точно так же, как в случае с данными нашего "основного" набора данных. В данном
случае это означает, что в файле данных о кредитах по программе PPP нам нужно
выполнить упреждающее обновление всех значений <NA> в столбце NAICSCode, заменив его каким-либо флаговым значением (я выбрала для этого строку None). Это
нужно для того, чтобы не допустить совпадений этих записей с по сути случайными значениями <NA> в файле кодов NAICS, полученном на веб-сайте Управления по
делам малого бизнеса. После слияния этих двух файлов нам нужно будет проверить, для каких кодов из файла данных кредитов по программе PPP не было совпадений. На данном этапе мы оставим открытым вопрос о том, как выполнять эту
операцию, пока мы немного не проанализируем данные, чтобы выяснить, хотим ли
мы "заполнить" их (например, обычными значениями или расшифровками NAICS),
пометить их флагом как нетипичные для Управления по делам малого бизнеса или
использовать комбинацию этих двух элементов информации.
Листинг 7.12. Сценарий ppp_adding_naics.py
# Сценарий для слияния данных о кредитах по программе PPP с данными NAICS
# Управления по делам малого бизнеса о требованиях к размерам предприятий.
# (Доступны по этой ссылке: https://www.sba.gov/document/support--table-size-standards)
# Импортируем библиотеку pandas для выполнения операций слияния и сортировки
import pandas as pd
# Считываем данные о кредитах по программе PPP в структуру DataFrame
ppp_data = pd.read_csv('public_150k_plus_fingerprints.csv', dtype='string')
# Считываем данные NAICS в отдельную структуру DataFrame
sba_naics_data = pd.read_csv('SBA-NAICS-data.csv', dtype='string')
# Если поле столбца NAICSCode не содержит значения, вставляем в него строку None.
ppp_data['NAICSCode'] = ppp_data['NAICSCode'].fillna("None")
# Объединяем эти два набора данных, используя для этого операцию "левое объединение"
merged_data = pd.merge(ppp_data, sba_naics_data, how='left',
left_on=['NAICSCode'], right_on=['NAICS Codes'],
indicator=True)
294
|
Глава 7
# Открываем файл для записи в него объединенных данных
merged_data_file = open('ppp-fingerprints-and-naics.csv', 'w')
# Записываем объединенные данные в наш выходной файл в формате CSV
merged_data_file.write(merged_data.to_csv())
# Отображаем значения столбца _merge, чтобы увидеть, сколько записей данных о кредитах
# не содержат совпадений с кодом NAICS
print(merged_data.value_counts('_merge'))
# Создаем новую структуру DataFrame только для строк без совпадений
unmatched_values = merged_data[merged_data['_merge']=='left_only']
# Открываем файл для записи значений без совпадений
unmatched_values_file = open('ppp-unmatched-naics-codes.csv', 'w')
# Записываем в новый файл CSV все несовпавшие коды NAICS из файла кредитов
# по программе PPP вместе с информацией о количестве вхождений каждого такого кода
unmatched_values_file.write(unmatched_values.value_counts('NAICSCode').to_csv())
Параметр dtype='string' принуждает библиотеку pandas рассматривать все значения нашего набора данных как строки. Таким образом мы упростим выполнение
будущих операций сравнения и поиска совпадений.
Если не выполнить эту замену, то наши данные будут непредсказуемо совпадать
со значениями NA в файле SBA-NAICS-data.csv.
Дополнение набора данных таким образом, как это делается в сценарии в листинге 7.12, может помочь нам расширить круг вопросов, для получения ответа на которые мы сможем его использовать, а также способствовать поддержке более быстрого и полного анализа и интерпретации данных. В то же самое время при любом
внедрении в набор данных новых данных нам нужно выполнить полный цикл
оценки, очистки, преобразования и (даже возможно) дополнения, которому мы
подвергли наш "основной" набор данных. Это означает, что нам всегда нужно искать компромисс между усовершенствованием наших основных данных (делая их,
возможно, более полезными) и объемом времени и усилий, прилагаемых для поиска и выпаса "вторичных" данных, используемых для дополнения основных данных.
Заключение
Разнообразие возможностей очистки, преобразования и дополнения данных настолько же велико, как и разнообразие наборов данных и возможностей их анализа.
Но основная цель этой главы состоит в том, чтобы проиллюстрировать распростра-
Очистка, преобразование и дополнение данных
|
295
ненные проблемы с очисткой, преобразованием и дополнением данных, а также
представить некоторые ключевые методы для решения этих проблем.
Но прежде чем переходить к практическому генерированию знаний на основе наших данных, в главе 8 мы сделаем небольшое отступление и сконцентрируемся на
рассмотрении некоторых передовых методов программирования, которые могут
помочь нам обеспечить наивысший уровень понятности, эффективности и рациональности нашего кода. Поскольку использование языка Python для выпаса данных
уже позволяет нам выполнять задачи, которые было бы невозможно выполнить
другими инструментами, оптимизация нашего кода как для первичного, так и для
повторного использования является другим способом получения максимальной отдачи от каждой разрабатываемой нами программы или фрагмента кода. В большинстве случаев это означает структурирование наших файлов таким образом,
чтобы они были более универсальными, удобочитаемыми и поддающимися компоновке, чем мы и займемся в следующей главе.
ГЛАВА 8
Структурирование и рефакторинг кода
Прежде чем приступать к рассмотрению анализа и визуализации выпаса данных мы
сделаем небольшое отступление и обсудим некоторые стратегии для реализации
большинства задач, которые мы рассмотрели до сих пор. В предыдущих главах мы
исследовали, как получить доступ и извлечь данные из разных источников и форматов, как оценить их практическое качество, а также как очистить и дополнить их
для возможного анализа. В процессе работы наши сравнительно простые программы эволюционировали и изменялись, неизбежно становясь более запутанными и
сложными. Например, циклы for приобретали один (или несколько) вложенных
условных операторов if, некоторые из которых содержали загадочные цифры, как,
например, в выражении if the_date.weekday() <= 4: в листинге 7.5. Является ли это
ценой за более функциональный код?
Вспомним, что комментирование кода может значительно повышать уровень понимания логики разрабатываемых сценариев как для других, так и для нас самих в
будущем. Но, как оказывается, подробное документирование (как бы мне ни нравился этот подход) — не единственный способ повысить ясность разрабатываемого
кода Python. Подобно другим типам текстовых средств, платформа Python поддерживает ряд полезных механизмов для структурирования и организации разрабатываемого кода. Разумное применение этих средств может упростить как первое, так
и повторные использования ваших программ.
Поэтому в этой главе мы рассмотрим инструменты и концепты, которые позволят
вам усовершенствовать свой код, сделав его одновременно удобочитаемым и пригодным для повторного использования.
Этот процесс называется рефакторингом (refactoring) и служит еще одним примером того, что использование платформы Python для выпаса данных позволяет повысить отдачу для прилагаемых нами усилий. В частности, хотя в случае надобности мы можем полагаться на функциональность, предоставляемую чьей-либо библиотекой, мы также можем создавать новые "заготовки", точно настроенные под
наши требования и предпочтения.
Обзор пользовательских функций
При рассмотрении основ языка Python в главе 2 мы вкратце затронули такое понятие, как определяемые пользователем функции, или просто пользовательские
Структурирование и рефакторинг кода
|
297
функции1. В сценарии листинга 2.7 мы увидели, как пользовательскую функцию
можно использовать для инкапсуляции простой задачи вывода на экран приветствия
при предоставлении определенного имени. Конечно же, пользовательские функции
можно создавать для задач любого уровня сложности. Но прежде чем приступать к
рассмотрению механики создания пользовательских функций, отступим на шаг назад и обдумаем, какие аспекты дизайна могут наилучшим образом помочь нам решить, когда создание пользовательской функции будет наиболее полезным. Подобно любому процессу создания литературных сочинений, здесь нет жестких правил,
но далее приводится несколько эвристических методов, которые помогут решить, в
каких случаях и как имеет смысл использовать рефакторинг.
Многократное использование кода
Подобно переменным, определить фрагменты кода, упаковка которых в пользовательские функции может принести пользу, можно, определив многократно выполняющуюся задачу. Наличие в сценарии большого количества неудобных условных
выражений или повторяющихся шагов, от проверки входных данных до форматирования выходных (см. например, листинг 7.7), является индикатором того, что
следует рассмотреть возможность создания пользовательских функций. Также надо
иметь в виду, что рефакторинг может быть полезным не только в случае операций,
повторяющихся только в одном сценарии. В частности, при наличии определенных
задач, исполняющихся многократно в разных сценариях (например, проверка, является ли данный день рабочим, как в листинге 7.5), соответствующую пользовательскую функцию всегда можно вынести в отдельный сценарий и подключать его
в любом требуемом месте, как мы это делали при обработке файлов с учетными
данными в главе 5.
Аккуратное и понятное документирование
Документирование разрабатываемого кода сродни подарку как для членов команды
разработки, так и для самого себя в будущем2. В то же самое время подробное документирование разрабатываемого кода может в конечном итоге понизить его удобочитаемость, особенно если документировать не только используемые методы, но
также и причину их использования. Таким образом, создание действительно понятного кода — это балансирование между предоставлением объема подробностей,
достаточного для понимания кода, но при этом достаточно краткого, чтобы пользователи не устали читать вашу документацию.
Выделение соответствующих фрагментов кода в пользовательские функции является ключевым способом достичь такого баланса: подобно переменным, пользовательским функциям можно (и нужно!) присваивать описательные названия. Просто
читая название функции, пользователь может получить вполне достаточную ин1
2
В данном случае "пользователем" считается сам программист.
Воистину, качественная документация в некоторых случаях может быть настоящим спасителем.
298
|
Глава 8
формацию о ее работе, без необходимости сопровождать ее код строками поясняющих комментариев. Если название функции достаточно описательное и/или
данному пользователю в настоящий момент не требуются дополнительные подробности, он может просто продолжить свою работу. Но в случае надобности он все
равно сможет найти подробную документацию по этой функции, аккуратно размещенную в другой части программы (или в другом файле). Это означает, что строчные комментарии для вашего основного сценария могут быть сравнительно краткими, но при этом без ущерба для полноты документации.
Недостаточная функциональность по умолчанию
Это, может быть, и не самая лучшая причина для создания пользовательских функций, но зато абсолютно реальная. Со временем в ваших работах по выпасу данных
могут обнаружиться некоторые повторяющиеся задачи, но существующие функции
и библиотеки, используемые для их решения, могут быть не совсем оптимальными.
Возможно, что какая-либо функция имеет труднозапоминаемое название и вам постоянно приходится вспоминать, как именно она называется. Или вы постоянно
забываете добавить какой-либо параметр, что усложняет всю вашу работу (да-да, я
говорю о параметре dtype='string' функции pd.read_csv()). Для разработчиков, работающих самостоятельно или в небольшой команде, будет абсолютно приемлемым создавать пользовательские функции, упрощающие их работу просто потому,
что они упрощают их работу. Вам не нужно искать какое-либо серьезное обоснование для этого. Если это упростит вашу работу, то полный вперед! В этом и заключается мощь программиста.
Конечно же, здесь есть определенные ограничения. Если только не использовать
гораздо более формальный и сложный подход к разработке кода Python, вы не
сможете, например, определить новую функцию, имеющую то же самое название,
что и уже существующая функция, или изменить поведение операторов + или – 3.
Но если вы просто хотите немного откорректировать работу какой-либо существующей функции, то с этим не будет никаких проблем. Только обязательно подробно задокументируйте свою версию.
Область видимости
Рассмотрев некоторые из причин для возможного рефакторинга разрабатываемого
кода посредством пользовательских функций, можно вкратце рассмотреть и механизм для этого. Вероятно, самое важное понятие, которое нужно понимать для создания пользовательских функций, — это область видимости, или действия (scope),
функции. Хотя мы еще не использовали этот термин, мы работали с обозначаемой
им возможностью с момента объявления наших первых переменных в сценариях в
листингах 2.22.4.
3
Эти вещи вполне реализуемы, но выходят далеко за рамки большинства операций по выпасу данных
и, поэтому, за рамки данной книги.
Структурирование и рефакторинг кода
|
299
В них мы увидели, что мы можем:
1. Создавать именованные переменные и присваивать значения их содержимому
(например, author="Susan E. McGregor").
2. Обращаться к содержимому этих переменных по их названиям и передавать их
значения функциям (например, print(author)).
При этом мы знаем, что программа, состоящая только из строки вызова функции,
которой передается наша объявленная переменная, вызовет ошибку:
print(author)
Это объясняется тем, что во "вселенной" нашего однострочного сценария не существует ячейки памяти с названием author. В результате интерпретатор Python выдает нам сообщение об ошибке и отказывается продолжать исполнение программы.
В программировании под областью видимости имеется в виду текущая "вселенная" с точки зрения определенного фрагмента кода. Область видимости отдельного
сценария эволюционирует по мере считывания компьютером каждой строки кода
сверху вниз, вызывая вполне ожидаемое поведение сценариев в листингах 8.1 и 8.2.
Листинг 8.1. Переменная author отсутствует в области видимости
# "Вселенная" этой строки кода не содержит переменной author.
# Интерпретатор выдает сообщение об ошибке
print(author)
Листинг 8.2. Область видимости содержит переменную author
# Создаем переменную author
author = "Susan E. McGregor"
# "Вселенная" этой строки кода содержит переменную author.
# Продолжаем исполнение
print(author)
Точно так же, как и при создании каждой переменной в памяти компьютера создается новый "контейнер" для хранения ее содержимого, при объявлении каждой функции для нее создается новая "вселенная" или область видимости. Это означает, что
при использовании пользовательских функций мы разделяем наш код не только визуально, но также логически и функционально. В результате мы можем обращаться с
созданными нами пользовательскими функциями так же, как и со встроенными методами Python и библиотечными функциями, которые мы использовали в этой книге:
как с "рецептами", для которых мы предоставляем "ингредиенты", и получать в результате их исполнения некоторое значение или новый объект Python. Единственное
отличие состоит в том, что пользовательские функции мы "готовим" сами.
Чтобы получить представление, что все это означает на практике, возвратимся к
сценарию в листинге 2.8, но слегка откорректировав его, как показано в листинге 8.3.
300
|
Глава 8
Листинг 8.3. Сценарий greet_me_revisited.py
# Создаем функцию, отображающую на экране приветствие по любому переданному ей имени
def greet_me(a_name):
print("Variable `a_name` in `greet_me`: "+a_name)
print("Hello "+a_name)
# Создаем переменную с именем author
author = "Susan E. McGregor"
# Создаем другую переменную с именем editor
editor = "Jeff Bleiel"
a_name = "Python"
print("Variable `a_name` in main script: "+a_name)
# Используем нашу пользовательскую функцию greet_me() для вывода персонализированного
# сообщения "Hello..." для каждого пользователя
greet_me(author)
greet_me(editor)
print("Variable `a_name` in main script again: "+a_name)
Исполнение этого сценария дает следующий результат:
Variable `a_name` in main script: Python
Variable `a_name` in `greet_me`: Susan E. McGregor
Hello Susan E. McGregor
Variable `a_name` in `greet_me`: Jeff Bleiel
Hello Jeff Bleiel
Variable `a_name` in main script again: Python
Поскольку любая пользовательская функция автоматически получает свою собственную область видимости, она может видеть только те переменные и значения,
которые явно передаются ей. В свою очередь, переменные и значения внутри этой
функции фактически скрыты от кода основного сценария. Одно из последствий
этого состоит в том, что при создании пользовательских функций нам не нужно
беспокоиться о том, что используемые в них названия переменных уже могли быть
использованы в основном сценарии, или наоборот. В результате нам нужно придумывать меньшее количество уникальных названий переменных, что помогает при
создании сценариев большего размера и сложности. Это также означает, что, добившись ожидаемой работы пользовательской функции, мы можем использовать ее
и даже модифицировать ее работу без необходимости вносить корректировки в переменные и функции в сценарии, в котором она используется.
Структурирование и рефакторинг кода
|
301
Определение параметров функции
У нас уже есть довольно значительный опыт предоставления "ингредиентов" (которые официально называются аргументами) методам и функциям, встроенным в
Python и предоставляемым используемыми библиотеками. Но приступая к разработке пользовательских функций, нам нужно исследовать более подробно процесс
для определения параметров, передаваемых этим функциям4.
Прежде всего, в отличие от некоторых других языков программирования, язык Python не требует (более того, даже по-настоящему не позволяет) от программистов
настаивать на том, чтобы параметры функции были определенного типа данных5.
Поэтому при желании (или по неведению) функции вполне возможно передать
данные неправильного типа. Таким образом, решение о том, каким образом проверять (и проверять ли вообще) достоверность передаваемых пользовательской
функции аргументов, принимается вами как разработчиком этой функции. В принципе, к решению этой задачи можно подойти тремя способами:
проверяется тип данных всех передаваемых функции аргументов и в случае ка-
кого-либо несоответствия выдается предупреждение или сообщение об ошибке;
код
заключается
в
блок
try…except
(https://w3schools.com/python/
python_try_except.asp), чтобы улавливать определенные типы ошибок, не прерывая исполнение всей программы. При этом можно также выдавать сообщение
о причине ошибки;
не беспокоиться об этом и позволить пользователю функции (т. е. программи-
сту) самому решить все проблемы при помощи сообщений об ошибках языка
Python по умолчанию.
Это может казаться несколько либеральным, но на данном этапе моя основная рекомендация — выбрать третий вариант: не беспокоиться об этом. Но не потому,
что не будет ошибок, они будут, и во врезках "Быстрая прокрутка" главы 2 были
описаны некоторые из них. А потому что наш основной интерес состоит в выпасе
данных, а не в разработке программ Python уровня предприятия. Как и в случае со
сценариями в главе 4, мы хотим сбалансировать задачи, решаемые нами, и задачи,
выясняемые и решаемые самим программистом (кем бы он ни был). Поскольку в
случае их неправильной работы наши программы не выведут из строя какой-либо
важный веб-сайт или не исказят единственную копию важных данных, кажется более разумным не пытаться предвидеть и реагировать на каждую возможную ошибку, которая может возникнуть при их работе. Конечно же, если четко и ясно документировать разрабатываемые функции (мы рассмотрим этот процесс более под4
Строго говоря, параметры означают названия переменных, указываемых в определении функции, а
аргументы — это сами значения этих переменных, передаваемые функции при ее вызове. Но на
практике эти термины используются взаимозаменяемо.
5
Так называемые языки со статическим контролем типов данных (statically typed languages) не разрешат исполнение кода, если функции или методу переданы данные неправильного типа.
302
|
Глава 8
робно в разд. "Использование pydoc для документирования сценариев и пользовательских функций" далее в этой главе), тогда другие программисты будут иметь
всю требуемую информацию, чтобы не допускать ошибок с самого начала.
Доступные опции
Даже если разрабатываемые вами пользовательские функции не предназначены для
использования тысячами других пользователей, их все равно можно сделать гибкими и полнофункциональными. Один из самых простых способов для этого —
разрабатывать функции для решения наиболее распространенной версии задачи, но
при этом делать их в некоторой степени адаптируемыми, снабдив их возможностью
принимать дополнительные аргументы, подобно тому как это делается в библиотеке pandas6 и других библиотеках. Например, нашу функцию greet_me() можно модифицировать таким образом, чтобы при желании вместо приветствия по умолчанию "Hello" она выводила любое приветствие, передаваемое в нее программистом.
Таким образом мы можете создавать функции, которые можно эффективно использовать в разных контекстах. Для примера рассмотрим модифицированную версию
функции greet_me(), код которой приводится в листинге 8.4.
Листинг 8.4. Сценарий greet_me_options.py
# Создаем функцию, отображающую на экране приветствие по любому переданному ей имени
def greet_me(a_name, greeting="Hello"):
print(greeting+" "+a_name)
# Создаем переменную с именем author
author = "Susan E. McGregor"
# Создаем другую переменную с именем editor
editor = "Jeff Bleiel"
# Используем нашу пользовательскую функцию greet_me() для вывода
# индивидуализированного приветствия для каждого пользователя.
# По умолчанию выводим приветствие "Hello"
greet_me(author)
# Программист задает приветствие "Hi"
greet_me(editor, greeting="Hi")
Как видно, добавить необязательные аргументы можно так же просто, как и задать
значение по умолчанию в определении функции. Если при вызове функции программист передаст ей другое значение, то оно просто заменит значение по умолчанию.
6
Опять этот параметр dtype='string'!
Структурирование и рефакторинг кода
|
303
Предоставление аргументов
Кроме предоставления значения по умолчанию в объявлении функции, пользовательской функции можно передавать необязательные дополнительные аргументы.
В частности, язык Python поддерживает два общих типа необязательных аргументов, *args и **kwargs:
*args
Этот параметр полезен для передачи функции нескольких значений, когда присвоение им всех названий и/или значений по умолчанию было бы довольно трудоемким. Значения, передаваемые через *args, сохраняются в списке, и в функции доступ к ним можно получить, обрабатывая их поочередно посредством
цикла for...in (т.е., for arg in args).
**kwargs
Параметр **kwargs похож на *args, с той разницей, что он позволяет передавать в
функцию произвольное количество именованных аргументов, не присваивая им
значения по умолчанию, как это делалось в сценарии в листинге 8.4. Доступ к
передаваемым таким образом значениям можно получить посредством метода
kwargs.get() (например, my_var=kwargs.get(greeting)).
Наличие параметров *args и **kwargs может показаться удобным способом оставить открытыми разные варианты при разработке пользовательских функций. Но я
должна сказать вам, что пользовательские функции в частности и сценарии в целом
всегда лучше разрабатывать для решения существующих задач, а не для задач, с
которыми, вы полагаете, вам, возможно, придется иметь дело когда-либо в будущем. Хотя идея чрезвычайной гибкости может изначально казаться привлекательной, это обычно ведет к трате большого количества времени, думая о возможных
будущих задачах, вместо того чтобы решать те, которые стоят перед вами в настоящее время. А у кого есть для этого время, когда нам нужно заниматься выпасом данных?
Возвращаемые значения
До сих пор наши модификации функции greet_me() не отличались большими амбициями: мы, по сути, просто использовали их для вывода на экран индивидуально
настроенных сообщений. При этом функции внешних библиотек, которые мы использовали, обладали невероятно мощными возможностями. Например, взять данные в простом формате разделения запятыми (.csv) и преобразовать их в структуру
DataFrame библиотеки pandas или преобразовать целый файл .xls в коллекцию подробных списков и атрибутов, улавливающих почти все аспекты этого многоуровневого типа файла. Такой уровень программирования на Python выходит за рамки
этой книги, но мы все же можем создавать опрятные, чрезвычайно полезные пользовательские функции, используя мощь возвращаемых значений.
Если параметры/аргументы представляют собой "ингредиенты" наших функций"рецептов", то возвращаемые значения — это конечное блюдо, которое употребля-
304
|
Глава 8
ется остальной программой. По сути, возвращаемые значения — это обыкновенные
данные; они могут быть литералами (например, строка «Hello») или переменными
любого типа данных. Возвращаемые значение полезны тем, что они позволяют нам
передать функции требуемые для нее значения и получить обратно значения, требуемые нам, не беспокоясь (по крайней мере, с точки зрения, т. е. области видимости, основной программы) о внутренностях этого процесса. Для примера модифицируем функцию greet_me() (см. листинг 8.3) для возвращения значения. Соответствующий код приводится в листинге 8.5.
Листинг 8.5. Сценарий make_greeting.py
# Создаем функцию, возвращающую приветствие по любому переданному ей имени
def make_greeting(a_name):
return("Hello "+a_name)
# Создаем переменную с именем author
author = "Susan E. McGregor"
# Создаем другую переменную с именем editor
editor = "Jeff Bleiel"
# Используем нашу пользовательскую функцию greet_me() для создания и сохранения
# индивидуализированных сообщений "Hello..." для каждого пользователя
author_greeting = make_greeting(author)
editor_greeting = make_greeting(editor)
# Теперь выводим на экран приветствия, созданные и возвращенные каждым вызовом функции
print(author_greeting)
print(editor_greeting)
Поначалу вы можете задаваться вопросом, и в чем же польза этого подхода. В том,
что хотя наша основная программа увеличилась в размере, она, возможно, стала
более гибкой и понимаемой. Поскольку функция make_greeting() возвращает значение, а не просто печатает приветствие на экране, круг возможностей его использования значительно расширяется. Конечно же, его можно просто отобразить на экране, как это делается в листинге 8.5, но возвращаемое значение можно сохранить в
переменной для какой-либо последующей обработки. Например, можно добавить
следующую строку кода:
print(editor_greeting+", how are you?")
Эта новая модификация может не выглядеть чем-то особенным, но она позволяет
как выделить некоторую работу (в данном случае добавление приветствия "Hello" к
любому имени) в функцию, так и предоставить бóльшую гибкость в аспекте использования результата (например, добавить дополнительный текст к одному, но не
к другому).
Структурирование и рефакторинг кода
|
305
Работа со стеком
Конечно же, создание новой переменной лишь для хранения простого приветствия,
как это делается в сценарии в листинге 8.5, создает больше проблем, нежели пользы. Но нигде не сказано, что результат одной функции обязательно сохранять в переменной, прежде чем передавать его другой. В действительности функции можно
вкладывать друг в друга, так что вывод одной функции просто становится вводом
другой. Мы уже применяли этот подход в нашей предыдущей работе — при передаче строк функции print(), например, добавляя вызов функции strip() в процесс
создания данных, разделенных запятыми (формат CSV) в сценарии в листинге 7.8.
Но его можно применять с любым набором функций, предполагая, что одна функция возвращает значение, требуемое следующей. Соответствующий пример приводится в листинге 8.6. Это просто сценарий из листинга 8.5, модифицированный
вставкой в него новой функции, которая добавляет текст ", how are you" к тексту
приветствия.
Листинг 8.6. Сценарий make_greeting_no_vars.py
# Функция, возвращающая приветствие по любому переданному ей имени
def make_greeting(a_name):
return("Hello "+a_name)
# Функция, добавляющая вопрос к любому приветствию
def add_question(a_greeting):
return(a_greeting+", how are you?")
# Создаем переменную с именем author
author = "Susan E. McGregor"
# Создаем другую переменную с именем editor
editor = "Jeff Bleiel"
# Отображаем сообщение приветствия
print(make_greeting(author))
# Передаем сообщение приветствия функции добавления вопроса и отображаем конечный
# результат
print(add_question(make_greeting(editor)))
Хотя оператор print(make_greeting(author)) довольно легко поддается пониманию,
оператор print(add_question(make_greeting(editor))) уже более сложный. Это как раз
иллюстрирует, что на практике количество вложений вызовов функций должно
быть в разумных пределах. Чем больше количество вложенных вызовов функций,
тем сложнее понимать этот код, несмотря на то что логика порядка операций остается прежней: самая внутренняя функция исполняется первой, а ее возвращаемое
значение "всплывает" и подается на вход следующей функции. Возвращаемое зна-
306
|
Глава 8
чение этой функции так же передается на вход следующей функции и так далее.
В традиционной терминологии программирования это называется стеком функций,
в котором самая внутренняя функция находится внизу стека, а самая внешняя —
вверху7. Стек функций для последней строки кода сценария в листинге 8.6 иллюстрируется на рис. 8.1.
Этот тип вложения вызовов функций лежит в основе всей философии программирования (https://en.wikipedia.org/wiki/Functional_programming), но ради удобочитаемости соответствующего кода в большинстве случаев его следует использовать
с осторожностью.
Рис. 8.1. Стек вложенных вызовов функций
Рефакторинг для получения удовольствия
и прибыли
Ознакомившись с некоторыми ключевыми принципами и механизмами для рефакторинга разрабатываемого кода, рассмотрим, как это можно использовать для повышения ясности некоторых наших сценариев из предыдущих глав. В процессе исследования следующих примеров помните, что (подобно любому процессу редактирования) решение относительно того, каким образом и какой код подвергать
рефакторингу, зависит от личных предпочтений и стиля конкретного программиста. Поэтому после каждого примера я буду описывать логику моих решений. Эти
рассуждения могут оказаться полезными для вас при разработке своего собственного подхода к рефакторингу.
Функция для определения рабочих дней
Сценарий в листинге 7.5 считывает данные по аренде велосипедов системы Citi
Bike (https://s3.amazonaws.com/tripdata/index.html), выбирает из них данные только
для аренд по рабочим дням и сохраняет их в новом файле. Нельзя сказать, чтобы
7
Этот термин, возможно, был причиной, почему так назван форум Stack Exchange.
Структурирование и рефакторинг кода
|
307
используемый в этом сценарии подход был чем-то фундаментально плох, но все же
я полагаю, что он является хорошим кандидатом для рефакторинга по ряду причин.
Во-первых, сценарий полагается на пару непривлекательных вызовов функций с
не очень описательными названиями. Первый вызов требуется для преобразования строки данных даты в собственно формат datetime, с которым Python может
работать:
the_date = datetime.strptime(a_row['starttime'], '%Y-%m-%d %H:%M:%S.%f')
И хотя встроенный метод weekday() довольно прямолинеен (хотя его лучше было бы
назвать dayofweek()), нам нужно сравнивать его результат с "магическим числом" 4,
чтобы определить, действительно ли значение the_date является рабочим днем:
if the_date.weekday() <= 4:
В целом, я полагаю, что эти фрагменты кода были бы более удобочитаемыми, если
бы они не содержали этих достаточно неясных форматов и сравнений.
Во-вторых, задача проверки, является ли определенная строка, похожая на дату,
рабочим днем (с понедельника по вторник), выглядит типом задачи, которую в области выпаса данных может потребоваться решать сравнительно часто. Если выделить этот код в пользовательскую функцию, то ее можно будет использовать в других сценариях.
Мой подход к рефакторингу сценария из листинга 7.5 приводится в листинге 8.7.
Листинг 8.7. Сценарий weekday_rides_refactored.py
# Цель: отфильтровать все аренды велосипедов системы Citi Bike за рабочие дни
# сентября 2020 года и сохранить в новый файл
#
#
#
#
#
#
Краткое описание программы:
1. Считываем файл данных: 202009-citibike-tripdata.csv
2. Создаем новый выходной файл и записываем в него строку заголовка
3. Для каждой строки в файле создаем из строки starttime значение даты
a. Если дата выпадает на рабочий день, записываем строку в выходной файл
4. Закрываем выходной файл
# Импортируем библиотеку csv
import csv
# Импортируем datetime
from datetime import datetime
def main():
# Открываем файл данных в режиме чтения
source_file = open("202009-citibike-tripdata.csv","r")
# Открываем выходной файл в режиме записи
output_file = open("202009-citibike-weekday-tripdata.csv","w")
308
|
Глава 8
# Передаем исходный файл source_file методу DictReader и сохраняем результат
# в переменной citibike_reader
citibike_reader = csv.DictReader(source_file)
# Создаем соответствующую структуру DictWrited; указываем, что названия ее полей
# нужно взять из citibike_reader
output_writer = csv.DictWriter(output_file,
fieldnames=citibike_reader.fieldnames)
# Записываем строку заголовка в выходной файл
output_writer.writeheader()
# Обрабатываем в цикле for...in список строк citibike_reader
for a_row in citibike_reader:
# Если текущее значение starttime является рабочим днем
if is_weekday(a_row['starttime']):
# Записываем эту строку данных в наш выходной файл
output_writer.writerow(a_row)
# Закрываем выходной файл
output_file.close()
def is_weekday(date_string, date_format='%Y-%m-%d %H:%M:%S.%f'):
# Преобразовываем значение в date_string в формат datetime
the_date = datetime.strptime(date_string, date_format)
# Если значение the_date является рабочим днем (т. е. целым числом от 0 до 4)
if the_date.weekday() <= 4:
return(True)
else:
return(False)
if __name__ == "__main__":
main()
Основной сценарий оформляется в виде функции main() согласно одному из соглашений Python и служит важной практической цели. Поскольку интерпретатор
Python обрабатывает код сценариев сверху вниз, то если не заключить этот код в
функцию, то при достижении оператора if is_weekday(a_row['starttime']) у интерпретатора еще не будет определения функции is_weekday() и он выдаст сообщение
об ошибке.
Функция is_weekday() используется для преобразования даты в виде строки в
дату, а затем в соответствующее числовое значение рабочего дня (от 0 до 4). Опи-
Структурирование и рефакторинг кода
|
309
сательное название функции передает смысл, не заставляя пользователя разбираться во всех подробностях процесса.
Интерпретатор считывает код сверху вниз, и хотя он сначала доходит до обеих
функций — main() и is_weekday() — он не должен исполнять никакого кода, пока не
дойдет до конца сценария.
Как видно, хотя основная часть кода (и даже многие комментарии) в листинге 8.7
та же самая, как и в листинге 7.5, этот код переупорядочен таким образом, что
часть main() сценария становится намного более краткой и удобочитаемой. Если
программисту захочется узнать подробности определения рабочих дней, то он может найти эти подробности здесь же, в определении функции is_weekday(). Если же
его эти подробности не интересуют, он может просто прочитать код основной части сценарии и с легкостью убедиться в том, что он реализует именно то, о чем говорится в кратком описании в начале сценария.
Что такое раздел __main__?
Оператор if в примечании листинга 8.7 — это одно из соглашений Python и
сокращение, указывающее компьютеру, выполняется ли этот сценарий напрямую. Основная цель защитного оператора if — предотвратить непредвиденное
поведение при импорте одного сценария Python в другой8. Другой способ минимизации шансов перекрытия или "сбоя" кода — агрегировать полезные
функции в файлах с описательным названием (например, date_functions.py), а
затем при необходимости импортировать их.
Хотя конструкция if __name__ == "__main__": обеспечит бесперебойное исполнение программы при (почти) любых обстоятельствах, по своему опыту могу
сказать, что будет легче отслеживать часто используемые функции, собрав их
в одном (или нескольких) файлах с понятными описательными названиями.
Опрятные метаданные
В главе 7 мы собрали на основе кода сценариев 4.8 и 7.6 сценарий (см. листинг 7.7),
который как обрабатывает даты в формате Microsoft Excel, так и разделяет исходный файл данных на текстовый файл с метаданными и структурированный файл
значений, разделенных запятыми (CSV). Получившийся сценарий делал все, что
нам требовалось, но это достигалось за счет увеличения объема кода, ухудшения
его удобочитаемости и наполнения труднопонимаемыми условиями и вызовами
непонятных функций форматирования дат.
8
Полезное описание и демонстрация обоснования этого соглашения приводятся по адресу
https://freecodecamp.org/news/if-name-main-python-example.
310
|
Глава 8
Мы можем немного улучшить этот код, вынеся форматирование разных типов исходного содержимого (в файлах .csv или .txt) в отдельные функции. Но для этого
необходимо немного переупорядочить и уточнить логику нашего сценария. Этот
процесс также иллюстрирует некоторые сложности, которые могут возникнуть при
вводе всей требуемой информации в пользовательские функции. В листинге 8.8
иллюстрируется подход, предпочитаемый мною в настоящее время для решения
этих задач.
Листинг 8.8. Сценарий xls_meta_and_date_parsing_refactored.py
# Преобразовываем табличные данные в файле .xls в формат csv и сохраняем их
# в соответствующем файле. Также отделяем метаданные и сохраняем их в отдельном файле.
# Для работы с данными дат используем библиотеку xrld
# Сначала установим библиотеку xlrd, используя установщик pip:
# https://pypi.org/project/xlrd/2.0.1/
# Импортируем библиотеку xlrd
import xlrd
# Импортируем библиотеку csv
import csv
# Требуется для проверки, является ли данное значение каким-либо типом числа
from numbers import Number
# Требуется для парсинга и форматирования наших дат Excel
from datetime import datetime
def main():
# Загружаем данные в переменную source_workbook посредством метода open_workbook()
source_workbook = xlrd.open_workbook("fredgraph.xls")
global the_datemode
the_datemode = source_workbook.datemode
# Открываем и именуем текстовый файл для хранения метаданных
source_workbook_metadata = open("fredgraph_metadata.txt","w")
# Рабочая книга .xls может содержать несколько листов
for sheet_name in source_workbook.sheet_names():
# Создаем переменную, указывающую на текущий рабочий лист
current_sheet = source_workbook.sheet_by_name(sheet_name)
Структурирование и рефакторинг кода
# Создаем выходной файл xls_"+sheet_name+".csv для текущего листа
output_file = open("xls_"+sheet_name+"_dates.csv","w")
# Записываем строки в формате .csv посредством метода writer()
output_writer = csv.writer(output_file)
# Создаем булеву переменную для определения достижения табличных данных
is_table_data = False
# Обрабатываем в цикле for каждую строку в листе
for row_num, row in enumerate(current_sheet.get_rows()):
# Извлекаем значение первого столбца текущей строки
first_entry = current_sheet.row_values(row_num)[0]
# Если строка заголовков таблицы данных
if first_entry == 'observation_date':
# Присваиваем переменной флага is_table_data значение True
is_table_data = True
# Если значение переменной is_table_data равно True
if is_table_data:
# Передаем требуемые данные функции create_table_row()
new_row = create_table_row(current_sheet, row_num)
# Записываем эту новую строку в наш выходной файл данных
output_writer.writerow(new_row)
# В противном случае эта строка должна быть метаданными
else:
# Передаем требуемые данные функции create_meta_text()
metadata_line = create_meta_text(current_sheet, row_num)
# Записываем эту новую строку в наш выходной файл метаданных
source_workbook_metadata.write(metadata_line)
# На всякий случай закроем наши файлы
output_file.close()
source_workbook_metadata.close()
def create_table_row(the_sheet, the_row_num):
# Извлекает значения табличных данных в отдельные переменные
the_date_num = the_sheet.row_values(the_row_num)[0]
U6_value = the_sheet.row_values(the_row_num)[1]
|
311
312
|
Глава 8
# Создаем новый объект строки, содержащий все значения
new_row = [the_date_num, U6_value]
# Если the_date_num содержит число, тогда текущая строка не является строкой
# заголовка. Нужно выполнить преобразование даты
if isinstance(the_date_num, Number):
# Создаем объект Python datetime, использую метод xldate_as_datetime()
# библиотеки xlrd
the_date_num = xlrd.xldate.xldate_as_datetime(the_date_num, the_datemode)
# Создаем новый список, содержащий значение the_date_num (в формате ММ/ДД/ГГГГ
# посредством функции strftime()) и значение во втором столбце
new_row = [the_date_num.strftime('%m/%d/%Y'),U6_value]
# Возвращаем полностью отформатированную строку
return(new_row)
def create_meta_text(the_sheet, the_row_num):
meta_line = ""
# Для форматирования файла метаданных обрабатываем отдельные ячейки каждой строки
# метаданных
for item in the_sheet.row(the_row_num):
# Записываем в ячейку значение с символом табуляции в конце
meta_line = meta_line + item.value + '\t'
# В конце каждой строки метаданных добавляем символ новой строки
meta_line = meta_line+'\n'
# Возвращаем полностью отформатированную строку
return(meta_line)
if __name__ == "__main__":
main()
Объявление переменной глобальной (global) дает доступ к ее значению любой
функции. В данном случае имеется в виду глобальная область видимости. Но использовать глобальные переменные следует с умеренностью. Обратите внимание
на то, что в Python глобальным переменным нельзя присваивать значения в той же
самой строке кода, в которой они объявляются. Это объясняет, почему здесь используется два отдельных оператора — один для объявления глобальной переменной, а другой для присвоения ей значения.
Например, если не создавать глобальную переменную для формата даты
(the_datemode), то нам нужно было бы передавать это значение функции
create_table_row() в виде дополнительного аргумента, что выглядит несколько неуместным.
Структурирование и рефакторинг кода
|
313
Сравнивая код листинга 8.8 с кодом в листинге 8.7, можно увидеть в них много
общего: вместо одного линейно исполняемого фрагмента кода отдельные функциональности выделены в отдельные методы, а я main() защищен условным оператором if __name__ == "__main__":. Этот пример почти идеально совпадает с кодом в
листинге 7.7: импортируются одни и те же библиотеки, и хотя код в листинге 8.8
реорганизован в три отдельные функции, по большому счету он почти идентичен
коду в листинге 7.7.
Один из моментов, которые я хочу здесь проиллюстрировать, заключается в том,
что рефакторинг кода не обязательно требует громадных усилий и может дать
важные и полезные результаты. Как программа, основной сценарий в листинге
8.8, по большому счету, содержит только логику решения задачи, но никаких
подробностей этого решения, которые реализуются нашими пользовательскими
функциями. Если бы мне потребовалось загрузить с базы данных FRED и обработать новый набор данных, я вполне спокойно могла бы применить этот сценарий,
что посмотреть, что из этого выйдет, поскольку в случае каких-либо проблем я
знаю точно, куда идти, чтобы их исправить. Например, в случае возникновения
каких-либо проблем форматирования с новым источником данных мне не потребуется "бродить" по всей программе, потому как я почти наверняка знаю, что
причина будет крыться в коде либо функции create_table_row(), либо функции create_meta_text(). Это означает, что для того, чтобы приспособить этот сценарий для
работы с новыми (подобными) источниками данных, нужно будет проверить (возможно) всего лишь десяток строк кода. А это намного лучше, чем иметь дело с
почти 100 строками!
Иными словами, рефакторинг сценариев для выпаса данных обычно не требует
создания слишком большого объема дополнительного кода, но при необходимости
облегчит использование или адаптирование кода в будущем. Это поможет избавить
вас от необходимости исследования бóльшего объема кода, чем это необходимо. То
есть рефакторинг — это еще один способ, который поможет вам масштабировать
свою работу по выпасу данных при использовании языкаPython.
Глобальные переменные
Глобальные переменные Python (см., например, листинг 8.8) отличаются от
обычных переменных двумя признаками:
1. Ключевое слово global указывает, что к данной переменной можно обращаться (а также изменять ее) из любой функции сценария. Иными словами, она
имеет глобальную область видимости.
2. Глобальной переменной (как, например, the_datemode) нельзя присвоить значение в строке ее объявления, как это можно делать с большинством других переменных (например, is_table_data = False). Вместо этого значение таким переменным присваивается в отдельной строке кода, следующей сразу же за
строкой ее объявления.
314
|
Глава 8
Сейчас вы, наверное, думаете: эй, а это ключевое слово global — удобная штука.
Почему бы не сделать все мои переменные глобальными? Потому что, хотя глобальные переменные могут быть полезными в некоторых случаях, их необходимо использовать осмотрительно, как последнее средство.
Правда, использование глобальных переменных в коде может содействовать
уменьшению объема модификаций кода при рефакторинге сценария. Но недостатки использования большого количества глобальных переменных могут превзойти преимущества рефакторинга, особенно когда идет речь об использовании
одинаковых имен переменных в разных функциях и сценариях. Со временем в
процессе разработки кода вы начнете вырабатывать свои собственные соглашения для именования переменных, а также создавать набор наиболее часто используемых функций. Но если какой-либо из ваших сценариев будет содержать
глобальную переменную с тем же самым названием, как и название переменной
в подключаемой функции, ваш код начнет вести себя странным и непредсказуемым образом. Подобное наложение может привести вас к длительному поиску
причины проблемы, поскольку для ее обнаружения вам придется исследовать
каждую строку своего кода. Иными словами, глобальные переменные следует
использовать в высшей степени осторожно и крайне редко или вообще не использовать, если это возможно.
Учитывая все причины не использовать глобальные переменные, почему я в данном случае все же решила использовать одну из них? Прежде всего, метод
source_workbook.datemode может возвратить только одно возможное значение, поскольку каждая электронная таблица Excel имеет только один атрибут datemode. Поэтому, даже если определенная рабочая книга электронной таблицы имела бы 20
разных рабочих листов, содержащих 100 столбцов данных, для них всех будет
только одно, единственное, значение datemode. Получается, что значение datemode в
действительности является "глобальным", и будет логичным, чтобы глобальной
была и переменная, используемая для его хранения. А поскольку значение datemode
никогда не нужно будет обновлять в сценарии, то получение из него неожиданного
значения несет минимальный риск.
Но, как и в случае с любой писательским творчеством, эти решения являются делом личных предпочтений, хотя собственные предпочтения также могут со временем измениться. Мне сначала нравилась симметричность создания одной функции
для построения каждой строки табличных данных и другой функции для построения каждой строки текста метаданных, однако нарушение этой симметрии и не использование глобальной переменной datemode также имеет свои достоинства. Соответствующий пример приводится в сценарии в листинге 8.9.
Листинг 8.9. Сценарий xls_meta_and_date_parsing_refactored_again.py
# Преобразовываем табличные данные в файле .xls в формат csv и сохраняем их
# в соответствующем файле. Также отделяем метаданные и сохраняем их в отдельном файле.
# Для работы с данными дат используем библиотеку xrld.
Структурирование и рефакторинг кода
|
315
# Сначала установим библиотеку xlrd, используя установщик pip:
# https://pypi.org/project/xlrd/2.0.1/
# Импортируем библиотеку xlrd
import xlrd
# Импортируем библиотеку csv
import csv
# Требуется для проверки, является ли данное значение каким-либо типом числа
from numbers import Number
# Требуется для парсинга и форматирования наших дат Excel
from datetime import datetime
def main():
# Загружаем данные в переменную source_workbook посредством метода open_workbook()
source_workbook = xlrd.open_workbook("fredgraph.xls")
# Открываем и именуем текстовый файл для хранения метаданных
source_workbook_metadata = open("fredgraph_metadata.txt","w")
# Рабочая книга .xls может содержать несколько листов
for sheet_name in source_workbook.sheet_names():
# Создаем переменную, указывающую на текущий рабочий лист
current_sheet = source_workbook.sheet_by_name(sheet_name)
# Создаем выходной файл xls_"+sheet_name+".csv для текущего листа
output_file = open("xls_"+sheet_name+"_dates.csv","w")
# Записываем строки в формате .csv посредством метода writer()
output_writer = csv.writer(output_file)
# Создаем булеву переменную для определения достижения табличных данных
is_table_data = False
# Обрабатываем в цикле for каждую строку в листе
for row_num, row in enumerate(current_sheet.get_rows()):
# Извлекаем значение первого столбца текущей строки
first_entry = current_sheet.row_values(row_num)[0]
# Если строка заголовков таблицы данных
if first_entry == 'observation_date':
# Присваиваем переменной флага is_table_data значение True
is_table_data = True
316
|
Глава 8
# Если значение переменной is_table_data равно True
if is_table_data:
# Извлекает значения табличных данных в отдельные переменные
the_date_num = current_sheet.row_values(row_num)[0]
U6_value = current_sheet.row_values(row_num)[1]
# Если the_date_num содержит число, тогда текущая строка не является
# строкой заголовка, поэтому преобразовываем дату.
if isinstance(the_date_num, Number):
the_date_num = format_excel_date(the_date_num, source_workbook.datemode)
# Записываем эту новую строку в наш выходной файл данных
output_writer.writerow([the_date_num, U6_value])
# В противном случае эта строка должна быть метаданными
else:
# Передаем требуемые данные функции create_meta_text()
metadata_line = create_meta_text(current_sheet, row_num)
# Записываем эту новую строку в наш выходной файл метаданных
source_workbook_metadata.write(metadata_line)
# На всякий случай закроем наши файлы
output_file.close()
source_workbook_metadata.close()
def format_excel_date(a_date_num, the_datemode):
# Создаем объект Python datetime, используя метод xldate_as_datetime()
# библиотеки xlrd
a_date_num = xlrd.xldate.xldate_as_datetime(a_date_num, the_datemode)
# Создаем новый список, содержащий значение the_date_num (в формате ММ/ДД/ГГГГ
# посредством функции strftime()) и значение во втором столбце
formatted_date = a_date_num.strftime('%m/%d/%Y')
return(formatted_date)
def create_meta_text(the_sheet, the_row_num):
meta_line = ""
# Для форматирования файла метаданных обрабатываем отдельные ячейки каждой строки
# метаданных
for item in the_sheet.row(the_row_num):
Структурирование и рефакторинг кода
|
317
# Записываем в ячейку значение с последующим символом табуляции
meta_line = meta_line + item.value + '\t'
# В конце каждой строки метаданных добавляем символ новой строки
meta_line = meta_line+'\n'
return(meta_line)
if __name__ == "__main__":
main()
Хотя я больше не использую подход с передачей параллельной информации
пользовательской функции для создания выходных файлов метаданных и табличных данных, отказ от этой симметрии избавляет меня от необходимости создавать
глобальную переменную.
Какой из этих подходов лучше? Как и во всем, что касается творчества, это зависит
от личных предпочтений, вариантов использования и аудитории. Некоторые группы или организации будут иметь свое особое мнение касательно использования или
не использования глобальных переменных, некоторые отдадут предпочтение более
коротким решениям, а другие будут видеть ценность в структурной симметрии или
возможности повторного использования. Хотя в сценарии в листинге 8.9 я жертвую
некоторой структурной симметрией, в нем создается функция с более обширными
возможностями повторного использования. Решение же всегда остается за вами.
Использование pydoc для документирования сценариев
и пользовательских функций
До сих пор мы применяли подход всестороннего, но в произвольной форме, документирования разрабатываемого кода. В принципе, в этом нет ничего плохого, особенно если такой подход помогает обеспечить вообще ведение документирования.
Но по мере расширения вашего набора сценариев Python (и их целевой аудитории)
будет полезно иметь возможность проверить свою коллекцию пользовательских
функций без необходимости открывать и исследовать каждый соответствующий
файл по отдельности. Наличие большого количества открытых файлов значительно
повышает вероятность внесения ошибки в какую-либо широко используемую
функцию в результате, например, нечаянного нажатия клавиши. А это может очень
быстро испортить весь ваш день.
К счастью, добавив лишь немного форматирования, мы можем адаптировать наши
существующие комментарии и описания программ для работы с функцией командной строки pydoc. Это позволит нам выводить описание сценариев и функций в окно
командной строки, просто предоставив соответствующее название файла.
318
|
Глава 8
Чтобы продемонстрировать эту возможность в действии, начнем с рефакторинга
еще одного сценария. На этот раз мы модифицируем исходный код сценария в листинге, несколько сократив его размер. В процессе я также обновлю комментарии в
начале сценария (а также добавлю некоторые комментарии к новой функции), чтобы сделать их совместимыми с командой pydoc. Конечный результат приводится в
листинге 8.10.
Листинг 8.10. Сценарий fixed_width_strip_parsing_refactored.py
""" Форматер данных управления NOAA
Считывает данные из файла данных управления NOAA фиксированной ширины средствами Python
и сохраняет их в новый файл в формате CSV (значения, разделенные запятыми)
Исходный файл данных для этого примера предоставлен управлением NOAA и доступен здесь:
https://www1.ncdc.noaa.gov/pub/data/ghcn/daily/ghcnd-stations.txt
Метаданные для файла доступны здесь:
https://www1.ncdc.noaa.gov/pub/data/ghcn/daily/readme.txt
Доступные функции
------------------* convert_to_columns: Преобразовывает строку текста в список
Requirements
-----------* Модуль csv
"""
# Начинаем с импортирования библиотекы csv
import csv
def main():
# Переменная для сопоставления названия выходного файла названию входного файла
filename = "ghcnd-stations"
# Открываем файл данных для чтения, указывая режим r
source_file = open(filename+".txt", "r")
# Метод "readlines()" method преобразовывает содержимое текстового файла в список
# строк
stations_list = source_file.readlines()
# Создаем выходной файл для записи модифицированного набора данных
output_file = open(filename+".csv","w")
# Используем библиотеку csv для создания средства записи, которое предоставляет
# нам удобные функции для создания нового файла в формате csv
output_writer = csv.writer(output_file)
Структурирование и рефакторинг кода
|
319
# Эти заголовки нужно создавать "вручную", используя содержимое файла readme.txt`
headers = ["ID","LATITUDE","LONGITUDE","ELEVATION","STATE","NAME",
"GSN_FLAG","HCNCRN_FLAG","WMO_ID"]
# Создаем список "кортежей", содержащих указатель начала и конца каждого столбца
column_ranges = [(1,11),(13,20),(22,30),(32,37),(39,40),(42,71),(73,75),
(77,79),(81,85)]
# Записываем заголовки в выходной файл
output_writer.writerow(headers)
# Обрабатываем в цикле каждую строку файла
for line in stations_list:
# Отправляем данные для форматирования
new_row = convert_to_columns(line, column_ranges)
# Записываем new_row в выходной файл, используя метод writerow()
output_writer.writerow(new_row)
# На всякий случай закрываем выходной файл
output_file.close()
def convert_to_columns(data_line, column_info, zero_index=False):
"""Преобразовывает строку текста в список на основе предоставленных пар указателей
Параметры
---------data_line : str
Строка текста для обработки
column_info : Список кортежей
Каждый кортеж содержит указатель начала и конца каждого столбца данных
zero_index: boolean, optional
Если False (состояние по умолчанию), указатель начальной позиции уменьшается
на единицу
Возвращает
------Список
Список значений с удаленными начальными и конечными пробелами
"""
new_row = []
# Функция полагает, что предоставляемые указатели имеют НЕНУЛЕВОЕ начальное
# значение, поэтому уменьшаем начальные значения указателей на 1
index_offset = 1
320
|
Глава 8
# Если column_info имеет начальное нулевое значение, то тогда начальные значения
# указателей не уменьшаем на 1
if zero_index:
index_offset = 0
# Обрабатываем в цикле список указателей столбцов
for index_pair in column_info:
# Извлекаем начальное значение, модифицированное значением index_offset
start_index = index_pair[0]-index_offset
# Извлекаем конечное значение
end_index = index_pair[1]
# Удаляем пробелы вокруг данных
new_row.append((data_line[start_index:end_index]).strip())
# Возвращаем данные с удаленными пробелами
return new_row
if __name__ == "__main__":
main()
Заключение описания основного сценария и функции convert_to_columns() в три
двойные кавычки (""") выделяет их визуально от остальных комментариев в коде и
делает их доступными для отображения в окне командной строки, исполняя команду pydoc9:
pydoc fixed_width_strip_parsing_refactored
В результате исполнения этой команды в окне консоли отобразятся описания основного сценария и функции. Если весь текст не помещается в окно консоли, то по
нему можно перемещаться построчно, используя стрелки вверх и вниз, или постранично, используя клавиши <PgUp> или <PgDn>.Чтобы завершить просмотр документации и возвратиться в режим командной строки, нажмите клавишу <Q>.
Вместо того чтобы создавать уникальную строку кода для извлечения каждого
столбца данных из заданной строки текста, я поместила все начальные и конечные
значения для каждого столбца в список кортежей Python (http://docs.python.org/
3.3/library/stdtypes.html?highlight=tuple#tuples), которые представляют собой, по
сути, неизменяемые списки.
Передавая функции convert_to_columns() вместе с каждой строкой информацию о
начале и конце столбца, мы можем преобразовать текст в столбцы посредством
9
В данном случае я использую смесь разных стилей структурирования и форматирования, взятых из
руководства, доступного по адресу https:// realpython.com/documenting-python-code/#documentingyour-python-code-base-using-docstrings. И если при работе в большой команде может быть важным
использование стандартного метода, то для самостоятельной работы или работы в небольшой группе
предпочтительнее использовать стиль, наиболее удобный для вас.
Структурирование и рефакторинг кода
|
321
цикла for…in. Это делает наш основной сценарий более удобочитаемым, а также
создает функцию, которую можно использовать повторно с любой строкой текста,
при условии передачи ей пар указателей начала и конца столбца в правильном
формате. Я даже добавила флаг zero_index, который позволяет использовать эту
функцию с парами указателей с нулевым значением первой позиции (по умолчанию —
значение первой позиции, равное 1, как это сделано и в данном наборе данных).
Обратите внимание на то, что кроме просмотра документации для всего сценария
посредством команды pydoc можно просматривать документацию и для отдельной
функции (например, для функции convert_to_columns)), исполнив ее следующим образом:
pydoc fixed_width_strip_parsing_refactored.convert_to_columns
Перемещение по документации отдельной функции осуществляется тем же самым
образом, как и по документации для всего сценария.
Перемещения по документации в командной строке
Документацию для любого сценария Python (при условии, что она была отформатирована должным образом) можно просмотреть в консоли командной
строки, исполнив следующую команду:
pydoc название_файла_без_расширения_.py
Подобным же образом можно просмотреть и документацию для любой функции в сценарии, исполнив следующую команду:
pydoc название_файла_без_расширения_.py.название_функции
Поскольку по отображаемому таким образом тексту нельзя перемещаться при
помощи мыши, для этого используются следующие клавиши на клавиатуре:
Стрелка вверх/стрелка вниз
Перемещение на одну строку вверх или вниз.
Клавиша пробела
Перемещение на одну страницу вниз.
Клавиша <Q>
Завершение просмотра документации.
О полезности аргументов командной строки
Кроме рефакторинга длинного сценария в набор функций существуют и другие
способы для повторного использования кода выпаса данных. Для многошаговых
процессов выпаса данных (например, содержащие загрузку данных, как в сценарии
в листинге 5.8, или для преобразования изображений PDF в текст, как в сценарии в
листинге 4.20) разбиение кода на несколько сценариев будет еще одним способом
сэкономить время и усилия. Прежде всего, этот подход позволяет свести к мини-
322
|
Глава 8
муму выполнение ресурсозатратных задач, таких как, например, загрузка данных
или преобразование страниц PDF-документа в изображения. Более того, такие задачи, как правило, довольно рутинные. Нет большой разницы, какие именно данные загружать или какой PDF-документ преобразовывать в изображения, и наш
сценарий, по большому счету, будет делать одно и то же. Нам требуется всего лишь
несколько дополнительных приемов, чтобы преобразовать наши специальные сценарии для выпаса данных в автономный код, пригодный для многократного повторного использования без каких-либо дополнительных модификаций.
Для примера рассмотрим снова сценарий из листинге 5.8. Основная исполняемая в
нем задача — загрузка содержимого веб-страницы, но в данном случае адрес целевого файла (http://web.mta.info/developers/turnstile.html) жестко прописан в сценарии. Подобным образом, код в листинге 5.1 для загрузки файлов XML и JSON
почти идентичен данному, с единственной существенной разницей, состоящей в
использовании других URL-адресов и названий локальных файлов. В долгосрочной
перспективе мы могли бы сэкономить много времени и усилий, выполнив рефакторинг этих сценариев, чтобы они работали подобно функции.
К счастью, благодаря библиотеке Python argparse, это вполне достижимая цель для
автономных сценариев Python. Эта библиотека позволяет создавать сценарии, которые как требуют, так и используют аргументы, передаваемые им из командной
строки. Таким образом, нам не нужно создавать новый сценарий для каждой отдельной веб-страницы, которую мы хотим загрузить, поскольку мы можем указать
как целевой URL-адрес, так и название выходного файла из командной строки. Соответствующий сценарий приводится в листинге 8.11.
Сценарий 8.11. Сценарий webpage_saver.py
""" Web page Saver!
Загружает и сохраняет содержимое веб-страницы на локальном устройстве
Использование
----python webpage_saver.py target_url filename
Параметры
---------target_url : str
Полный URL-адрес веб-страницы, подлежащей загрузке
filename : str
Желаемое название файла локальной копии данных
Требования
-----------* Модуль argparse
* Модель requests
"""
Структурирование и рефакторинг кода
|
323
# Импортируем библиотеку requests для получения данных из Интернета
import requests
# Импортируем argparse для извлечения аргументов из командной строки
import argparse
# Создаем новый экземпляр ArgumentParser()
parser = argparse.ArgumentParser()
# Аргументы будут присваиваться в порядке их предоставления
parser.add_argument("target_url", help="Full URL of web page to be downloaded")
parser.add_argument("filename", help="The desired filename of the local copy")
args = parser.parse_args()
# Извлекаем из предоставленных аргументов URL-адрес веб-страницы, из которой следует
# выполнять загрузку
target_url = args.target_url
# Извлекаем из предоставленных аргументов требуемое название локального файла
# для хранения полученных данных
output_filename = args.filename
# Создаем заголовочную информацию для нашего запроса веб-страницы
headers = {
'User-Agent': 'Mozilla/5.0 (X11; CrOS x86_64 13597.66.0) ' + \
'AppleWebKit/537.36 (KHTML, like Gecko) ' + \
'Chrome/88.0.4324.109 Safari/537.36',
'From': 'YOUR NAME HERE - youremailaddress@emailprovider.som'
}
# Поскольку мы загружаем обычную веб-страницы, отправляем по URL-адресу запрос GET
# вместе с заголовочной информацией
webpage = requests.get(target_url, headers=headers)
# Открываем локальный файл в режиме записи для сохранения содержимого загруженной
# веб-страницы
output_file = open(output_filename,"w")
# Код веб-страницы имеет свойство text ответа веб-страницы, поэтому записываем
# это в наш файл
output_file.write(webpage.text)
# Закрываем выходной файл
output_file.close()
Здесь мы присваиваем название параметра, которое можно использовать для обращения к значениям, передаваемым из сценария через командную строку. Справочный текст имеет важность! Делайте его описательным, но кратким.
324
|
Глава 8
На помощь!
Данный код использует позиционные аргументы в командной строке вместо
необязательных аргументов, потому что он не сможет делать ничего полезного без наличия хотя бы аргументов целевого URL-адреса target_url и filename
для хранения полученных данных.
Но каким образом пользователь, который не знаком с нашим сценарием, может знать об этом? Очевидно, что у нас имеется документация для просмотра
при помощи команды pydoc, но у нас также есть текст справки, добавленный к
каждому аргументу. Таким образом, новый пользователь узнает требуемые аргументы, исполнив следующую команду:
python webpage_saver.py -h
В результате в консоли командной строки выводится краткая инструкция по
использованию нашего сценария (по крайней мере, что касается требуемых аргументов).
Теперь у нас должен быть легкий способ для загрузки любой веб-страницы на локальное устройство, без необходимости создавать сценарий для каждого нового
URL-адреса. Например, исполнение следующей команды:
python webpage_saver.py "http://web.mta.info/developers/turnstile.html"
\"MTA_turnstiles_index.html"
дает точно такие же результаты, как и сценарий в листинге 5.8. Но исполнив команду:
python webpage_saver.py \
"https://www.citibikenyc.com/system-data/operating-reports" \
"citibike_operating_reports.html"
мы сможем получить операционные отчеты компании Citi Bike без необходимости
даже открывать, не то что модифицировать наш сценарий. Удобно, не так ли?
Использование аргументов командной строки со сценариями, специфичными для определенного типа задач, может сэкономить время, но не в
том случае, если URL-адреса нужно вводить в командную строку вручную с клавиатуры. Чтобы упростить себе жизнь, рассмотрим вкратце, как
можно копировать требуемые аргументы и вставлять их в командную
строку, в зависимости от используемой операционной системы.
Linux (включая Chromebook)
Выделите URL-адрес, который нужно скопировать, затем щелкните по
выделению правой кнопкой мыши и в открывшемся контекстном меню
выберите опцию Копировать. В командной строке просто щелкните
мышью, и скопированный текст вставится автоматически.
Структурирование и рефакторинг кода
|
325
Windows/Macintosh
Выделите URL-адрес, который нужно скопировать, затем щелкните по
выделению правой кнопкой мыши и в открывшемся контекстном меню
выберите опцию Копировать. В окне командной строки снова щелкните
правой кнопкой мыши и в контекстном меню выберите опцию Вставить.
Отличия во взаимодействии со сценариями
в автономных файлах и блокнотах
Вы, возможно, заметили, что в предыдущих разделах я не описывала способа передачи аргументов через командную строку в блокнотах Jupyter. Я также не очень
обсуждала создание документации для сценариев и функций для блокнотов Jupyter
и последующее взаимодействие с этой документацией. Но это объясняется не невозможностью таких действий, а тем, что взаимодействие пользователей с Python в
блокнотах Jupyter осуществляется по-другому, нежели в автономных сценариях,
поэтому некоторые из этих концепций менее применимы к ним. Я начала работать
с Python до появления блокнотов Jupyter (ранее IPython), поэтому по-прежнему
склоняюсь к использованию автономных сценариев для большинства своих повседневных задач выпаса данных на Python. Хотя блокноты (обычно) замечательно
подходят для тестирования и настройки фрагментов кода, после определения рабочего подхода для конкретной задачи по выпасу данных, в конечном итоге я почти
всегда перехожу к автономным сценариям. Это объясняется тем, что по мере углубления в проект у меня часто не хватает терпения даже для открытия и модифицировании сценария (если только это абсолютно не необходимо), не говоря уже о
запуске веб-сервера или ожидании загрузки веб-страницы. (К тому же, может оказаться так, что даже нет подключения к Интернету!) Это несколько из тех причин,
по которым я предпочитаю использовать аргументы командной строки и автономные сценарии для повседневных, несложных задач.
Но, как мы вскоре увидим, когда дело доходит до экспериментирования (и особенно совместного использования) с анализом данных и визуализации нашей
работы по выпасу данных, то интерактивные возможности блокнотов Jupyter делают их несколько предпочтительней автономных сценариев Python. Поэтому
блокнотам Jupyter уделяется больше внимания в главах 9 и 10, в которых рассматриваются эти темы.
Заключение
В этой главе мы значительно отошли от предмета непосредственно выпаса данных,
чтобы пересмотреть некоторые аспекты нашей предыдущей работы, где код стал
громоздким. Посредством процесса рефакторинга мы исследовали возможность
реорганизации просто работающего кода в хорошо работающий код, поскольку
326
|
Глава 8
такой код удобно читать и повторно использовать. По мере развития наших проектов по выпасу данных это поможет нам создать и использовать собственную коллекцию пользовательских функций, служащих для удовлетворения наших конкретных требований по выпасу данных. Подобным образом, немного повысив уровень
структуризации нашей документации, мы сделали ее доступной непосредственно
из командной строки, чтобы можно было найти требуемый нам сценарий или
функцию, не открывая для этого ни одного файла сценария. Мы применили к нашим сценариям логику рефакторинга, чтобы можно было настраивать их функциональность, также не открывая их.
В следующей главе мы снова обратим наше внимание на работу с данными и приведем обзор некоторых важных методов анализа данных. Затем, в главе 10, мы
вкратце рассмотрим основные методы визуализации, которые помогут вам эффективно понимать и представлять свои данные, чтобы можно было лучше поделиться
своими знаниями, полученными в результате выпаса данных, с остальным миром.
ГЛАВА 9
Введение в анализ данных
Выше в этой книге основное внимание уделялось способам нахождения данных, а
также их оценке, преобразованию и дополнению. Мы исследовали, как написать
код, достающий данные из Интернета, извлекающий их из неудобных форматов,
оценивающий их полноту и учитывающий, есть ли в данных какая-либо несогласованность. Мы даже обдумали, как гарантировать, что используемые нами инструменты (наши скрипты на Python) действительно со всем этим справятся, и как оптимизировать их под наши потребности, в том числе — на будущее.
Но в данный момент пришло время вернуться к вопросу, почему все это работает.
Еще в разд. "Что такое выпас данных" главы 1 я описала, зачем нужен выпас данных, т. е. преобразование "сырой" информации в осмысленный материал, на основе
которого могут быть сделаны выводы. Если не углубляться в анализ, то мы не узнаем, были ли достаточны наши усилия по выпасу данных — и какие выводы на их
основе можно было бы сделать. Поэтому остановить выпас наших данных на этапе
аугментации/преобразования — все равно, что разложить все нужное для готовки —
и выйти из кухни. Никто не будет битый час шинковать овощи и подбирать другие
ингредиенты, если не собирается готовить. То же касается и анализа данных: берем
все красиво очищенные и препарированные данные — и превращаем их в новые
открытия и знания.
Если вы опасаетесь, что мы вновь скатываемся в абстракции, не волнуйтесь —
основы анализа данных просты и достаточно конкретны. Как и оценка данных,
анализ данных примерно на три четверти состоит из рассуждений и на четверть —
из технических деталей. Да, в основы анализа данных заложена бесспорная математика в стиле "2 + 2 = 4", но выводы зависят от интерпретации результатов этих
весьма простых формул. Именно здесь в работу включаются логика и изыскания —
наряду с человеческим здравым смыслом и экспертным опытом — в качестве связующего звена.
В рамках этой главы мы исследуем основы анализа данных — в частности, такие
простые показатели, как центральная тенденция и распределение, помогающие
поместить данные в информативный контекст. Мы также рассмотрим эмпирические правила, помогающие делать верные выводы о данных на основе этих показателей и на основе роли визуального и числового анализа в понимании тенденций и
аномалий в рамках нашего датасета. В завершение этой главы мы поговорим об
ограничениях анализа данных и постараемся объяснить, почему для перехода от
328
|
Глава 9
"что" к "почему" одним анализом данных как таковым не обойтись. Мы также рассмотрим, как Python может помочь нам с решением этих задач и почему он так
уместен для любых дел — от быстрых расчетов до подробных визуализаций.
Вся суть — в контексте
Если бы я прямо сейчас предложил вам яблоко за 50 центов, вы бы купили? Ради
примера предположим, что вам нравятся яблоки и сейчас вы не прочь перекусить.
А еще это красивое яблоко: лоснящееся, ароматное, тяжеленькое. Также будем исходить из того, что вы уверены: я предлагаю вам яблоко без злого умысла. Это просто классное свежее яблоко за 50 центов. Вы бы купили?
Большинство людей ответит: по ситуации. От чего зависит решение? От множества
вещей. Независимо от того, насколько вы мне доверяете (и безотносительно моего
почти идеального яблока), если рядом со мной окажется кто-нибудь, продающий
"не менее хорошие яблоки" по 25 центов за штуку, вы решите купить яблоко у него, а не у меня. Почему? Очевидно, потому, что у него яблоки дешевле, и даже мое
невероятно аппетитное яблоко, пожалуй, не лучше двух яблок конкурента. В то же
время, если бы по другую руку от меня стоял ваш кузен и продавал вкусные яблоки
по 60 центов за штуку, то вы, вероятно, предпочли бы его яблоки, просто из солидарности с ним, желая поддержать его "стартап" по продаже яблок.
Такой процесс принятия решений может показаться весьма сложным, если речь
идет всего-то о выборе фрукта, но на самом деле мы принимаем такие решения все
время, и результаты порой бывают удивительны. Некоторые экономисты, например
Дэн Ариели и Тим Харфорд, проводили исследования, показывающие, каково
влияние "простого" подарка, пусть вам и придется на него немного потратить, или
как может падать наша удовлетворенность собственным жалованьем, если мы узнаем, сколько зарабатывают наши коллеги1. Большинство наших приоритетов и
решений зависят от оценочных суждений, и чтобы эти суждения были эффективны,
мы должны знать, какие у нас есть варианты. Купил бы я сказочно бесподобное
яблоко за 50 центов? Может быть. Но, пожалуй, только если бы спешил, и если бы
за другим яблоком мне пришлось пройти еще километр. Хотя все мы понимаем, что
такое "по ситуации", правильнее в данном случае говорить "зависит от контекста".
Именно по причине важности контекста разрозненные точки данных сами по себе
бессмысленны. Даже если данные фактически "верны", единичный фрагмент данных не поможет нам принять решение. Генерация и усвоение новых знаний, в
принципе, сводится к привязке новой информации к той, что нам уже известна.
Иными словами, знания заключены не "в данных" как таковых, а в соотношении
данных с контекстом. Поскольку невозможно исчерпывающим образом исследовать контекст каждой ситуации, в которой требуется принимать решение (в том
числе, каковы ваши соображения по поводу яблочного стартапа кузена и важности
1
Для дополнительной информации читайте "Предсказуемо иррациональный" Дэна Ариели.
Введение в анализ данных
|
329
поддержать семейное дело), часто приходится ограничиваться изучением тех элементов контекста, которые можно (или решено) измерить и количественно выразить. Иными словами, обратимся к данным.
Как мы извлекаем контекст из данных? Например, нужно исследовать, откуда они
взялись, задаться вопросом, кто их собрал, когда и зачем — эти вопросы помогают
уточнить, что включено в данные и чего в них может недоставать. Кроме того, изыскиваем способы систематически сравнить каждую точку данных со всеми оставшимися, чтобы было понятно, насколько эта точка соответствует любым закономерностям, присутствующим в целостном датасете, либо нарушает их. Разумеется,
все это не приведет нас к однозначным "ответам", но многое для нас прояснит и
выведет нас на идеи, которыми мы сможем поделиться с другими. Эти выводы натолкнут нас на следующий вопрос о том, что происходит в окружающем мире.
Одинаковые, но не совсем
Насколько важно выстраивать контекст для извлечения ценной информации из
данных и откуда узнать, какие контексты важны? Учитывая, что насчитывается
бесконечно много типов отношений, которые мы могли бы выявить — даже среди
точек данных в совсем небольшом датасете — как нам определиться, на которые из
них обратить внимание? Возьмем, к примеру, данные из листинга 2.10, где содержится простой (выдуманный) список номеров страниц:
page_counts = [28, 32, 44, 23, 56, 32, 12, 34, 30]
В данном случае, имея дело даже с небольшим количеством чисел, можно представить немало типов "контекста". К примеру, можно отметить, что здесь встречаются
как четные, так и нечетные числа, а также, что некоторые числа здесь делятся без
остатка на 8. Проблема в том, что большинство из этих отношений не особо интересны и важны. Как же нам найти среди них заслуживающие внимания?
Оказывается, человеческий мозг особенно хорошо подходит для того, чтобы замечать и отыскивать отношения двух типов: одинаковости и несхожести. Наше внимание привлекают тенденции и аномалии такого рода, каков бы ни был конкретный
стимул — от усмотрения закономерностей в контурах облаков или результатах лотерей до быстрого выявления отличий в ориентации похожих объектов. Это означает, что тенденции и отклонения от них интересны практически по определению.
Поэтому, когда мы хотим выстроить осмысленный контекст для наших данных,
весьма целесообразно для начала выяснить, каким образом отдельные записи в
конкретном датасете схожи друг с другом или отличаются друг от друга.
Что типично? Оценка центральной тенденции
Что такое "среднее" значение какого-либо показателя? Употребляя этот термин в
обыденной жизни, мы часто понимаем "среднее" как "непримечательное", "ожи-
330
|
Глава 9
даемое" или "типичное". С учетом таких особенно заурядных ассоциаций, зачастую
"среднее" также может стать синонимом "скучному".
Однако, когда речь заходит об анализе данных, оказывается, что именно "среднее"
нас и интересует, так как именно на основе среднего производится сравнение, а
сравнения — одна из тех вещей, которые особенно интересны человеку. Помните
исследование о зарплатах? Чисто по-человечески мы хотим знать, как соотносятся
затрагивающие нас вещи с показателями, "типичными" для других людей. Поэтому, даже если мы никогда и не стремились быть "средними", в целом, мы все равно
хотим знать, чему равно "среднее" и как с ним соотносится "наше".
Что это значит?
Возможно, вам уже знакомо, как вычисляется "среднее" от множества чисел: складываем их все и делим на количество слагаемых. Данный конкретный параметр
центральной тенденции более известен как среднее арифметическое и подсчитывается именно так, как мы только что описали. Итак, для нашей переменной
page_counts математика будет такова:
mean_pgs = (28+32+44+23+56+32+12+34+30)/9
что даст нам среднее значение в
32,333333333333336
Такой подход кажется весьма разумным для представления "типичной" длины главы; во многих наших главах действительно плюс-минус 30 страниц, есть даже две
главы ровно по 32 страницы. Поэтому среднее количество страниц по главам чуть
выше 32 страниц кажется относительно правильным.
В данном случае этот подход действительно нам помог, однако во многих ситуациях полагаться на среднее как на меру "типичности" бывает ошибочно. Допустим,
что в книгу добавилась одна очень длинная глава (скажем, 100 страниц). Метод
подсчета среднего не изменится:
mean_pgs = (28+32+44+23+56+32+12+34+30+100)/10
Но теперь среднее значение составит:
39,1
Совершенно внезапно наша "средняя" длина главы увеличилась почти на 6 страниц, хотя мы и добавили всего одну новую главу, а половина наших глав имеет
длину 2834 страницы. Настолько ли "типична" в данном случае глава длиной 39
страниц? Однозначно — нет.
Даже на данном небольшом примере мы четко видим, что хотя в некоторых случаях среднее является адекватной мерой "типичности", на этот показатель сильно
влияют экстремумы — и всего одного такого значения достаточно, чтобы среднее
арифметическое по датасету полностью девальвировалось. Но какие у нас есть альтернативы?
Введение в анализ данных
|
331
Берем на вооружение медиану
Еще один подход к определению "типичных" значений по датасету — выявить, какое из них (в буквальном смысле) находится посередине. В анализе данных "срединное" значение в последовательности записей называется медианой, и для его
нахождения требуется даже меньше математических расчетов, чем для вычисления
среднего арифметического. В данном случае нам понадобится всего лишь отсортировать и посчитать. Например, в нашем исходном множестве длин глав отсортируем значения от наименьшего к наибольшему2:
page_counts = [28, 32, 44, 23, 56, 32, 12, 34, 30]
page_counts.sort()
print(page_counts)
Имеем:
[12, 23, 28, 30, 32, 32, 34, 44, 56]
Теперь нам всего лишь остается выбрать "срединное" значение — то, что расположено на полпути между первой и последней позицией в списке. Поскольку в этом
списке девять элементов, это будет пятое значение (по обе стороны от него будет
по четыре элемента). Следовательно, медианное значение нашего датасета
page_count равно 32.
Теперь давайте посмотрим, что произойдет с медианой, если мы добавим в книгу
сверхдлинную главу. В отсортированном виде наши данные примут вид:
[12, 23, 28, 30, 32, 32, 34, 44, 56, 100]
А что же медиана? Поскольку теперь список состоит из четного количества элементов, мы можем просто взять два "средних" значения, сложить их и разделить на
два3. В данном случае речь идет о значениях на позициях 5 и 6, их сумма равна 32.
Соответственно, имеем медианное значение (32 + 32) / 2 = 32. Пусть мы и добавили еще одну очень длинную главу, медианное значение не изменилось!
На первый взгляд может показаться: "Постойте, это же неверно. Добавилась целая
новая глава — очень длинная глава, а значение медианы совсем не изменилось.
Разве не должно оно было сдвинуться, хотя бы чуть-чуть?"
Реальная разница между средним и медианным значением заключается в том, что
на среднее сильно влияют конкретные значения, содержащиеся в датасете, — насколько они высокие или низкие. В свою очередь, на медиану влияет в основном
частота встречаемости определенных значений. Таким образом, подход с медианой гораздо ближе к понятию "одно значение, один голос", тогда как при использовании среднего крайние значения могут получаться "слишком громкими". Поскольку в настоящий момент мы стремимся понять, какие значения являются "типичными" для нашего датасета, медиана всегда будет давать наиболее репрезентативный результат.
2
3
В частности, для сумм, расположенных непосредственно ниже или выше этих значений.
Подробнее см. на сайте https://www.sba.gov/document/support-faq-ppp-borrowers-lenders.
332
|
Глава 9
Поразмыслим нестандартно: выявляем выбросы
В разд. "Одинаковые, но не совсем" я отметила, что человеческие существа в принципе интересуются "схожестью" и "несхожестью". Рассматривая две наши возможные меры центральной тенденции, мы исследуем, в каком отношении значения в
датасете могут быть похожи. Но что насчет качеств, которыми они отличаются?
Возвращаясь к исходному списку page_count, мы полагаем, что 32-страничная глава
вполне может считаться типичной, а может быть, даже 30-, 28- и 34-страничные
главы — тоже. А что насчет 12-страничной главы? А 56-страничной? Они явно кажутся нетипичными, но откуда нам знать, какие значения достаточно отличаются
от массы, чтобы считать их по-настоящему "необычными"?
Именно здесь мы начинам объединять математику и здравый смысл. Меру центральной тенденции можно вычислить вполне однозначно, но определить, какие
значения в датасете по-настоящему необычны — т. е. являются аномалиями или
выбросами, — средствами одной лишь арифметики невозможно. По мере увеличения и усложнения датасетов человеку становится все сложнее эффективно интерпретировать их как наборы точек данных4. Поэтому в самом ли деле можно судить
о больших датасетах с точки зрения человеческого здравого смысла? Здесь нам
придется прибегнуть к крупнейшему и самому универсальному механизму обработки данных: человеческому зрению5.
Визуализация для анализа данных
Роль визуализации в работе с данными является двоякой. С одной стороны, визуализация может помочь проанализировать данные и осмыслить их; с другой — помогает передать те выводы, которые удалось получить по результатам сделанного
анализа. Использование данных именно со второй целью — в качестве средства
коммуникации, позволяющего поделиться выясненной нами информацией о данных с более широкой аудиторией — это тема, подробно рассматриваемая в главе
10. Здесь мы сосредоточимся на том, каким образом визуализация может помочь
нам яснее увидеть уже имеющиеся у нас данные.
Чтобы понять, как при помощи визуализации идентифицировать экстремальные
значения в наших данных, придется — в самом буквальном смысле — посмотреть
на данные. Здесь я не имею в виду, что мы должны открыть CSV-файл и приступать к прочесыванию строк с данными. Вместо этого мы создадим особую столбчатую диаграмму, которая называется гистограмма, где каждый столбец показывает,
4
Хотя точные оценки количества объектов, которые человек способен хранить в рабочей памяти,
разнятся, исследователи согласны с тем, что эта способность ограничена. См. на сайтах
https://ncbi.nlm.nih.gov/pmc/articles/PMC2864034 и https://pnas.org/content/113/27/7459.
5
Хотя до достижения эффективных результатов еще далеко, ведутся разнообразные исследования в
области тактильной графики. Эти решения позволят снизить зависимость визуальных подходов от
качества зрения, особенно для слепых или слабовидящих людей. См. на сайте http://shape.stanford.edu/
research/constructiveVizAccess/assets20-88.pdf.
Введение в анализ данных
|
333
сколько раз конкретное значение встречается в нашем датасете. Например, на рис. 9.1
показана очень простая гистограмма нашего (расширенного) датасета page_count.
Рис. 9.1. Простая гистограмма
Гистограмма для столь малого датасета, как в нашем примере с page_count, ничего
особого нам не скажет. На самом деле, когда в датасете всего 10 значений, его, пожалуй, не хватит ни для формулировки центральной тенденции (ни по среднему,
ни по медиане), ни для выявления выбросов. Но даже на рис. 9.1 усматриваются
зачатки потенциального паттерна: две равновеликие главы образуют двухвершинный пик, тогда как уникальные значения количества страниц для оставшихся глав
получаются отдельно стоящими столбиками, сгруппированными в кластер достаточно близко от пика. В то же время далеко справа расположена наша 100-страничная глава, и поблизости от этой точки данных нет никаких значений. Мы, вероятно, уже и так склонялись к выводу, что 100-страничная глава является аномалией
— выбросом, — опираясь на одну лишь математику, но данная гистограмма определенно подкрепляет нашу интерпретацию.
Разумеется, чтобы в полной мере оценить всю силу визуализации при анализе данных, давайте взглянем на датасет, значения которого по определению нельзя "подметить", как в случае с постраничным размером глав. К счастью, у нас есть данные
по программе гарантии выплаты заработной платы (PPP), с которыми мы работали
в главе 6, и они в этом отношении определенно полноценны, поскольку в этом
множестве данных содержатся тысячи кредитных записей. Чтобы понять, что можно узнать о типичности и "нетипичности" таких ссудных сумм, одобренных по программе PPP, напишем экспресс-скрипт, и он сгенерирует гистограмму тех значений
ссуд, которые к настоящему времени одобрены по PPP. Затем пометим эту гистограмму как средними, так и медианными значениями, чтобы проверить, насколько
334
|
Глава 9
хорошо каждый из этих показателей может послужить для потенциального определения центральной тенденции. После этого вернемся к вопросу идентификации
возможных выбросов среди одобренных ссудных сумм, подкрепив наши наблюдения как визуализацией данных, так и математикой.
Приступая к этой работе, я вновь вооружусь несколькими мощными библиотеками
Python — в частности, matplotlib и seaborn. В обеих есть функции для вычисления и
визуализации данных. В то время как библиотека matplotlib остается основополагающей для создания диаграмм и визуализаций на Python, мы также воспользуемся
seaborn, так как она очень пригодится нам при работе с более продвинутыми вычислениями и форматами. Поскольку две эти библиотеки очень хорошо совместимы друг с другом (фактически, seaborn можно считать надстройкой над matplotlib),
такая комбинация обеспечит нам гибкость, которая понадобится нам как для быстрого создания нужных здесь простейших визуализаций, так и для их кастомизации,
чтобы представлять данные еще эффективнее, — этим мы займемся в главе 10.
Пока давайте сосредоточимся на аналитическом размере нашего процесса визуализации. Для начала создадим простейшую гистограмму данных по нашим PPPссудам, для этого воспользуемся значениями CurrentApprovalAmount. Также для расширения контекста добавим среднюю и медианную линии, как показано в примере 9.1.
Листинг 9.1. ppp_loan_central_measures.py
# `pandas`для считывания и оценки наших данных
import pandas as pd
# `seaborn` для использования встроенных тем и типов диаграмм
import seaborn as sns
# `matplotlib` для кастомизации визуальных деталей
import matplotlib.pyplot as plt
# считываем данные
ppp_data = pd.read_csv('public_150k_plus_221.csv')
# задаем базовую цветовую тему для нашей визуализации
sns.set_theme(style="whitegrid")
# используем методы `mean()` и `median()`, встроенные в `pandas
mean = ppp_data['CurrentApprovalAmount'].mean() //
median = ppp_data['CurrentApprovalAmount'].median()
# создаем гистограмму значений в столбце `CurrentApprovalAmount`
approved_loan_plot = sns.histplot(data=ppp_data, x="CurrentApprovalAmount")
Введение в анализ данных
|
335
# получаем минимальное и максимальное значение по оси y в нашей гистограмме
y_axis_range = approved_loan_plot.get_ylim()//
# ставим вертикальные линии там, где они нужны
approved_loan_plot.vlines(mean, 0, y_axis_range[1], color='crimson', ls=':')//
approved_loan_plot.vlines(median, 0, y_axis_range[1], color='green', ls='-')
# Метод `show()` из matplotlib — тот самый, который отображает визуализацию
plt.show()//
Столбец CurrentApprovalAmount в данных о программе PPP сообщает, какова сумма каждой из одобренных в настоящее время ссуд в долларах (независимо от того,
была ли уплачена эта сумма).
Метод get_ylim() возвращает в виде списка наименьшее и наибольшее значение
по оси y. Как правило, он используется для того, чтобы задать удобочитаемую длину для строк с медианными и средними значениями.
В любой части гистограммы (или другой визуализации) можно добавить вертикальные линии, указав позицию по оси "x", начальную точку по оси "y", конечную
точку по оси "y", цвет и оформление линии. Обратите внимание: единицы для значений "x" и "y" даются относительно датасета, а не видимого размера диаграммы.
Притом, что в ноутбуках Jupyter не требуется явно вызывать метод show() из
библиотеки matplotlib, в отличие, например, от инструкций print(), я предпочитаю
включать show() для ясности и единообразия. По умолчанию диаграмма будет отображаться в "интерактивном" режиме (в Jupyter потребуется включить "магическую" команду %matplotlib notebook, как я сделал в предоставленных файлах). С ее
помощью можно уменьшать и увеличивать, панорамировать и другими способами
исследовать гистограмму, без написания дополнительного кода.
Скорее всего, вы рассматриваете диаграмму, получившуюся в результате запуска
именно этого скрипта (и выглядит она, надеюсь, примерно как на рис. 9.2), — и думаете: "а дальше что?" Признаться, эта первичная визуализация выглядит не блестяще — а то и совершенно запутывает нас. Не бойтесь! Как бы то ни было, считайте, что на этом примере вы впервые смогли увидеть, почему визуализация для
анализа и коммуникативная визуализация — совсем не одно и то же. Так, аналитические визуализации, подобные этой, сравнительно сложно читать, понимать и
уточнять, нежели любую коммуникативную визуализацию, которую вы показали
бы широкой аудитории. Однако, чтобы извлечь информацию из данных, начинать
нужно именно с такого примера.
Прежде чем двинуться далее с нашим анализом данных, давайте уделим время и
оценим подчеркнуто олдскульный6, но невероятно полезный интерфейс, предостав6
Ставший классикой. — Примеч. ред.
336
|
Глава 9
ляемый нам в Python именно для этой диаграммы. Этот скрипт на Python предоставляет нам не только статическое изображение, но и целую панель инструментов
(показанную на рис. 9.2), при помощи которой можно работать с изображением:
увеличивать, уменьшать, панорамировать, модифицировать и даже сохранять вывод. И хотя мы, конечно же, можем (и в конечном итоге будем) кастомизировать
содержание и оформление этой диаграммы в коде, интересен сам факт, что данные
можно эффективно исследовать, не занимаясь при этом постоянным изменением и
перезапуском кода — получается громадная экономия времени. Чтобы попробовать, каковы в данном случае наши возможности, уделите несколько минут и сами
поиграйте с элементами управления. Когда будете готовы продолжить анализ данных, щелкните по ярлыку home (домой), чтобы вернуть диаграмму в исходный вид —
и читайте следующие разделы.
Рис. 9.2. Гистограмма ссуд по программе PPP
Какова форма наших данных?
Учимся понимать гистограммы
При работе с данными в табличном формате мы привыкли считать, что данные
представлены в виде некоторого количества строк и столбцов (именно эту информацию возвращает свойство pandas.shape объекта DataFrame). В данном контексте
интересующая нас "форма" буквально означает контуры гистограммы и позволяет
нам заметить потенциально интересные паттерны или аномалии.
Введение в анализ данных
|
337
Первым делом в таких случаях мы будем обращать внимание вот на что:
Симметрия
Симметричны ли наши данные по вертикали? То есть можем ли мы провести
где-нибудь поверх наших визуализированных данных такую вертикальную линию, чтобы узор столбцов по одну сторону от нее выглядел (примерно) как их
отражение с другой стороны?
Плотность
Где кластеризована большая часть значений наших данных (и есть ли такая область)? Имеется несколько кластеров или всего один?
Неудивительно, что эти вопросы касаются не только эстетики. Контур гистограммы
датасета иллюстрирует ее свойство, называемое распределением. Поскольку определенные распределения обладают конкретными свойствами, распределение наших
данных может помочь нам выявить, что в данной диаграмме типично, что необычно, а что следует рассмотреть внимательнее.
Важность симметрии
В природе симметрия встречается повсюду. Разнообразная симметрия присуща животным и растениям. Скажем, и собачья морда, и дубовый лист в равной степени
обладают так называемой зеркальной симметрией — можно сказать, что одна сторона "отражает" другую. В популяциях организмов также встречается симметрия в
распределении определенных характеристик — например, роста особей или длины
крыльев. Гистограмма позволяет наблюдать эту симметрию, показывая частоту
встречаемости тех или иных показателей роста и длины крыльев в пределах популяции. Классический пример такого рода приведен на рис. 9.3, где показана длина
крыльев обыкновенной мухи на материале особей, измеренных командой биологов
в середине XX века7.
Симметричная колоколообразная кривая, показанная на рис. 9.3, иногда также именуется "нормальным", "стандартным" или "гауссовским" распределением. Если вы
когда-либо видели "кривую" академических оценок, то это было распределение, в
которое вписываются оценки всего вашего потока. По краям распределения находятся немногочисленные отличники и самые неуспевающие, а большинство значений сконцентрировано в середине.
Но мощь гауссовского распределения не только в его красивых очертаниях, а в
том, что можно сделать, ориентируясь на этот контур. Датасеты, демонстрирующие гауссовские распределения, можно как описывать, так и сравнивать друг с
другом такими способами, какие неприменимы к несимметричным распределени7
См. датасет под названием A Morphometric Analysis of Ddt-Resistant and Non-Resistant House Fly Strains,
авторы Robert R. Sokal и Preston E. Hunter, https://doi.org/10.1093/aesa/48.6.499 — в нем представлены
соответствующие данные.
338
|
Глава 9
ям, поскольку в гауссовском распределении можно осмысленно вычислить два
показателя: стандартное отклонение, количественно выражающее тот числовой
диапазон, в котором находится большинство значений данных, и показатель zоценки каждой точки данных, описывающий расстояние этого значения от среднего в терминах стандартных отклонений. Поскольку гауссовским распределениям присуща фундаментальная симметрия, можно пользоваться стандартным отклонением и z-оценкой для сравнения двух множеств функционально подобных
данных, даже если у них отличается шкала. Например, если оценки студентов
укладываются в гауссовское распределение, то можно вычислить и сравнить
z-оценки отдельных студентов (то есть, их успеваемость на фоне потока), даже
среди разных курсов и для разных преподавателей, критерии оценки у которых
могут отличаться. Иными словами, даже если у одного преподавателя студент
получает оценки от 90 до 100 баллов, а у другого — от 70 до 80 баллов, то, если
оба множества студентов описываются истинно гауссовским распределением, то
все равно можно определить, какие студенты на всех потоках успевают лучше
всех либо нуждаются в помощи. Эту информацию никак не узнать из номинальных оценок (например, 74 или 92).
Рис. 9.3. Распределение длин мушиных крыльев
Эти характеристики также подсказывают, как подходить к измерению центральной
тенденции и выбросов. Например, в "идеальном" гауссовском распределении среднее и медианное значения будут равны. Более того, z-оценка значения позволяет
быстро определить, что в данном значении типично, а что необычно, поскольку
четко определен процент точек данных, у которых ожидается конкретное zзначение. Запутались? Не волнуйтесь. Эта информация, как и любые сложные отношения между данными, станет гораздо понятнее, если ее визуализировать.
Введение в анализ данных
|
339
Как показано на рис. 9.48, если распределение ваших данных является гауссовским,
то более 2/3 точек данных (34,1% + 34,1% = 68,2%) находится в пределах одного
стандартного отклонения (часто обозначаемого, как здесь, греческой буквой σ) одного среднего. Еще 27,2% находится между одним и двумя стандартными отклонениями от среднего, а последние 4,2% отделены от среднего на 23 стандартных отклонения. Таким образом, при гауссовском распределении 99,7% всех значений
находятся в пределах 3 стандартных отклонений от среднего.
Рис. 9.4. Гауссовское распределение, демонстрирующее,
какой процент значений приходится на 1-е, 2-е и 3-е стандартные отклонения (σ) от среднего
И что же? Как вы помните, одна из фундаментальных целей анализа данных заключается в том, чтобы понять, какие значения "типичны" для нашего датасета, а
какие по-настоящему экстремальны. Тогда как среднее и медианное значения позволяют наскоро прикинуть "типичное" значение данного датасета, такие показатели, как стандартное отклонение и рассчитываемые на его основе z-значения, помогают систематически оценивать, какие значения могут или не могут оказаться понастоящему необычными.
Неудивительно, что вычислить эти значения при помощи Python не составляет труда. Воспользовавшись либо pandas, либо статистической библиотекой, можно быстро найти, каково стандартное отклонение (σ) нашего датасета, а затем на его основе провести по нашей гистограмме линии там, где находятся соответствующие
z-значения. Здесь будем основываться на данных, из которых был сгенерирован
рис. 9.3, как показано в листинге 9.2.
Листинг 9.2. wing_length_with_sd.py
# Библиотека `pandas` для считывания наших данных
import pandas as pd
# Библиотека `seaborn` для встроенных тем и типов диаграмм
import seaborn as sns
8
CC BY 2.5, автор M. W. Toews. Краткое описание см. на https://creativecommons.org/licenses/by/2.5, с
использованием Wikimedia Commons.
340
|
Глава 9
# Библиотека `matplotlib` для кастомизации визуальных деталей
import matplotlib.pyplot as plt
# Библиотека `statistics` для легкого вычисления статистических показателей
import statistics
# считывание наших данных
wing_data = pd.read_csv('wing_length - s057.csv')
# устанавливаем базовую цветовую тему для нашей визуализации
sns.set_theme(style="white")
# создаем гистограмму, позволяющую `seaborn` выбирать задаваемые по умолчанию
# значения "bin"
wing_plot = sns.histplot(data=wing_data, x="wing_length (0.1mm)", kde="True") //
# вычисляем стандартное отклонение, воспользовавшись методом `stdev()` из библиотеки
# `statistics`
sd = statistics.stdev(wing_data['wing_length (0.1mm)'])
//
# получаем для нашей гистограммы минимальное и максимальное значения по оси y
y_axis_range = wing_plot.get_ylim()
# проводим сплошную линию, соответствующую среднему значению
mean = wing_data['wing_length (0.1mm)'].mean()
wing_plot.vlines(mean, 0, y_axis_range[1], color='gray', ls='-')
# наносим граничные линии трех стандартных отклонений по обе стороны от среднего
for i in range(-3,4): //
# находим актуальное граничное значение
z_value = mean + (i*sd)
# не отрисовываем вторую линию поверх линии среднего значения
if z_value != mean:
# проводим прерывистую линию из точек, соответствующую каждому граничному значению
wing_plot.vlines(z_value, 0, y_axis_range[1], color='gray', ls=':')
# выводим график!
plt.show()
Каждый "bin" — это диапазон конкретных данных, которые будут объединены в
один столбец гистограммы; именно благодаря параметру kde в нашей визуализации
появляется сглаженная линия. Эта линия аппроксимирует паттерн, который мы бы
увидели, будь в нашем датасете бесконечное количество точек данных.
Введение в анализ данных
|
341
Здесь у нас также мог бы использоваться метод std() из библиотеки pandas:
wing_data['wing_length(0.1mm)'].std().
Не забывайте, что наш цикл закончится прежде, чем методу range() будет предоставлено второе значение. Поэтому, чтобы получить три положительных значения,
устанавливаем в качестве второго значения 4. Если начинать с отрицательного значения, то, фактически, на первом шаге мы будем вычитать из среднего — мы будем
так делать, поскольку хотим захватить значения, расположенные как выше, так и
ниже среднего.
Просто просмотрев вывод листинга 9.2, можно подумать: "Отлично, значит, мы
провели несколько линий по множеству данных о багах. Как это поможет нам при
интерпретации реальных данных?" В конце концов, тот пример с мушиными
крыльями, который мы взяли в качестве прототипа, обладавший гауссовским распределением, не слишком похож на вывод, который мы получили, строя диаграмму
данных по PPP-ссудам. График ссуд отчетливо асимметричен, и именно такой рисунок, вероятно, будет у большинства наших данных.
Что же делать, когда в распределении наших данных отсутствует симметрия? Мы
уже знаем, как найти "середину" асимметричного распределения, такого, как в листинге 9.1: нужно вычислять медианное значение, а не среднее. Но как же находить
экстремальные значения? Поскольку асимметричные, они же скошенные распределения, понятно, симметрией не обладают, у них нет единого "стандартного" отклонения, а также для них нельзя рассчитать z-значения. Но мы по-прежнему можем с
пользой подразделить асимметричный датасет таким образом, что нам удастся рассмотреть в нем потенциально необычные или экстремальные значения.
Этот процесс подразделения, как и нахождение медианы, на самом деле очень
прост. Сначала находим в отсортированном датасете срединное значение — иными
словами, медиану. Далее рассматриваем сначала одну, а затем вторую половину
множества, как если бы они были отдельными датасетами, и находим их медианные значения. Медиана нижней половины традиционно обозначается Q1, а медиана
верхней половины — Q3. На данном этапе мы уже разделили наш датасет на четыре части, или квартили, и в каждом квартиле содержится равное количество значений данных.
Что это нам дает? Напомню, что серьезная часть той информации, которую сообщают z-значения, заключается в том, каков процент точек данных, значения которых являются схожими. Например, из рис. 9.4 понятно, что точка данных с zзначением 0,75 (как и следовало ожидать) отстоит менее чем на одно стандартное
отклонение от медианы, — и мы знаем, что то же самое будет истинно примерно
для 68,2 % данных во всем датасете. Поделив данные на квартили, мы избрали
примерно такой же путь. Например, любое значение в нашем датасете, которое в
числовом выражении меньше значения из Q1, по определению будет меньше, чем
примерно 75% значений имеющихся у нас данных.
Все же реально нас интересуют способы, которыми можно было бы идентифицировать потенциально необычные значения. Знание, что величина больше — или
342
|
Глава 9
меньше — чем 75 % остальных данных — это уже что-то, но едва ли такое значение будет экстремальным. Идентифицировать границы наших квартилей также
будет недостаточно.
К счастью, можно воспользоваться нашими значениями Q1 и Q3, чтобы вычислить
так называемые верхнюю границу и нижнюю границу нашего датасета. Если подразумевалось, что наше распределение данных было гауссовским, то эти границы
почти идеально совпадут со значениями, расположенными в трех стандартных отклонениях выше и ниже медианы. Так что мы ориентируемся на эти границы именно потому, что наши данные не являются гауссовскими. Я привожу здесь это сравнение, чтобы проиллюстрировать: верхнюю и нижнюю границу можно использовать для выявления экстремальных значений в асимметрично распределенном
датасете.
Расчет верхней и нижней границы весьма прост, как и нахождение медианы. Для
начала находим значение, именуемое "межквартильный размах"9 (IQR), — вычурное название для числовой разницы между значениями Q3 и Q1. Затем мы умножаем это значение на 1,5, вычитаем его из Q1 — и так получаем нижнюю границу.
Чтобы узнать верхнюю границу, складываем вышеупомянутое произведение и Q3.
Вот и все!
IQR (межквартильный размах) = Q3 — Q1
Нижняя граница = Q1 — (1,5 x IQR)
Верхняя граница = Q3 + (1,5 x IQR).
При гауссовском распределении значения верхней и нижней границы будут находиться на три стандартных отклонения выше или ниже медианы — но значит ли
это, что любые значения за пределами верхней или нижней границы являются выбросами? Нет. Но, найдя эти границы, мы можем сузить область, в которой следует
искать выбросы. Что не менее важно, эти меры помогают нам понять, почему некоторые значения выбросами не являются, даже если на уровне чисел может показаться, что они весьма отличаются от "типичного" или "ожидаемого" значения, если судить по среднему или медианному.
Для примера давайте вернемся к нашим данным по PPP-ссудам. Ссуда в $1 млн кажется очень большой, даже если — как и в нашем случае — нас интересуют только
ссуды в $ 150 000 и выше. Но в самом ли деле ссуда в $1 млн необычна? Вот где
нам могут по-настоящему пригодиться наши показатели центральной тенденции и
дисперсии, в данном случае — медиана, квартили, а также значения верхней и
нижней границы. Давайте посмотрим, какой вид примет наша гистограмма, когда в
нее добавятся эти значения (см. листинг 9.3), и подумаем об этом.
Листинг 9.3. ppp_loan_central_and_dist.py
# `pandas` для считывания и оценки наших данных
import pandas as pd
9
Или интерквартильный размах.
Введение в анализ данных
|
# `seaborn` понадобится нам, так как в нее встроены нужные темы и типы диаграмм
import seaborn as sns
# `matplotlib` для кастомизации визуальных деталей
import matplotlib.pyplot as plt
# считывание данных
ppp_data = pd.read_csv('public_150k_plus_221.csv')
# установка базовой цветовой темы для визуализации
sns.set_theme(style="whitegrid")
# use the built-in `mean()` and `median()` methods in `pandas
mean = ppp_data['CurrentApprovalAmount'].mean()
median = ppp_data['CurrentApprovalAmount'].median()
# Q1 — это такое значение в нашем датасете, левее которого находится # 25%
# всех считанных данных
Q1 = ppp_data['CurrentApprovalAmount'].quantile(0.25)
# Q3 — это такое значение в нашем датасете, левее которого находится # 75%
# всех считанных данных
Q3 = ppp_data['CurrentApprovalAmount'].quantile(0.75)
# IQR — это разница между значениями Q3 и Q1
IQR = Q3-Q1
# Теперь вычисляем верхнюю и нижнюю границы
lower_bound = Q1 - (1.5*IQR)
upper_bound = Q3 + (1.5*IQR)
# при помощи `seaborn` вычерчиваем гистограмму
approved_loan_plot = sns.histplot(data=ppp_data, x="CurrentApprovalAmount")
# получаем минимальное и максимальное значение по оси y в гистограмме
y_axis_range = approved_loan_plot.get_ylim()
# обозначаем среднее серой линией
approved_loan_plot.vlines(mean, 0, y_axis_range[1], color='gray', ls='-')
# другие линии черные (медиана сплошная, остальные точечные)
approved_loan_plot.vlines(median, 0, y_axis_range[1], color='black', ls='-')
approved_loan_plot.vlines(lower_bound, 0, y_axis_range[1], color='black', ls=':')
approved_loan_plot.vlines(Q1, 0, y_axis_range[1], color='black', ls=':')
approved_loan_plot.vlines(Q3, 0, y_axis_range[1], color='black', ls=':')
approved_loan_plot.vlines(upper_bound, 0, y_axis_range[1], color='black', ls=':')
# отобразим, что у нас получилось!
plt.show()
343
344
|
Глава 9
Как видите на увеличенном представлении получившегося графика (показанного на
рис. 9.5), в принципе, не подтверждается гипотеза о том, будто ссуда в $1 млн является из ряда вон выходящей; эта сумма оказывается значительно ниже верхней границы, рассчитанной нами для данного датасета, хотя сумма этой ссуды выше, чем у
трех четвертей ссуд, оформленных к данному моменту (так как отметка в $1 млн,
сейчас обозначенная на оси x этой диаграммы как 1.0 1e6, находится правее линии
Q3). Тем не менее это не так много, чтобы каждую ссуду в $1 млн требовалось дополнительно проверять. Как минимум, если и начинать проверки, то не с такой
суммы.
Рис. 9.5. Подробности по гистограмме с суммами PPP-ссуд с указанием медианы, квартилей,
границами (нанесены черным) и медианой (нанесена серым)
Итак, где именно нам отыскивать потенциально интересные паттерны данных?
Прямо перед нами, на диаграмме, которая у нас уже есть. Ведь, хотя мы могли бы
поискать более сложные статистические показатели для расчета и оценки, даже
эта простейшая визуализация открывает нам в данных некоторые занятные закономерности. Первая, о которой стоит упомянуть, — пусть даже, чтобы лишний
раз убедиться в том, что мы не ошиблись с выбором статистических показателей,
— в том, что среднее от этого датасета находится в распределении практически
на той же позиции, что и значение Q3. Если бы у нас были какие-либо сомнения,
что предпочесть в качестве показателя центральной тенденции — среднее или
медиану — то вышеупомянутый факт их бы не оставил. Что еще заметно в пред-
Введение в анализ данных
|
345
ставлении данных на рис. 9.5, особенно, если прокрутить вправо, — так это интересные маленькие всплески в наших данных, означающие, что ссуду на определенную сумму одобряли относительно часто10. Учитывая, как ясно эти значения
проступают в паттернах окружающих данных, далее, пожалуй, стоит присмотреться именно к этим значениям.
Подсчет "кластеров"
Допустим, вы идете по людной улице — и замечаете группу людей, собравшихся
на углу через дорогу. Что вы сделаете? На шумном проспекте, где большинство
пешеходов волнует, только как добраться из точки "А" в точку "B", не один-два
человека, а больше остановились в одном и том же месте в одно и то же время. Это
уже явно свидетельствует: что-то там происходит. "Повод" может быть самым разным: возможно, там играет уличный музыкант или кто-то продает особенно популярные снеки, либо стоит коробка с котятами11. Факт есть факт: наша зрительная
система настроена подмечать аномалии, именно потому, что отклонение от тренда
указывает на какие-нибудь, как минимум слегка неординарные события.
Вот почему визуализация данных так помогает при их анализе. Наши глаза и мозг
заточены и на быстрое восприятие паттернов, и на столь же быстрое улавливание
отклонений от них. Иногда просто угадать, почему именно складывается тот или
иной паттерн, иногда нет. Но любой предсказуемый паттерн, скажем, поток людей
на улице, колоколообразное распределение данных или распределение, образующее пологую кривую, — заслуживает изучения.
В случае, показанном на рис. 9.5 заметен целый диапазон таких отклонений от паттерна. Во-первых, резкая линия в левой части графика, выразительно напоминающая, что в нашем множестве данных содержатся только одобренные ссуды на сумму $150 000 и более, а не все без исключения ссуды, одобренные по данной программе. Если мы упустили это из виду, то нам об этом хорошо напомнит четкая и
очевидная линия "отреза" ближе к левому краю.
Но здесь прослеживается и еще одна совокупность нарушений паттерна: небольшие всплески в окрестности конкретных точек по оси x, в районе таких точек данных, как $2 млн. Откуда они берутся? Просмотрев нашу гистограмму, можем с определенностью сказать, что подобные всплески встречаются с интервалом примерно в $500 000, особенно по мере увеличения сумм ссуд. В некоторой степени эта
тенденция может объясняться естественным тяготением сумм к круглым числам:
если вы запросите $1 978 562,34, почему бы просто не "округлить" эту сумму до
$2 млн? Разумеется, вы в таком случае получите на $21 437,66 больше, чем вам
требуется, — а для большинства из нас это, в любом случае, много денег. Учитывая, что PPP-ссуды выдаются на обеспечение специфических расходов, должно по-
10
11
Особенно относительно сумм кредитов, находящихся немного выше или ниже этих значений.
И так бывает.
346
|
Глава 9
казаться немного странным, что так много ссуд, почти 2000 из общего количества,
судя по получившемуся графику, оказывается, составляют ровно по $2 млн.
Что же здесь происходит? Вот где нам понадобится провести дополнительное исследование, чтобы эффективно интерпретировать то, что мы видим в данных. Мой
опыт подсказывает, что первым делом нужно посмотреть правила выдачи PPP-ссуд
и попытаться по ним понять, почему запросы на $2 млн так популярны. Например,
могут ли $2 млн оказаться минимальной или максимальной допустимой суммой,
обусловленной конкретными аспектами бизнеса или той цели, на которую запрашивается поддержка?
Немного поискав на сайте https://www.sba.gov/funding-programs/loans/covid-19relief-options/paycheck-protection-program/second-draw-ppp-loan Управления по
делам малого бизнеса (SBA), находим как минимум часть ответа:
Для большинства заемщиков максимальная сумма ссуды на второй раунд
помощи в 2,5 раза выше средних ежемесячных затрат на рабочую силу за
2019 г. или 2020 г. до $2 млн. Для заемщиков из сектора "Гостиницы, жилье
и общественное питание" (источник: NAICS 72) максимальная сумма ссуды
на второй раунд помощи в 3,5 раза выше средних ежемесячных затрат на
рабочую силу за 2019 г. или 2020 г. до $2 млн.
Поскольку $2 млн — потолок практически для всех отраслей бизнеса, для которых
применим второй раунд помощи по программе PPP, в том числе для тех случаев,
где исходно предусматривались более серьезные суммы, — объяснимо, почему так
велик кластер ссуд, одобренных ровно на $2 млн.
Этот "ответ", конечно же, оставляет больше вопросов, чем дает ответов. Согласно
документации, сумма $2 млн была потолком для второго раунда ссуд по программе
PPP; ссуды, выданные в рамках первого раунда, могли достигать до $10 млн. Если
так много компаний запрашивают максимально возможную ссуду именно во втором раунде, то это значит, что многие компании: 1) уже получили ссуду в первом
раунде и 2) ссуда, полученная ими в первом раунде, могла даже превышать $2 млн.
Ведь во втором раунде им пришлось округлять ссуду вниз до $2 млн, если в первом
раунде они получили одобрение на более крупную ссуду. Иными словами, можно
ожидать, что те компании, которые запрашивали во втором раунде сумму ровно в
$2 млн, — как раз таковые, которым также были одобрены наибольшие общие суммы облегчения суммы задолженности по программе PPP. Естественно, если они
действительно получили самые большие денежные мешки, то мы (и многие другие)
определенно хотим об этом знать!
Вопрос за $2 миллиона
Чтобы понять, какие общие характеристики могут быть у компаний, запросивших
ссуды по $2 млн во втором раунде программы PPP (и есть ли они вообще), сначала
нужно эффективно вычленить записи о них из нашего датасета. Как это можно сделать? Что ж, нас интересуют компании, о которых известно, что им одобрили более
Введение в анализ данных
|
347
одной ссуды. Это значит, что соответствующее имя BorrowerName должно фигурировать в наших данных более одного раза. Мы также знаем, что до 13 января 2021
года никакого "второго раунда" выдачи ссуд не было. Сопоставив вместе два этих
факта, мы, пожалуй, можем воспользоваться нашими навыками выпаса данных и
уверенно идентифицировать, какие компании во втором раунде кредитования запросили ссуду ровно на $ 2 млн.
Чтобы этого добиться, давайте выполним в нашем датасете пару ключевых преобразований:
1. Создадим для каждого займа новый столбец с меткой first_round или maybe_second,
в зависимости от того, когда был выдан займ — до или после 13.01.2021. Притом, что мы не можем быть уверены, что все займы, выданные после этой даты,
относятся ко "второму раунду", мы можем быть уверены, что все займы, выданные до этой даты, относятся к "первому раунду".
2. Посмотрим, есть ли в нашем датасете дублирующиеся записи. Для каждого
одобренного займа создается отдельная запись, поэтому если для одной и той же
записи одобрено два займа, это значит, что данная компания будет фигурировать в наших записях дважды.
Логика здесь такова: если название конкретной компании дважды попадется в наших данных, и у этих записей будут отличаться "округляющие" метки, то это означает, что эта компания получила одобрение на два разных займа.
Как обычно, воспользуемся в данном случае библиотеками Python. Как и ранее, нам
потребуется воспользоваться pandas, но здесь мы поработаем и с другой библиотекой, numpy, в которой содержится множество полезных функций для работы со
списками и массивами (на самом деле, pandas при работе серьезно опирается "под
капотом" на numpy). Кроме того, здесь я снова подтяну seaborn и matplotlib, чтобы у
нас была возможность сгенерировать визуализации, которые помогли бы нам в
оценке нашего датасета по мере его развития.
Хотя все, что мы пытались проделать с этими данными, в концептуальном отношении тривиально, этот анализ делается с привлечением выпаса, а выпас данных —
процесс многоэтапный, как показано в листинге 9.4.
Листинг 9.4. who_got_2_loans_by_date.py
# `pandas` для загрузки и преобразования данных
import pandas as pd
# `seaborn` для визуализации
import seaborn as sns
# `matplotlib` для детальной поддержки визуализаций
import matplotlib.pyplot as plt
# `numpy` для манипуляций над массивами и списками
import numpy as np
348
|
Глава 9
# загрузка наших данных
ppp_data = pd.read_csv('public_150k_plus_borrower_fingerprint_a.csv') //
# преобразуем столбец `DateApproved` в конкретный тип данных для даты и времени
ppp_data['DateApproved'] = pd.to_datetime(ppp_data['DateApproved']) //
# создаем переменную, в которой будет содержаться дата начала второго раунда
second_round_start = pd.to_datetime('2021-01-13')
# будем считать сегодняшнюю дату как "верхний" предел для всех возможных ссуд
# в рамках второго раунда
todays_date = pd.to_datetime('today')
# Используем дату 1/1/2020 в качестве "нижнего" предела, поскольку программа PPP была
# запущена позже
program_start = pd.to_datetime('2020-01-01')
# передаем наши граничные значения и метки категорий функции `cut()` из библиотеки
# pandas
loan_round = pd.cut(ppp_data.DateApproved,
bins=[program_start,second_round_start, todays_date],
labels=['first_round', 'maybe_second'])//
# вставляем новый столбец на той позиции, которую укажем
ppp_data.insert(2,'Loan Round',loan_round)
# эта "сводная таблица" вернет Ряд, демонстрирующий, сколько раз
# конкретный 'BorrowerNameFingerprint' фигурирует в датасете
loan_count = ppp_data.pivot_table(index=['BorrowerNameFingerprint'], aggfunc='size')
# Преобразуем наш Ряд в DataFrame и даем ему имя
loan_count_df = loan_count.to_frame('Loan Count')
//
# при помощи метода `describe()` выводим на экран сводную статистику
print("Description of duplicate borrower table:")
print(loan_count_df.describe())
//
Этот файл мы сгенерировали, выполнив процесс фингерпринтинга для BorrowerName,
как описано далее во врезке "Нахождение фингерпринта".
Нам требуется узнать, какие ссуды были одобрены до 13 января 2021 года. Быстрейший способ сделать это — преобразовать наши строки DateApproved в "реальные" даты и сравнить с ними даты выдачи ссуд.
При помощи функции cut() из библиотеки pandas можно создать новый столбец
на основе имеющегося, применив к имеющемуся столбцу граничные значения и
Введение в анализ данных
|
349
метки. В данном случае мы размечаем все столбцы в зависимости от того, когда
именно была выдана ссуда: до или после 13 января 2021 года.
Это делается для удобства, поэтому здесь можно воспользоваться методом
describe().
Ожидается, что максимальное значение в таблице будет равно 2, так как ни одной компании не разрешено получать более двух ссуд по программе PPP.
Если вы запустите код из листинга 9.4, и примерно за минуту ничего не произойдет —
не отчаивайтесь. У меня на Chromebook на выполнение этого скрипта уходит от 40
до 90 секунд (в зависимости от того, сколько еще приложений под Linux у меня
одновременно работает)12. Когда скрипт закончит работу, вы увидите на экране
примерно следующий вывод:
Description of duplicate borrower table:
Loan Count
count 694279.000000
mean 1.104022
std 0.306489
min 1.000000
25% 1.000000
50% 1.000000
75% 1.000000
max 12.000000
Представляется, что в этой первой попытке чего-то… не хватает. По выводу команды .describe() можно быстро собрать почти всю обобщающую статистику, которая нас интересует (здесь Q1, медиана и Q3 размечены в соответствии с процентным количеством значений, которые будут фигурировать в диаграмме левее той
или иной отметки — т. е. 25%, 50% и 75%, соответственно). Эти значения подсказывают, что более одной ссуды получили менее 25% всех компаний (в противном
случае значение для 75% было бы больше 1) — и это логично. Но максимальное
значение заставляет поволноваться, поскольку правила PPP, по-видимому, не позволяют одной компании получить более двух ссуд, а это гораздо меньше 12! Давайте внимательнее присмотримся к этой ситуации, добавив к примеру 9.4 код,
приведенный в примере 9.5, — и посмотрим, что получится.
Листинг 9.5 who_got_2_loans_by_date.py (продолжение листинга 9.4)
# для начала отсортируем DataFrame с количеством ссуд от наибольшего к наименьшему
sorted_loan_counts = loan_count_df.sort_values(by=['Loan Count'], ascending=False)
12
Если их слишком много, то в выводе появится надпись «Killed». Это признак того, что вам нужно
закрыть некоторые приложения или перейти в облако.
350
|
Глава 9
# Создадим новый DataFrame *только* с теми компаниями, которые получили более
# двух ссуд
more_than_two = sorted_loan_counts[sorted_loan_counts['Loan Count'] > 2]
# выводим по одному экземпляру каждой компании, попадающей в категорию `more_than_two`
print("Businesses that seem to have gotten more than 2 loans:")
print(more_than_two.shape)
print("Number of businesses that appear to have gotten precisely 2 loans:")
precisely_two = sorted_loan_counts[sorted_loan_counts['Loan Count'] == 2]
print(precisely_two.shape)
Теперь получаем дополнительный вывод, показанный здесь:
Businesses that seem to have gotten more than 2 loans:
(58, 1)
Number of businesses that appear to have gotten precisely 2 loans:
(72060, 1)
Это говорит о том, что существует (относительно) немного компаний, которым
может быть одобрено более двух ссуд, и такие случаи, пожалуй, можно связать с
сочетанием выбранного нами подхода с фингерпринтингом (комбинация
BorrowerName, BorrowerCity, и BorrowerState), а также возможности, что в одном городе
найдется несколько точек одной и той же франшизы, которой было одобрено финансирование по программе PPP13. Мы не будем тратить свои ресурсы на поиск
подробностей об этих компаниях. В любом случае, их достаточно мало, чтобы существенно изменить результаты анализа. По крайней мере, второй результат, показывающий, что 72 060 отдельных предприятий получили ровно два кредита, пока
кажется разумным, так как это определенно менее 25% общего датасета. Следовательно, такой результат соответствует сводной статистике, полученной из DataFrame Loan Count (поскольку значение Q3 все еще равно 1, а это означает, что менее
25% всех названий предприятий появились в нашем датасете более одного раза).
Конечно, это все еще только оценка. Было бы гораздо лучше, если бы у нас был
более официальный подсчет кредитов второго раунда, с которым мы могли бы работать. Как отмечалось в конце главы 6, Управление по делам малого бизнеса действительно выпустило Официальный словарь данных14 для данных по ссудам PPP.
И хотя он не содержит всей информации, на которую мы могли бы надеяться, словарь указывает, что поле ProcessingMethod различает кредиты первого (PPP) и второго
(PPS) раундов. Давайте посмотрим на данные с этой точки зрения и сравним их с
оценкой на основе сопоставления имен, добавив код из листинга 9.6 далее в нашем
файле.
13
Подробнее см. на https://sba.gov/document/support-faq-ppp-borrowers-lenders и
https://sba.gov/document/support-sbafranchise-directory.
14
https://data.sba.gov/dataset/ppp-foia/resource/aab8e9f9-36d1-42e1-b3ba-e59c79f1d7f0
Введение в анализ данных
|
351
Листинг 9.6. who_got_2_loans_by_date.py (опять продолжение)
# используем значение `ProcessingMethod` для идентификации ссуд второго раунда
pps_loans = ppp_data[ppp_data['ProcessingMethod'] == 'PPS']
# выводим на экран значение `shape` из этого объекта DataFrame, чтобы узнать,
# сколько у нас компаний
print("Number of loans labeled as second round:")
print(pps_loans.shape)
Повторный запуск нашего скрипта дает дополнительный результат:
Number of loans labeled as second round:
(103949, 52)
Вот это да! Даже используя слишком слабый метод фингерпринта, мы все равно не
смогли найти более 300 000 компаний, получивших обе ссуды. Что же делать?
Прежде всего, признайте, что это не необычная ситуация. Мы имеем дело примерно с 750 000 записей данных, каждая из которых представляет собой комбинацию
данных, введенных несколькими лицами, включая заемщика, кредитора и, возможно, SBA. Тот факт, что существует так много расхождений, на самом деле не удивителен (я проиллюстрирую некоторые из них в следующей врезке), но еще не все
потеряно. Помните, что изначально нас интересовали те предприятия, которые получили именно $2 млн на второй раунд ссуды, что, скорее всего, представляет собой лишь малую частью всех предприятий, получивших две ссуды. Мы все еще
можем продолжить эту часть анализа, чтобы: 1) проверить, насколько эффективной
была наша оценка ссуд второго раунда на основе даты, и 2) посмотреть, что мы
можем узнать о конкретной подгруппе предприятий, получивших ровно $2 млн во
втором раунде.
Нахождение фингерпринта
Поскольку в предыдущих главах описано, как использовать библиотеку
fingerprints, я пропустила точный процесс, применяемый для подготовки рассматриваемых здесь данных15. Однако, как всегда, эффективность процесса сопоставления зависит не только от самого алгоритма фингерпринтинга, но и от
данных, к которым он применяется. Хотя сочетание BorrowerName, BorrowerCity и
BorrowerState явно далеко от совершенства, я остановилась на нем только после
того, как сначала попыталась сопоставить ссуды на основе значения
BorrowerName непосредственно, а затем на сочетании фингерпринтинга значений
BorrowerName и BorowerZip. В обоих случаях я смогла найти совпадения менее
чем для половины кредитов второго раунда.
15
Но вы можете найти его в файле ppp_fingerprint_borrowers.py.
352
|
Глава 9
Интересно, как выглядят различия в данных? Вот один из примеров:
Как видите, хотя поле BorrowerName аналогично, оно не такое же, как значение в
BorrowerAddress или капитализация BorrowerCity. Почтовые индексы одинаковые, но многие другие столбцы, такие как Gender, Veteran и даже LMIIndicator, не
совпадают. Вот вам и реальные данные!
На данном этапе мы собираемся использовать информацию из столбца
PaymentProcessingMethod для подтверждения нашей предыдущей работы с использованием сопоставления имен и оценки раунда кредитования на основе даты. Для
этого произведем слияние DataFrame Loan Count обратно с исходным датасетом. Затем отберем только те ссуды на сумму $2 млн, которые, по нашим оценкам на основе даты выдачи, были ссудами второго раунда. Наконец, мы сравним это количество ссуд с количеством ссуд на сумму $2 млн, которые, как мы узнали, были повторным кредитованием, основываясь на их значении ProcessingMethod из PPS.
Очевидно, что это означает добавление дополнительного кода в наш файл, как показано в листинге 9.7.
Введение в анализ данных
|
353
Листинг 9.7. who_got_2_loans_by_date.py (снова продолжение)
# сколько ссуд в нашей производной выборке данных было одобрено именно
# на сумму $2 млн во время (возможно) второго раунда?
# слияние `loan_count_df` с исходными данными для отслеживания компаний,
# которые мы пометили как имеющие ровно два кредита
ppp_data_w_lc = pd.merge(ppp_data, loan_count_df,
on=['BorrowerNameFingerprint'], how='left')
# теперь получим *все* записи о названиях компаний, которые мы связывали
# с двумя ссудами
matched_two_loans = ppp_data_w_lc[(ppp_data_w_lc['Loan Count'] == 2)]
# выберем ссуды со значением $2 млн, которые могут быть вторыми (`maybe_second`)
maybe_round2_2M = matched_two_loans[(matched_two_loans[
'CurrentApprovalAmount'] == 2000000.00) &
(matched_two_loans[
'Loan Round'] == 'maybe_second')]
print("Derived $2M second-round loans:")
print(maybe_round2_2M.shape)
# выберем те ссуды, которые, как мы *знаем*, являются ссудами второго
# раунда со значением $2 млн
pps_got_2M = pps_loans[pps_loans['CurrentApprovalAmount'] == 2000000.00]
print("Actual $2M second-round loans:")
print(pps_got_2M.shape)
Добавив этот код в основные файлы, мы получим еще несколько строк вывода:
Derived $2M second-round loans:
(1175, 53)
Actual $2M second-round loans:
(1459, 52)
Если сравнить эти результаты с предыдущими, то кажется, что дела обстоят немного лучше. По всем кредитам у нас есть совпадение 72 060 из 103 949 реальных кредитов второго раунда, или около 70%. Среди организаций, получивших одобрение
на получение кредитов в размере $2 млн во втором раунде, мы нашли 1 115 из
1 459, или около 80%.
Что же можно сказать о предприятиях, получивших $2 млн во втором раунде? Мы
не можем сказать ничего со 100% уверенностью, пока не найдем совпадения для
тех 284 компаний, у которых значение BorrowerNameFingerprint не совпадает между
ссудами первого и второго раунда. Необходимо понять, что можно обнаружить при
просмотре нашей 80-процентной выборки.
354
|
Глава 9
Для этого я собираюсь выполнить следующие действия16:
1. Найти все уникальные значения BorrowerNameFingerprint для компаний, которые
точно получили кредиты второго раунда на сумму $2 млн.
2. Создать DataFrame (biz_names_df) на основе этого списка и заполнить его значением флага 2Mil2ndRnd.
3. Объединить этот DataFrame с моим датасетом и использовать значение флага
для получения всех записей о ссудах для этих компаний (как первого, так и второго раунда).
4. Провести базовый анализ того, сколько денег было одобрено этим предприятиям
в обоих раундах, и визуализировать эти суммы, сравнивая официальное обозначение второго раунда (то есть ProcessingMethod == 'PPS') с нашей производной
категорией на основе даты.
И, конечно, теперь, когда я выписала в виде списка шаги, которые должен выполнить мой скрипт (это именно то, что вам необходимо записать в дневник данных
и/или план программы), осталось только запрограммировать его под существующую задачу. Для ясности я поместила этот код во второй файл скрипта, его полный
код показан в листинге 9.8.
Лиситнг 9.8. who_got_2M_with_viz.py
# `pandas` для загрузки/преобразования данных
import pandas as pd
# `seaborn` для визуализации
import seaborn as sns
# `matplotlib` для поддержки детальной визуализации
import matplotlib.pyplot as plt
# `numpy` для работы с массивами/списками
import numpy as np
# загружаем наши данные
ppp_data = pd.read_csv('public_150k_plus_borrower_fingerprint_a.csv')
# преобразуем столбец `DateApproved` в актуальный тип данных datetime
ppp_data['DateApproved'] = pd.to_datetime(ppp_data['DateApproved'])
# создаем переменную для хранения даты начала второго раунда
second_round_start = pd.to_datetime('2021-01-13')
16
Обратите внимание, что я намеренно делаю это немного окольными путями. Это позволяет продемонстрировать еще несколько стратегий сбора данных и визуализации, но не стесняйтесь переделывать этот код для большей эффективности в качестве упражнения!
Введение в анализ данных
|
# обрабатываем сегодняшнюю дату, чтобы использовать ее в качестве "верхнего"
# предела для возможных ссуд второго раунда
todays_date = pd.to_datetime('today')
# используем 1/1/2020 в качестве "нижнего" лимита, так как это дата до запуска PPP
program_start = pd.to_datetime('2020-01-01')
# передаем наши границы и метки категорий в функцию `cut()` библиотеки pandas
loan_round = pd.cut(ppp_data.DateApproved,
bins=[program_start,second_round_start, todays_date],
labels=['first_round', 'maybe_second'])
# добавляем новый столбец в указанную позицию
ppp_data.insert(2,'Loan Round',loan_round)
# эта таблица "pivot table" вернет последовательность, показывающую количество
# раз, когда определенное значение 'BorrowerNameFingerprint' появляется в датасете
loan_count = ppp_data.pivot_table(index=['BorrowerNameFingerprint'],
aggfunc='size')
# преобразуем нашу последовательность в DataFrame и даем ей имя
loan_count_df = loan_count.to_frame('Loan Count')
# используем метод `describe()` для вывода сводной статистики
print("Description of duplicate borrower table:")
print(loan_count_df.describe())
# начнем с сортировки нашего DataFrame количества ссуд от наибольшего
# к наименьшему
sorted_loan_counts = loan_count_df.sort_values(by=['Loan Count'],
ascending=False)
# создаем новый объект DataFrame, содержащий *только* тех, у кого более
# двух кредитов
more_than_two = sorted_loan_counts[sorted_loan_counts['Loan Count'] > 2]
# выводим на экран по одному экземпляру каждого названия компании,
# которое встречается в `more_than_two`.
print("Businesses that seem to have gotten more than 2 loans:")
print(more_than_two.shape)
print("Number of businesses that appear to have gotten precisely 2 loans:")
precisely_two = sorted_loan_counts[sorted_loan_counts['Loan Count'] == 2]
print(precisely_two.shape)
# используем значение `ProcessingMethod` для идентификации ссуд второго раунда
pps_loans = ppp_data[ppp_data['ProcessingMethod'] == 'PPS']
355
356
|
Глава 9
# выводим на экран значение `shape` из этого объекта DataFrame, чтобы узнать,
# сколько у нас компаний
print("Number of loans labeled as second round:")
print(pps_loans.shape)
# сколько ссуд в нашей производной выборке данных было одобрено именно
# на сумму $2 млн во время (возможно) второго раунда?
# слияние `loan_count_df` с исходными данными для отслеживания компаний,
# которые мы пометили как имеющие ровно два кредита
ppp_data_w_lc = pd.merge(ppp_data, loan_count_df,
on=['BorrowerNameFingerprint'], how='left')
# теперь получим *все* записи о названиях компаний, которые мы связывали
# с двумя ссудами
matched_two_loans = ppp_data_w_lc[(ppp_data_w_lc['Loan Count'] == 2)]
# выберем ссуды со значением $2 млн, которые могут быть вторыми (`maybe_second`)
maybe_round2_2M = matched_two_loans[
(matched_two_loans['CurrentApprovalAmount'] == 2000000.00) &
(matched_two_loans['Loan Round'] == 'maybe_second')]
print("Derived $2M second-round loans:")
print(maybe_round2_2M.shape)
# выберем те ссуды, которые, как мы *знаем*, являются ссудами второго раунда
# со значением $2 млн
pps_got_2M = pps_loans[pps_loans['CurrentApprovalAmount'] == 2000000.00]
print("Actual $2M second-round loans:")
print(pps_got_2M.shape)
# отделим фингерпринты компаний, получивших одобренные кредиты второго раунда
# на $2 млн
biz_names = pd.unique(pps_got_2M['BorrowerNameFingerprint'])
# преобразуем этот список в DataFrame
biz_names_df = pd.DataFrame(biz_names, columns=['BorrowerNameFingerprint'])
# создаем новый массив той же длины, что и biz_names_df, и заполняем его
# значением флага
fill_column = np.full((len(biz_names),1), '2Mil2ndRnd')
biz_names_df['GotSecond'] = fill_column
# теперь объединим этот новый DataFrame из двух столбцов с нашим списком
# full_data, чтобы (надеемся) найти их ссуды первого раунда.
second_round_max = pd.merge(ppp_data_w_lc, biz_names_df,
on='BorrowerNameFingerprint')
Введение в анализ данных
|
357
# теперь все ссуды, имеющие общие фингерпринты с теми, которые получили
# максимальную сумму во втором раунде, должны содержать значение флага
# '2Mil2ndRnd'в столбце 'GotSecond'
second_max_all_loans = second_round_max[
second_round_max['GotSecond'] == '2Mil2ndRnd']
# мы ожидаем, что это в два раза больше, чем число предприятий, получивших
# кредиты второго раунда в размере $2 млн
print('Total # of loans approved for most orgs that got $2M for second round:')
print(second_max_all_loans.shape)
# сколько денег эти предприятия получили от PPP, в общей сложности?
total_funds = second_max_all_loans['CurrentApprovalAmount'].sum()
print("Total funds approved for identified orgs that could have " + \
"second-round max:")
print(total_funds)
# построим график наших данных с пометкой `Loan Round` на основе даты рядом
# с записями, разделенными `ProcessingMethod`. Получим ли мы те же результаты?
# установим тему seaborn
sns.set_theme(style="whitegrid")
# используем `matplotlib` `subplots()` для построения графиков рядом друг с другом
# используем `tuples` для последующего доступа к различным значениям subplots
fig, ((row1col1, row1col2)) = plt.subplots(nrows=1, ncols=2)
# построим гистограмму нашего анализа на основе дат
date_based = sns.histplot(data=second_max_all_loans, x='CurrentApprovalAmount',
hue='Loan Round', ax=row1col1)
# построим гистограмму нашего анализа на основе дат
data_based = sns.histplot(data=second_max_all_loans, x='CurrentApprovalAmount',
hue='ProcessingMethod', ax=row1col2)
# выводим график!
plt.show()
Запустив этот скрипт, мы получим все результаты предыдущих примеров, а также
еще несколько строк дополнительной информации:
Total # of loans approved for most orgs that got $2M for second round:
(2634, 54)
Total funds approved for identified orgs that could have second-round max:
6250357574.44
Сначала кажется, что что-то не так, потому что ожидалось, что общее количество
ссуд составит 2 × 1 175 = 2 350. Но помните, что мы сопоставляли ссуды на основе
358
|
Глава 9
того, получили ли компании одобрение на сумму ровно $2 млн во втором раунде, и
не смогли сопоставить 284 ссуд по значению BorrowerNameFingerprint.
Это означает, что у нас есть все кредиты второго раунда, но в этих цифрах не хватает 284 кредитов первого раунда. Другими словами, мы ожидаем получить (2 ×
1,175) + 284 = 2,634 — и получаем! Хорошо! Всегда приятно, когда что-то совпадает. Это означает, что наша "общая" цифра, хотя и не будет на 100% точной, представляет собой в определенной степени обоснованную оценку минимального значения общего количества ссуд, одобренных этой группе компаний в рамках PPP:
около $6 млрд.
Наконец, давайте посмотрим на визуализацию, показанную на рис. 9.6, которая
представляет собой вид графика, созданного скриптом. На этом графике мы можем
сравнить, как наша классификация Loan Round соответствует указанным PPS ссудам.
Это грубый (но все же полезный) способ проверки нашей работы, и результаты выглядят довольно хорошо17!
Рис. 9.6. Сумма в долларах большинства одобренных ссуд для компаний,
получивших два кредита PPP, в разбивке по раундам кредитования
Интересно, однако, что рис. 9.6 иллюстрирует и другое: похоже, что значительное
число компаний, получивших кредиты на сумму $2 млн во втором раунде, нарушают нашу предыдущую гипотезу о том, что компании, получившие кредиты на
сумму $2 млн во втором раунде, получили больше этой суммы в первом раунде,
когда лимиты были выше. Как обычно, отвечая на один вопрос, мы породили другой!
17
Если сравнить результаты численно, то можно заметить, что они идентичны по крайней мере для
нашей подгруппы компаний, получивших $2 млн во втором раунде.
Введение в анализ данных
|
359
И, конечно, уже проделанная работа даст нам фору на пути к ответу на этот вопрос.
Однако прежде чем приступить к следующему раунду вопросов и ответов, необходимо поговорить еще об одном важном компоненте анализа и интерпретации данных: пропорциональности.
Пропорциональный ответ
Представьте, что идете в кафе с друзьями. Вы недавно поели, поэтому заказываете
только напиток, но трое ваших друзей проголодались и заказали по полному обеду.
Как вы решаете, кто сколько должен оплатить, когда приносят счет? Большинство
из нас согласится, что наиболее разумным было бы вычислить — или хотя бы оценить — в каких пропорциях заказ каждого человека составляет общий счет, а затем
попросить каждого человека оплатить эту долю, а также такую же долю, скажем,
налога и чаевых.
Такая же логика применима и при анализе данных. В разд. "Вопрос за $2 миллиона"
мы рассмотрели общий объем средств, утвержденных для определенной группы
предприятий в рамках PPP, и хотя 6 млрд долларов звучит внушительно, нас, вероятно, должно больше интересовать то, как эти предприятия использовали предоставленные средства, а не абсолютное количество полученных ими денег. Поскольку программа PPP была разработана для того, чтобы сохранить людям заработную
плату, мы могли бы узнать, сколько денег получили эти предприятия по отношению к сохраненному ими количеству рабочих мест, — процесс, который я считаю
рационализацией данных18.
К счастью, процесс рационализации наших данных чрезвычайно прост: мы вычисляем отношение между двумя значениями данных путем деления одного числа на
другое. Например, если мы хотим узнать, сколько долларов на одно рабочее место
потратили компании, определенные в разд. "Вопрос за $2 миллиона", можно (после
некоторой проверки на вменяемость) разделить значение в PAYROLL_PROCEED на значение в JobsReported для каждой записи, как показано в листинге 9.9.
Листинг 9.9. dollars_per_job_2M_rnd2.py
# `pandas` для загрузки/преобразования данных
import pandas as pd
# `seaborn` для визуализации
import seaborn as sns
# `matplotlib` для кастомизации визуальных деталей
import matplotlib.pyplot as plt
18
У этого термина в мире бизнеса и статистики/науки о данных есть более конкретное определение —
пропорционализация, но оно звучит как-то неуклюже. Кроме того, мой вариант лучше соответствует
реальному процессу расчета!
360
|
Глава 9
# `numpy` для работы с массивами/списками
import numpy as np
# загружаем наши данные
ppp_data = pd.read_csv('public_150k_plus_borrower_fingerprint_a.csv')
# сначала проверим данные на вменяемость
print(ppp_data[ppp_data['JobsReported'] <= 0])
# отбрасываем записи, не имеющие значения в поле`JobsReported'
ppp_data.drop(labels=[437083,765398], axis=0)
# рассчитываем затраты на одно рабочее место
dollars_per_job = ppp_data['CurrentApprovalAmount']/ppp_data['JobsReported']
# вставляем новый столбец в наш исходный датасет
ppp_data.insert(3, 'Dollars per Job', dollars_per_job)
# используем значение `ProcessingMethod` для идентификации ссуд второго раунда
pps_loans = ppp_data[ppp_data['ProcessingMethod'] == 'PPS']
# выбираем все ссуды второго круга, значение которых составляет $2 млн
pps_got_2M = pps_loans[pps_loans['CurrentApprovalAmount'] == 2000000.00]
print("Actual $2M second-round loans:")
print(pps_got_2M.shape)
# получаем фингерпринты всех компаний, получивших кредиты второго раунда
# на сумму $2 млн
biz_names = pd.unique(pps_got_2M['BorrowerNameFingerprint'])
# преобразуем этот список в DataFrame
biz_names_df = pd.DataFrame(biz_names, columns=['BorrowerNameFingerprint'])
# создаем массив той же длины, что и `biz_names_df`; заполняем значением флага
fill_column = np.full((len(biz_names),1), '2Mil2ndRnd')
biz_names_df['GotSecond'] = fill_column
# теперь объединим этот новый DataFrame из двух столбцов с нашим списком full_data
second_round_max = pd.merge(ppp_data, biz_names_df, on='BorrowerNameFingerprint')
# все ссуды, фингерпринты которых совпадают с таковыми у компаний, получивших
# $2 млн во втором раунде, должны содержать значение '2Mil2ndRnd'
# в столбце `GotSecond`
second_max_all_loans = second_round_max[
second_round_max['GotSecond'] == '2Mil2ndRnd']
Введение в анализ данных
|
361
# предприятий, получивших одобрение на $2 млн во втором раунде, должно быть
# в 2 раза больше
print('Total # of loans approved for most orgs that got $2M for second round:')
print(second_max_all_loans.shape)
# сколько денег эти предприятия получили от PPP в общей сложности?
total_funds = second_max_all_loans['CurrentApprovalAmount'].sum()
print("Total funds approved for identified orgs that could have " + \
"second-round max:")
print(total_funds)
# теперь давайте построим график этого нового столбца на выбранном датасете
# установим тему seaborn
sns.set_theme(style="whitegrid")
# `matplotlib` `subplots()` для построения графиков рядом друг с другом
fig, ((row1col1)) = plt.subplots(nrows=1, ncols=1)
# построим гистограмму нашего анализа на основе дат
date_based = sns.histplot(data=second_max_all_loans, x='Dollars per Job',
hue='ProcessingMethod', ax=row1col1)
# выводим график!
plt.show()
Оказалось, что несколько предприятий не сообщили о наличии рабочих мест, что
нарушит наши расчеты. Поскольку причиной послужили только две записи, мы
просто отбросим их, используя их метки строк, назначенные библиотекой pandas.
Хотя текстовый вывод подтверждает, что мы рассматриваем тот же набор ссуд, что
и в разд. "Вопрос за $2 миллиона", наши рационализированные данные выявляют
примечательные аномалии в некоторых ссудах первого раунда. В этих случаях горстка компаний, похоже, одобрила кредиты, которые выделяли больше средств на
фонд заработной платы, чем разрешенный лимит в 100 000 долларов на одно рабочее место, как показано на рис. 9.7.
Что можно с этим сделать? Вы можете заметить, что к этому моменту мы довольно
далеко отошли от вопроса, заданного в разд. "Пандемия и программа PPP" главы 6,
где мы оценивали, помогла ли программа PPP "спасти" американский бизнес. Хотя
то направление помогло в работе над оценкой качества данных, проведение контекстного анализа открыло ряд новых вопросов и направлений, что, как мне кажется, представляется довольно распространенным явлением, когда речь идет выпасе
данных. Надеюсь, это побудит вас продолжить работу с новыми датасетами, чтобы
увидеть, что еще вы можете найти!
362
|
Глава 9
Рис. 9.7. Детализация в долларах на одно рабочее место компаний,
получивших кредиты второго раунда в размере $2 млн
Заключение
После всех этих анализов мы узнали несколько новых вещей — некоторые из них
относятся именно к этому датасету, но многие из них имеют гораздо более широкое
применение:
относительно небольшое число компаний получили одобрение на максимально
допустимую сумму ссуды второго раунда в рамках программы кредитования
PPP. Хотя многие из них подали заявку на получение в первом раунде гораздо
большей суммы, некоторые компании этого не сделали;
несколько компаний, получивших одобрение на получение кредита второго ра-
унда в размере $2 млн, заявили, что в первом раунде они получили более $100
тыс. на каждое заявленное рабочее место;
данные, введенные человеком, всегда беспорядочны. Вот почему очистка дан-
ных — это постоянный, итерационный процесс. Документирование работы необходимо для того, чтобы иметь возможность защитить свои результаты.
Итак, вводный курс анализа данных оставил нам гораздо больше вопросов, чем ответов. На данный момент у нас есть только один способ узнать больше: поговорить
с людьми. Конечно, некоторые из выявленных нами закономерностей выглядят сомнительно19, но у нас слишком много неизвестных, чтобы делать какие-либо обос19
https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3906395
Введение в анализ данных
|
363
нованные заявления на данном этапе. Например, многие из ссуд второго раунда в
размере $2 млн еще не были выданы на момент публикации этих данных, поэтому
некоторые компании могли получить или использовать гораздо меньше этой суммы. Поскольку правила программы PPP требуют, чтобы минимальный процент
кредита расходовался только на заработную плату, компании, получившие одобрение на слишком большую сумму, могли просто использовать разницу на другие
допустимые расходы, такие как проценты по ипотеке или траты на здравоохранение. Это позволило бы им удовлетворить условиям выдачи ссуды. Другими словами, хотя мы можем узнать что-то нового из такого типа анализа числовых данных,
этой информации не будет достаточно, чтобы получить представление о материале
в целом — как или почему. Для этого нам нужны данные, полученнные непосредственно от людей.
Когда мы проделали эту работу и четко определили, какими знаниями хотим поделиться, мы готовы подумать о наиболее эффективном способе донести то, что мы
узнали, до других. Подобно тому, как наш анализ данных опирается на данные и на
человеческие суждения и предоставляемые данные, наиболее эффективные типы
передачи данных почти всегда подразумевают баланс между словами и визуализацией. В следующей главе показано, что при тщательном подборе слов и визуализации можно гарантировать, что наше послание действительно будет услышано.
ГЛАВА 10
Представление данных
После всех усилий, затраченных на получение доступа, оценки, очистки, преобразования, дополнения и анализа данных, мы наконец-то достигли того момента, когда готовы подумать о передаче полученных выводов другим. Будь то официальная
презентация для коллег или пост в социальных сетях для друзей и последователей;
обмен информацией, полученной в результате работы по выпасу данных, — всё это
возможность оказать влияние результатами нашей работы на кого-то другого.
Как и любая другая часть процесса выпаса данных, эффективная и точная передача
наших выводов предполагает применение всего нескольких жестких правил, но при
этом содержит очень много суждений. Это относится как к письменным сообщениям, так и касается такого аспекта передачи данных, как визуализация, которой часто
уделяется больше всего внимания.
Как уже говорилось в разд. "Визуализация для анализа данных" главы 9, создание
визуализаций для эффективного обмена нашими данными с другими людьми требует иной направленности и подхода, чем при создании визуализаций для получения этих данных. Например, если вы не пытаетесь обратиться к достаточно специализированной аудитории (скажем, через академическую публикацию), очень маловероятно, что гистограмма попадет в ваш арсенал визуализации, когда придет
время поделиться своими результатами с миром. В то же время, весьма вероятно,
что в итоге вы будете использовать какую-либо форму линейчатой или столбчатой
диаграммы1, чтобы поделиться своими мыслями с неспециализированной аудиторией, поскольку эти широко используемые и хорошо читаемые графические формы
относительно легко поддаются точной интерпретации для большинства аудитории.
Другими словами, способ визуального представления результатов анализа данных
должен зависеть не только от того, какие данные у нас есть (или какие мы использовали для получения выводов), но и от аудитории, на которую мы пытаемся воздействовать. Многие программные пакеты (включая некоторые из применяемых в
этой книге) с удовольствием создадут диаграммы и графики, если вы просто укажете им на структурированный датасет и передадите несколько дополнительных
параметров. Хотя такой подход создаст необходимый минимум для визуализации,
результат будет больше похож на машинный перевод, чем на поэму. Да, на высоком уровне результат может соответствовать "правилам" языка, но прозрачность
1
Из которых гистограмма является особым типом.
Представление данных
|
365
смысла повествования (не говоря уже о его эффективности или красноречии) вызывает сомнения. Таким образом, использование визуализации для того, чтобы
сделать наши данные действительно доступными для других, требует тщательного
обдумывания. Необходимо определить, что вы хотите передать, какая визуальная
форма подходит для этого лучше всего и как вы можете адаптировать ее к своим
конкретным потребностям.
В этой главе мы рассмотрим каждую из этих задач по очереди, начиная с некоторых стратегий, призванных помочь определить ключевые моменты в полученных
данных. После этого мы проведем обзор наиболее распространенных (и полезных!)
визуальных форм для данных. В каждом случае мы рассмотрим правила и лучшие
практики использования визуальных форм, а также представим базовый код для их
визуализации в Python с использованием комбинации библиотек seaborn и matplotlib.
Наконец, мы возьмем базовую визуализацию реальных данных и рассмотрим, как
настроить и доработать различные ее элементы, чтобы превратить (обычно) по
умолчанию пригодную презентацию в нечто одновременно точное и привлекательное. Я надеюсь, что попутно вы изучите хотя бы несколько новых инструментов и
подходов, которые помогут вам более критически подойти к визуализации данных
в целом — независимо от того, являются ли они результатом вашей работы по выпасу данных или нет.
Основы визуального красноречия
Как уже несколько раз упоминалось, процесс написания хорошего кода является
зеркальным отражением процесса в большинстве других обстоятельств. Например,
в главе 8 мы потратили время на пересмотр и реструктуризацию кода — хотя он
уже работал — и превратили его в скрипты и функции, которые в итоге стали более
четкими, чистыми и пригодными для повторного использования. Этот процесс по
сути не отличается от того, как пересматривается эссе или статья: собрав все ключевые идеи в одном месте, можно вернуться к материалу позже и посмотреть, как
его стоит переформулировать и реорганизовать, чтобы он стал более лаконичным, а
концепции сливались в повествовании более логично.
Хотя этот же цикл "написать-редактировать-отшлифорвать" применим и к визуализациям данных, однако блок, с которым мы работаем, гораздо меньше похож на
эссе и гораздо больше на отдельный абзац — потому что в целом одна визуализация должна использоваться для передачи одной-единственной идеи. Это касается
как печатной, так и цифровой, статичной или интерактивной визуализации, части
длинного выступления или отдельного поста в социальных сетях. Одна визуализация = одна ключевая идея.
Я подчеркну это сейчас, потому что если вы открыли эту главу в надежде найти
примеры построения интерактивных визуализаций в стиле Gapminder2 или проду2
https://www.google.com/publicdata/directory
366
|
Глава 10
манного потокового графика3, хочу вас сразу разочаровать: в этой главе я сосредоточусь на наиболее часто используемых типах визуализации, таких как гистограммы, столбцы и линейные диаграммы. Причина отчасти в том, что они остаются самым простым и наиболее интерпретируемым способом представления данных, которые содержат только одну независимую переменную, — а это в любом случае
все, что вы будете пытаться представить бóльшую часть времени. Да, более сложные визуализации могут использоваться для построения графиков нескольких независимых переменных — оригинальную визуализацию Gapminder можно считать
редким хорошим примером — но без обаятельного шведского мужчины, который
проводит зрителей через материалы в реальном времени4, такие визуализации
больше похожи на красивые игрушки, чем на информативные инструменты. Именно поэтому мы сосредоточимся на совершенствовании доступных визуальных
форм до уровня, который мне нравится называть красноречивыми графиками, —
визуализации, которая, как и лучший текст, передает информацию четко, просто и
доступно. Хотя создание красноречивых графиков не исключает визуальной сложности или даже интерактивности, этот процесс требует, чтобы каждый аспект визуализации способствовал ясности и раскрытию смысла графика.
Достижение такого визуального красноречия означает размышление о визуализации данных на трех основных этапах:
Уточнение фокуса внимания
Что именно вы пытаетесь донести до людей? В некотором смысле это параллельно процессу выбора первоначального вопроса для выпаса данных: независимо от того, что выявил ваш выпас данных и анализ, вы должны передать одну
идею в каждой визуализации. Как узнать, эффективно ли вы это сделали? Чаще
всего это означает, что вы сможете выразить идею в одном предложении. Как и
в предыдущем вопросе выпаса данных, составленный вами отчет о данных будет служить своего рода "базовой истиной" для принятия решений о визуализации. Все, что помогает вам более четко донести свою идею, сохраняется; все остальное сокращается.
Поиск своей визуальной формы
Как лучше отображать данные — в виде столбцов или линейных диаграмм? В
виде диаграммы связей? Будет ли это круговая диаграмма? Диаграмма рассеяния или пузырьковая диаграмма? Определение наилучшей визуальной формы
для ваших данных всегда связано с некоторыми экспериментами. В то же время
выбор визуальной формы для выражения ваших данных — это не только вопрос
предпочтений или вкуса; существует несколько неоспоримых правил, как определенные типы и отношения данных должны быть визуально закодированы. Да,
эстетика играет определенную роль в эффективности визуализации, но она не
может отменить необходимость точности.
3
4
https://flowingdata.com/2008/02/25/ebb-and-flow-of-box-office-receipts-over-past-20-years
https://youtube.com/watch?v=jbkSRLYSojo
Представление данных
|
367
Повышение ясности и смысла
Даже если определена основная визуальная форма, существует множество способов, позволяющих деталям вашей визуализации улучшить или ухудшить ее
ясность, доступность, визуальную привлекательность и красноречие. Как минимум, вам придется принимать решения о цвете, рисунке, шкалах и легендах, а
также об подписях, заголовках и аннотациях. Если формулировка ваших данных
особенно сложна, то для отражения этих нюансов вам потребуется тщательно
продумать еще более наглядную структуру, например, линии ошибок или диапазоны неопределенности, или, возможно, прогнозируемые и/или отсутствующие
данные.
В следующих разделах мы не только концептуально обсудим каждый из этих этапов, но и на реальных данных посмотрим, как они применяются на практике с помощью Python.
Сформулируйте свои данные
Много лет назад мне посчастливилось пригласить Аманду Кокс5 из The New York
Times на один из моих курсов по визуализации данных, где она поделилась отличным советом, как оценить, действительно ли данные подходят для визуализации:
"Если в заголовке нет глагола, у вас проблемы".
При поверхностном рассмотрении, конечно, это требование легко выполнить6. Однако смысл ее заявления подразумевает нечто гораздо более строгое: заголовок
графика должен четко формулировать какую-то важную взаимосвязь или утверждение, а подтверждающие доказательства должны быть видны в самом графике.
Почему это так важно? Во-первых, если вы поместите свои утверждения прямо в
заголовок, это побудит читателей сразу же посмотреть на ваш график внимательно.
Четкое именование поможет убедиться, что зрители — в буквальном смысле —
будут знать, где искать доказательства этих утверждений. Конечно, наша работа
как информационных дизайнеров заключается в том, чтобы убедиться, что все визуальные подсказки графики также способствуют хорошему восприятию, но часто
именно заголовок привлекает людей7.
Если у вас не получается найти глагол действия для заголовка графика, это признак
того, что визуализация, вероятно, не лучший способ передать ваши мысли. Правда,
в правильных обстоятельствах люди могут обрабатывать визуализации невероятно
быстро8, но это преимущество реализуется только в том случае, если визуализации
есть что "сказать". Другими словами, хотя вы можете создать визуально точный
5
https://en.wikipedia.org/wiki/Amanda_Cox
Особенно если вы отдаете себе отчет, что используете глаголы-связки https://merriam-webster.com/
dictionary/linking%20verb.
7
https://psychologicalscience.org/news/how-headlines-change-the-way-we-think.html
8
https://news.mit.edu/2014/in-the-blink-of-an-eye-0116
6
368
|
Глава 10
график под названием "Один год ежедневных долгосрочных ставок казначейства",
реальность такова, что даже самый большой специалист по политике будет задаваться вопросом, почему он должен утруждать себя просмотром этого графика. Не
настаивайте на визуализации, если это не подходящий инструмент! Помните, что
наша цель — как можно эффективнее передать свои идеи по выпасу данных, а не
выразить их визуально любой ценой. Сосредоточившись в первую очередь на уточнении формулировки данных и подтвердив, что она обладает необходимой вам силой, вы сможете избежать больших временных затрат на разработку и создание визуализации, которая на самом деле не делает того, что вы хотите, или того, что
нужно вашей аудитории. Конечно, базовые визуализации данных можно быстро
создать с помощью датасета и надежной библиотеки визуализации (например,
seaborn). Но создание действительно красноречивых визуализаций требует тщательного обдумывания, а также детальной кастомизации даже самых лучших библиотечных графиков, предоставляемых по умолчанию. Прежде чем тратить столько
времени и энергии, стоит убедиться, что ваша сложная визуализация не была бы
лучше в качестве отдельной, выделенной статистики из вашего анализа.
После создания мощного "заголовка" формулировки данных пришло время определить графическую форму, которая поможет вам наиболее эффективно представить
данные, подтверждающие ваше заявление.
Диаграммы, графики и картограммы — вот это да!
Даже если мы ограничимся более простыми графическими формами, все равно остается достаточно вариантов, способных сделать процесс поиска оптимального варианта для наших данных немного ошеломляющим. Следует выбрать линейчатую
или столбчатую диаграмму? Если подходит вариант гистограммы, должна ли она
быть горизонтальной или вертикальной? Подходят ли вообще круговые диаграммы? К сожалению, это одна из ситуаций, когда библиотеки Python по большей части не могут нам помочь, поскольку они будут просто пытаться создать любой запрошенный тип графика с предоставленными данными. Поэтому нам нужен более
эффективный способ.
Вот здесь-то и пригодится хорошо сфокусированная формулировка данных. Ссылается
ли ваша формулировка на абсолютные значения — например, на CurrentApprovalAmount
в наших данных о кредитах PPP — или же она сосредоточена на взаимосвязи между значениями, как, например, диаграмма The pandemic cut annual FDI flows by onethird9? В то время как утверждения об абсолютных значениях часто лучше всего
выражаются с помощью линейчатых диаграмм, формулировка взаимосвязей данных может быть подкреплена с помощью более широкого спектра визуальных
форм. Например, если ваша формулировка данных включает утверждение об изменении во времени, хорошо было бы начать с линейного графика или диаграммы
9
https://economist.com/graphic-detail/2021/06/21/the-pandemic-cut-annual-fdi-flows-by-one-third
Представление данных
|
369
рассеяния. Ведь некоторые визуальные формы, такие как круговые диаграммы и
картограммы, трудно адаптировать к чему-либо, кроме данных, полученных в один
момент времени.
На самом деле, для визуализации данных не существует жестких и неизменных
правил10, но я описала применяющиеся в следующих разделах, наряду с некоторыми общими советами по созданию графики. Хотя эти рекомендации помогут выбрать форму для визуализации, которая не будет работать против ваших данных,
это только следующий шаг. Варианты, которые могут действительно поднять вашу
графику на более высокий уровень, мы рассмотрим в разд. "Элементы красноречивых визуальных эффектов". В этом разделе мы выйдем за рамки (все еще довольно
отличных) стандартных настроек библиотеки seaborn и начнем больше копаться в
matplotlib, чтобы управлять такими вещами, как метки, цвета и аннотации, которые
могут действительно выделить вашу визуализацию.
Круговые диаграммы
Круговые диаграммы — удивительно спорная тема в визуализации. Хотя круговые
диаграммы полезны для обучения детей дробям11, есть много людей, которые считают, что им практически нет места в лексиконе эффективной визуализации12.
Лично я считаю, что есть определенные, пусть и ограниченные ситуации, в которых круговая диаграмма может стать наилучшей визуализацией для поддержки вашей формулировки данных. Например, если вы пытаетесь объяснить, какая доля
или часть ваших данных имеет определенное значение, а остальные значения могут быть разумно сгруппированы в четыре или менее категорий, то круговая диаграмма вполне может вам подойти. Это особенно верно, если на результирующем
графике выделяются значения, соответствующие некоторой узнаваемой13 доле целого (например, 1/4, 1/3, 1/2, 2/3 или 3/4), поскольку человеческий глаз способен
обнаружить такие различия в расположении линий без особых усилий14.
Например, глядя на результаты демократических выборов мэра Нью-Йорка в июне
2021 г.15, можно представить, что мы сформулируем данные следующего содержания: "Несмотря на обширную область проведения голосования, 3 лучших кандидата набирают почти 3/4 голосов за первый тур выборов". Поскольку только четыре
кандидата набрали более 10% голосов после первого тура выбора, целесообразно
10
Хотя даже эти немногочисленные правила часто нарушаются публично, например Start at Zero Improves This Chart but Only Slightly, автор Junk Chart, https://junkcharts.typepad.com/junk_charts/2021/
06/start-at-zero-improves-this-chart-but-only-slightly.html.
11
https://pbs.org/parents/recipes/pegs-pizza-fractions
12
https://storytellingwithdata.com/blog/2011/07/death-to-pie-charts
13
https://store.moma.org/for-the-home/kitchen-dining/cookware-kitchen-tools/visual-measuring-cups/
8711-802262.html
14
https://csc2.ncsu.edu/faculty/healey/PP
15
https://washingtonpost.com/elections/election-results/new-york/nyc-primary
370
|
Глава 10
также объединить всех остальных кандидатов в одну категорию "Другие". В данном случае круговая диаграмма послужит вполне разумным способом точного
представления результатов и поддержки нашего утверждения. Отчасти потому, что
она позволяет легко увидеть, насколько сильно лидирующие кандидаты опережают
других.
Учитывая спорный характер круговых диаграмм, не удивительно, что в достаточно
универсальной библиотеке seaborn нет опции круговых диаграмм. Однако можно
напрямую использовать библиотеку matplotlib для получения очень удобной круговой диаграммы16. Однако в функции построения круговых диаграмм matplotlib есть
несколько своеобразных моментов, которые нам предстоит преодолеть. Например,
круговые диаграммы строятся так, чтобы самый большой раздел начинался с "12
часов", а остальные добавлялись в порядке убывания размера по часовой стрелке.
Однако в библиотеке matplotlib первый раздел начинается с "3 часов", а дополнительные разделы добавляются против часовой стрелки. Следовательно, нам нужно
указать значение startangle=90 и изменить порядок следования сегментов от наибольшего к наименьшему17. Аналогично, matplotlib по умолчанию присваивает каждому "кусочку" пирога свой цветовой оттенок (например, фиолетовый, красный,
зеленый, оранжевый и синий) в определенном диапазоне цветов, который может
быть недоступен людям с некоторыми видами дальтонизма. Поскольку в нашей
формулировке данные концептуально группируются по трем лучшим кандидатам, я
сделала их всех одинакового оттенка (зеленого). И, поскольку все кандидаты представляют одну политическую партию, я оставила все кусочки в зеленой гамме.
Чтобы увидеть, как программируется такой тип графика (включая эту небольшую
кастомизацию), посмотрите листинг 10.1 и полученную визуализацию на рис. 10.1.
Листинг 10.1. a_humble_pie.py
import matplotlib.pyplot as plt
# matplotlib работает против часовой стрелки, поэтому нам нужно изменить
# порядок отображения "кусочков" нашего пирога
candidate_names = ['Adams', 'Wiley', 'Garcia', 'Yang', 'Others']
candidate_names.reverse()
vote_pct = [30.8, 21.3, 19.6, 12.2, 16.1]
vote_pct.reverse()
colors = ['#006d2c','#006d2c', '#006d2c', '#31a354','#74c476']
colors.reverse()
fig1, ax1 = plt.subplots()
16
Поскольку библиотеки pandas и seaborn в значительной степени опираются на matplotlib, существует множество случаев, когда значительная кастомизация требует прямого использования функций
matplotlib. Более подробно это показано в разд. "Элементы красноречивых визуальных эффектов".
17
Конечно, можно просто изменить порядок первоначального представления данных, но я предпочитаю, чтобы порядок данных и возможный визуальный порядок совпадали.
Представление данных
|
371
# по умолчанию начальной осью является ось x; значение 90 гарантирует,
# что вместо нее будет вертикальная линия
ax1.pie(vote_pct, labels=candidate_names, autopct='%.1f%%', startangle=90,
colors=colors)
ax1.axis('equal') # равное соотношение сторон гарантирует, что пирог будет
# нарисован в виде круга.
# выводим график!
plt.show()
Аргумент, который мы передаем autopct, должен быть "форматированным строковым литералом", также известным как f-строка18. В этом примере указано, что
дробь должна быть выражена как число с плавающей точкой (десятичное) с точностью до одного знака. Двойной знак процента (%%) используется здесь для того, чтобы вывести один знак (экранируя зарезервированный символ знака процента другим символом).
Рис. 10.1. Круговая диаграмма первого тура выборов в Нью-Йорке
Вкратце, если вы рассматриваете круговую диаграмму:
Правило
Категории ваших данных должны концептуально (и буквально) складываться в
единое "целое".
Рекомендация
Количество категорий должно быть сокращено до пяти или менее.
18
https://docs.python.org/3/tutorial/inputoutput.html#formatted-string-literals
372
|
Глава 10
Рекомендация
Доля данных, которые вы хотите выделить, должна составлять 1/4, 1/3, 1/2, 2/3
или 3/4 от общего количества.
Однако если ваши данные не соответствуют одному или нескольким из этих требований, следующей графической формой для рассмотрения может стать гистограмма.
Линейчатые и столбчатые диаграммы
Гистограммы часто становятся наиболее эффективным способом выделения взаимосвязей (включая различия) между дискретными, номинальными (в отличие от
пропорциональных) значениями данных. В отличие от круговых диаграмм, гистограммы могут точно представлять датасеты, в которых значения не складываются в
"целое". Они также могут эффективно представлять как положительные, так и отрицательные значения (одновременно, если необходимо) и способны отображать
как сравнение различных категорий данных в момент времени, так и значения данных с течением времени. Столбцы также могут быть ориентированы вертикально
(часто такие диаграммы называют столбчатыми) или горизонтально (линейчатые),
чтобы сделать метки и взаимосвязи данных более разборчивыми.
Другими словами, гистограммы невероятно гибкие и предлагают множество вариантов для эффективного представления доказательств утверждений о данных. Однако существует одно жесткое и непреложное правило для работы с гистограммами: значения данных ДОЛЖНЫ начинаться с нуля! Исключений из этого правила
действительно нет, хотя это не останавливает некоторых людей от попыток19. Почему это правило так важно? По причине того, что начало столбцов графика с числа, отличного от нуля, означает, что их визуальная разница в длине больше не будет пропорциональна их фактической разнице в значении.
Например, представьте, что вы добиваетесь повышения зарплаты на работе, где в
настоящее время получаете минимальную федеральную зарплату в размере $7,25 в
час. Вы просите своего начальника повысить вашу почасовую ставку до $9,19 в
час20, чтобы учесть влияние инфляции с момента последнего повышения минимальной заработной платы в 2009 г.
"Что ж, — говорит начальник, — давайте посмотрим, как будет выглядеть такая
прибавка", — и затем показывает вам график, подобный тому, что показан на рис. 10.2.
Вы видите проблему? Благодаря тому, что столбики начинаются с 5, а не с нуля,
график на рис. 10.2 выглядит так, будто 9,19 доллара в час — это почти вдвое
больше вашей нынешней зарплаты. Но, конечно, простая математика (9,19 7,25 /
7,25 = ~,27) показывает, что это чуть более чем на 25% больше текущей ставки. Как
видите, начало гистограмм с нуля — это не вопрос вкуса, эстетики или семантики
— это предотвращение визуального обмана21.
19
https://datajournalism.com/read/longreads/the-unspoken-rules-of-visualisation
https://data.bls.gov/cgi-bin/cpicalc.pl?cost1=7.25&year1=200907&year2=202107
21
https://flowingdata.com/2017/02/09/how-to-spot-visualization-lies
20
Представление данных
Рис. 10.2. Неточное сравнение заработной платы
Рис. 10.3. Неточное сравнение пенсионного возраста
|
373
374
|
Глава 10
Тем не менее даже профессиональные графические команды иногда ошибаются.
Возьмем этот пример, приведенный в блоге Кайзера Фунга на сайте Junk Charts под
заголовком "Working Culture"22 и показанный на рис. 10.3.
На рис. 10.3 "ломаная" гистограмма претендует на то, чтобы показать, когда мужчины в разных странах прекращают работать, по сравнению с официальным пенсионным возрастом. Как и на рис. 10.2, начало столбиков не с нуля сильно искажает
реальную разницу этих значений: согласно маркировке данных, мужчины во Франции выходили на пенсию примерно на 10 лет раньше мужчин в Японии — разница
в годах работы составляет 15%. Однако фактически изображенная линия для Японии более чем в два раза длиннее, чем для Франции. Конечно, если бы все линии
начинались с нуля, разница в их значениях не казалась бы такой драматичной.
Что же случилось? Действительно ли дизайнеры этой визуализации пытаются обмануть нас, заставляя думать, что мужчины в Японии работают в два раза дольше,
чем мужчины во Франции? Почти наверняка нет. Графики, подобные приведенному на рис. 10.3, послужили причиной появления условия, говорящего о важности
выражения одной идеи в каждой визуализации: пытаясь добавить больше, вы, скорее всего, столкнетесь с проблемами и получите в итоге неточную и вводящую в
заблуждение визуализацию. На рис. 10.3 дизайнеры пытаются показать два разных
показателя (разница между официальным пенсионным возрастом и возрастом, когда мужчины перестают работать, и каков этот возраст), шкалы которых несовместимы. Начните шкалу с нуля, и читатели не смогут распознать возраст, в котором
расположены точки; измените масштаб, чтобы расположение точек было разборчивым, и шкала станет неточной. В любом случае, вы заставляете читателя проделать
большую работу — тем более, что заголовок графика не говорит ему о том, что
следует искать ;-)
Давайте посмотрим, что произойдет, если мы начнем с четкого заголовка с глаголами действия и используем его для изменения дизайна этого графика: "Мужчины
в Японии работают годами после достижения пенсионного возраста, в то время как
другие прекращают работу задолго до этого". В данном случае формулировка заголовка/данных говорит о том, что мы хотим подчеркнуть разницу между официальным выходом на пенсию и фактическим выходом на пенсию. Теперь можно разработать горизонтальную гистограмму, которая одновременно подтверждает это утверждение и точно представляет основные данные, как показано в листинге 10.2 и
на рис. 10.4.
Листинг 10.2. retirement_age.py
import
import
import
import
22
matplotlib.pyplot as plt
pandas as pd
seaborn as sns
numpy as np
https://junkcharts.typepad.com/junk_charts/2005/11/finally_a tange.html
Представление данных
|
375
# список стран (сокращенный)
countries = ['Japan', 'Iceland', 'Switzerland', 'France', 'Ireland', 'Germany',
'Italy', 'Belgium']
# разница в годах между официальным и фактическим выходом на пенсию
retirement_gap = [9, 2, 2, -1, -2, -2, -7, -8]
# соедините два списка вместе и укажите имена столбцов, как мы создаем DataFrame
retirement_data = pd.DataFrame(list(zip(countries, retirement_gap)),
columns =['country', 'retirement_gap'])
# на практике может быть предпочтительнее написать функцию, которая генерирует
# этот список, основываясь на значениях наших данных
bar_colors = ['#d01c8b', '#d01c8b', '#d01c8b', '#4dac26','#4dac26','#4dac26',
'#4dac26','#4dac26']
# передаем наши данные и палитру в функцию `barplot()` `seaborn`
ax = sns.barplot(x="retirement_gap", y="country",
data=retirement_data, palette=bar_colors)
# выводим график!
plt.show()
Присвоив числовые значения оси x, а значения категорий — оси y, seaborn отобразит это в виде горизонтальной, а не вертикальной гистограммы.
Рис. 10.4. Горизонтальная гистограмма пенсионного разрыва
Поскольку моя формулировка данных/заголовка теперь четко указывает на разницу
в годах между официальным и фактическим пенсионным возрастом, я решила ото-
376
|
Глава 10
бразить эту разницу на графике, а также изменить порядок данных: мужчины во
Франции выходят на пенсию только на один год раньше, в то время как мужчины в
Бельгии выходят на пенсию примерно на восемь лет раньше. Чтобы еще больше
подчеркнуть различие между возрастом до и после официального выхода на пенсию, я также выделила цветом столбики в соответствии с их положительным/отрицательным значением.
Сочетание положительных и отрицательных значений в этом датасете, а также более длинные метки названий стран делают этот график более читаемым в виде горизонтальной гистограммы, а не вертикальной. Однако если мы хотим проверить
представление в качестве вертикального графика для сравнения, необходимо поменять местами столбцы данных, которые мы передаем в качестве аргументов x и y
функции barplot(). Например, изменив это:
ax = sns.barplot(x="retirement_gap", y="country", data=retirement_data,
palette=bar_colors)
на это:
ax = sns.barplot(x="country", y="retirement_gap", data=retirement_data,
palette=bar_colors)
Хотя это изменение достаточно легко сделать, могут возникнуть реальные различия
в читабельности данного датасета при вертикальном или горизонтальном отображении. В частности, вертикальные столбики, как правило, лучше подходят для
данных с более короткими метками, меньшим разбросом и/или небольшим количеством или полным отсутствием отрицательных значений, в то время как горизонтальные столбики обычно лучше подходят для расходящихся данных (особенно
при высокой доле отрицательных значений) и/или данных с более длинными метками.
Хотя визуализация, показанная на рис. 10.4, довольно проста, если вы запустите
код, то сами увидите разницу в качестве, которую, например, привносит в визуализацию тщательная работа с цветовой палитрой. Хотя здесь я выбрала двоичное кодирование цвета (пурпурный/зеленый), я также могла бы указать одну из 170 цветовых палитр библиотеки seaborn (например, palette='BuGn'), что (по большей части)
выровняло бы интенсивность цвета со значением каждой полосы.
Напомним, что при работе с гистограммами:
Правило
Столбцы должны начинаться с нулевой отметки!
Рекомендация
Вертикальные столбцы хороши для более плотных данных с меньшим разбросом.
Рекомендация
Горизонтальные линии лучше использовать при применении более разнообразных и/или длинных меток данных.
Представление данных
|
377
Линейные диаграммы
Если в вашей формулировке данных речь идет о темпах изменения, а не о разнице
значений, самое время изучить линейные графики. Как и гистограммы, линейные
диаграммы могут эффективно отображать несколько категорий числовых данных,
но только по мере их изменения во времени. Однако, поскольку они не кодируют
визуально абсолютные значения данных, шкалы линейных диаграмм не обязательно должны начинаться с нуля.
Сначала это может показаться приглашением к манипуляциям — и действительно,
линейные диаграммы были в центре некоторых крупных политических споров23.
Однако и для гистограмм, и для линейных диаграмм масштаб оси y определяется
данными: точно так же, как нельзя начать гистограмму со значения, отличного от
нуля, было бы абсурдно масштабировать ось y во много раз больше значения самого большого показателя данных, как показано на рис. 10.5.
Рис. 10.5. Еще один плохой график сравнения зарплат
Несмотря на техническую точность, чрезмерно растянутая ось y на рис. 10.5 настолько сжала значения данных, что наши глаза больше не могут точно и эффективно различать разницу между ними. Для гистограмм наибольшее значение оси y
обычно должно находиться на следующем "целом" приращении метки (подробнее
об этом в разд. "Выбор масштаба"). Для линейных диаграмм эксперты по визуали23
См. The Hockey Stick: The Most Controversial Chart in Science, Explaine, автор Chris Mooney,
https://theatlantic.com/technology/archive/2013/05/the-hockey-stick-the-most-controversial-chart-inscience-explained/275753; но, тем не менее, организационные схемы тоже (предупреждение о ругательстве) свое отжили, https://flickr.com/photos/robertpalmer/3743826461.
378
|
Глава 10
зации, такие как Дона Вонг, рекомендуют, чтобы диапазон значений данных занимал примерно две трети пространства оси y24.
Конечно, такой подход подчеркивает влияние, которое оказывает выбор точек данных для линейного графика на общий посыл. Например, рассмотрим график из
журнала The Economist25, показанный на рис. 10.6.
Рис. 10.6. Потоки прямых иностранных инвестиций (FDI), 20072020 гг.
В данном случае заголовок "Пандемия сократила годовой поток FDI на одну треть"
(The pandemic cut annual FDI flows by one-third) на самом деле довольно эффективен; он одновременно активен и конкретен. Но хотя данные, о которых говорится в
этом заголовке, включены в сопроводительную диаграмму, они не очень выделены
— визуализация включает данные более чем за десятилетие, хотя описываемые изменения произошли между 2019 и 2020 гг. Если пересмотреть график и сосредоточиться только на том, что произошло между этими двумя годами, как показано на
рис. 10.7, мы можем более четко подтвердить формулировку данных и выявить дополнительное измерение данных: хотя прямые иностранные инвестиции значительно сократились в "развитых" странах, в "развивающихся" регионах они в основном остались на прежнем уровне. Как говорится в самой статье, "приток
средств в богатые страны падал быстрее, чем в развивающиеся, — на 58% против
всего 8%". Этот двухточечный линейный график, известный также как диаграмма
наклона, не только облегчает читателям восприятие доказательств, лежащих в основе утверждения, вынесенного в заголовок, но и позволяет сделать вывод о нерав24
25
Подробнее см. в книге The Wall Street Journal Guide to Information Graphics, автор Dona M. Wong.
https://economist.com/graphic-detail/2021/06/21/the-pandemic-cut-annual-fdi-flows-by-one-third
Представление данных
|
379
номерном воздействии пандемии на FDI в отношении "развитых" и "развивающихся" стран, тем самым предоставляя доказательства и для последующего утверждения статьи. Как показано в листинге 10.3, построение этого типа базового линейного графика занимает всего несколько строк кода.
Рис. 10.7. Пандемия сократила годовой поток FDI на одну треть
Листинг 10.3. covid_FDI_impact.py
import
import
import
import
matplotlib.pyplot as plt
pandas as pd
seaborn as sns
numpy as np
# каждый отдельный массив — это строка данных
FDI = np.array([[0.8, 0.7], [0.3, 0.6]])
fdi_data = pd.DataFrame(data=FDI,
columns=['Developed', 'Developing'])
ax = sns.lineplot(data=fdi_data)
# выводим график!
plt.show()
На
этом этапе у вас может возникнуть вопрос — не будет ли включение данных
только за два года ошибкой. В конце концов, у нас в запасе есть десятилетний запас.
380
|
Глава 10
Настоящий вопрос, конечно, заключается не в том, есть ли у нас больше данных, а в
том, не искажают ли данные, которые мы утверждаем, более широкую тенденцию.
Если посмотреть на исходный график на рис. 10.6, становится ясно, что за последние
15 лет или около того объемы FDI падали так быстро только дважды: с 2007 г. по
2008 г. и снова с 2016 г. по 2017 г. Почему? Мы не знаем точно — ни оригинальный
график, ни полный текст статьи (я проверила!) не проясняют этого. Мы знаем, что
абсолютное значение изменения (около $500 млрд) и пропорциональные изменения
значения достаточно велики и уникальны, поэтому концентрация внимания только
на годовом изменении не вводит в заблуждение. Если мы хотим уверить наших читателей в этом, лучше представить дополнительные подробности в таблице, где они
смогут подробно рассмотреть точные цифры, не отвлекаясь от главного.
Линейные диаграммы представляют собой важные визуальные формы, когда скорость изменения (отраженная в наклоне каждой линии) занимает центральное место в формулировке данных. Хотя этот тип графика не обязательно должен начинаться с нуля, он может использоваться только для представления данных с течением времени. Для обзора при работе с линейными графиками применяются
следующие правила:
Правило
Точки данных должны представлять значения с течением времени.
Рекомендация
Линии данных должны занимать примерно 2/3 площади диаграммы по вертикали.
Рекомендация
Четыре или менее линий должны быть четко окрашены/маркированы.
Хотя поначалу это может показаться нелогичным, на самом деле большое количество линий на линейной диаграмме — это вполне нормально. Конечно, при условии, что они оформлены так, чтобы не конкурировать с доказательствами нашей
формулировки данных. В следующем разделе показано, что такого рода "фоновые"
данные могут быть полезным способом предоставления дополнительного контекста для читателей, тем самым еще более эффективно поддерживая утверждения
вашего заголовка.
Диаграмма рассеяния
Хотя диаграммы рассеяния не часто используются в общем обмене данными, они
могут быть незаменимы в качестве точечного аналога линейных диаграмм, особенно когда у вас есть большое количество точек данных, которые иллюстрируют четкую тенденцию или отклонение от этой тенденции.
Рассмотрим график на рис. 10.8, который воспроизводит одну из статей New York
Times26, и иллюстрирует, как даже среди тысяч показаний температуры, получен-
26
https://nytimes.com/interactive/2021/06/29/upshot/portland-seattle-vancouver-weather.html
Представление данных
|
381
ных в течение более чем четырех десятилетий, три дня подряд в июне 2021 г. выходили далеко за пределы ожидаемого диапазона в городах от Портленда, штат Орегон, до Ванкувера, Канада. Хотя заголовок, безусловно, мог бы быть более ярким,
сама визуализация содержит четкое послание: максимальные температуры, зафиксированные в Портленде, штат Орегон, в течение трех дней в конце июня, были
выше, чем в любой другой день за последние 40 лет.
Рис. 10.8. Максимальные показатели температуры в Портленде по дням за 19792021 гг.
В большинстве случаев диаграммы рассеяния используются для отображения значений данных, полученных либо в течение времени (как на рис. 10.8), либо по отдельным членам некоторой "группы" (например, "школы", "большие города", "озера, питаемые реками" или "фильмы Marvel"). Иногда диаграммы рассеяния могут
включать рассчитанные линии тенденций. Такие линии служат эталоном для сравнения отдельных точек данных с "ожидаемыми" средними значениями. В иных
случаях эталоном может быть значение, определяемое профессиональными, юридическими или социальными нормами.
Например, черпая вдохновение из статьи Pioneer Press о школах, где ученики показывают лучшие результаты, чем можно было бы предположить27, можно использовать библиотеку seaborn для построения исторических данных школьной системы
Калифорнии, чтобы создать диаграмму рассеяния и выделить необычную точку
данных. Код для этого примера находится в листинге 10.4.
27
Schools That Work: Despite Appearances, Schools Doing Better than Expected Have Traits in Common,
автор Megan Boldt et al., 9 июля 2010 г., https://twincities.com/2010/07/09/schools-that-work-despiteappearancesschools-doing-better-than-expected-have-traits-in-common.
382
|
Глава 10
Листинг 10.4. schools_that_work.py
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
# импортируем данные школьного тестирования
school_data = pd.read_csv("apib12tx.csv")
# строим график зависимости результатов тестов от процента учащихся,
# обеспеченных питанием
sns.scatterplot(data=school_data, x="MEALS", y="API12B", alpha=0.6, linewidth=0)
# выделяем школу с высокими показателями
highlight_school = school_data[school_data['SNAME'] == \
"Chin (John Yehall) Elementary"]
plt.scatter(highlight_school['MEALS'], highlight_school['API12B'],
color='orange', alpha=1.0)
# выводим график!
plt.show()
Аргумент alpha регулирует непрозрачность точек; значение 60% (0,6 в десятичном исчислении) оказалось правильным балансом для разборчивости как отдельных, так и перекрывающихся точек. Аргумент linewidth=0 устраняет контуры вокруг каждой точки, которые мешают эффекту тепловой карты при регулировке непрозрачности.
Чтобы "выделить" школу, мы, по сути, просто создаем диаграмму рассеяния с
одной точкой в координатах x и y выбранной точки данных.
Одним из основных вызовов при использовании диаграмм рассеяния стала проблема окклюзии, когда точки данных могут накладываться друг на друга, тем самым
скрывая истинную плотность данных. Один из подходов к решению этой проблемы
заключается в добавлении джиттера — небольшого количества неупорядоченности в размещении отдельных точек, призванного минимизировать это визуальное
перекрытие. Однако, начиная с версии seaborn 0.11.2, джиттер указан как опциональный аргумент28, но числится как "не поддерживаемый". К счастью, можно сохранить точность данных без потери их интерпретируемости, регулируя непрозрачность или свойство alpha точек данных. Если сделать все точки в нашей визуализации в некоторой степени прозрачными, перекрывающиеся точки данных
превращаются в своего рода тепловую карту, управляемую непрозрачностью, которая проясняет тенденции без потери конкретности, как показано на рис. 10.9.
28
https://seaborn.pydata.org/generated/seaborn.scatterplot.html
Представление данных
|
383
Рис. 10.9. Некоторые школы не подпадают под общий паттерн
Когда же имеет смысл использовать диаграммы рассеяния?
Рекомендация
Данные должны быть достаточно объемными, чтобы были видны тенденции и
выбросы.
Рекомендация
Необходимо визуализировать соответствующие контрольные показатели, не
важно, вытекают ли они из данных или из внешних правил.
Рекомендация
Большая часть данных должна быть "фонового" цвета, с выделением не более
нескольких точек.
Рекомендация
Отрегулируйте непрозрачность или примените джиттер для минимизации окклюзии данных.
Картограммы
Для многих из нас картограммы — это одни из самых привычных типов визуализации. В зависимости от обстоятельств вы можете использовать картограммы для
планирования маршрута на работу или в школу, для поиска нового магазина или
ресторана, а также для поиска нового парка или велосипедной дорожки. Картограммы также стали распространенной визуальной формой в массовых коммуникациях, где они появляются в виде карт погоды, карт выборов или даже карт "локаторов", которые обеспечивают ориентиры для незнакомых мест. Если в данных есть
384
|
Глава 10
географическая составляющая, то вполне естественно рассмотреть возможность их
картографирования.
Однако в действительности, если ваши данные не связаны с географией, вам не
стоит наносить их на карту. Почему? Потому что карты представляют участок земли. Не преобладание и даже не численность населения, о чем обычно говорят наши
данные. Например, давайте вспомним данные по ссудам PPP из главы 6. Если бы
вы сгруппировали количество одобренных кредитов по штатам, используя функцию value_counts('ProjectState'), то получили бы следующий результат (переформатированный в столбцы для экономии места):
CA
TX
NY
FL
IL
PA
OH
NJ
MI
MA
GA
WA
99478
60245
54199
46787
33614
30768
26379
24907
24208
21734
20069
18869
VA
NC
MN
CO
MD
WI
IN
MO
TN
AZ
OR
LA
18682
18022
16473
15662
15170
14729
13820
13511
12994
12602
10899
10828
CT 10197
AL 9025
OK 8598
SC 8522
UT 7729
KY 7623
IA 7003
KS 6869
NV 6466
NE 4965
AR 4841
MS 4540
NH
ID
NM
ME
HI
DC
RI
WV
MT
ND
DE
SD
4197
3697
3524
3490
3414
3175
3012
2669
2648
2625
2384
2247
AK
PR
VT
WY
GU
VI
MP
AS
2076
2032
1918
1791
305
184
55
18
Без особых усилий можно догадаться, что порядок появления штатов в этой таблице похож на их порядок в таблице29, где штаты ранжированы по численности населения. Другими словами, если бы мы "нанесли на карту" данные об одобрении кредитов PPP, то в итоге получилась бы карта населения. Но, допустим, мы исправили
это и нормализовали количество ссуд по численности населения, и таким образом
создали новый столбец под названием "Одобренные кредиты на человека" или чтото подобное. Даже теперь, когда мы преобразовали свои данные из населения в
преобладание, мы на самом деле просто создали себе проблему гистограммы в географической форме: фактическая визуальная площадь, занимаемая конкретным
штатом, просто не пропорциональна отображаемым данным. Как бы мы ни выбирали цветовую палитру или диапазоны данных, Делавэр будет занимать 1/50 визуального пространства Вайоминга, несмотря на то, что у него на 25% больше одобренных кредитов. Составляя карту этих данных, мы просто гарантируем, что наше
визуальное представление работает с реальными данными.
Очевидно, что существует множество визуализаций карт, и многие из них, вероятно, допускают несколько из описанных мной ошибок. Картографирование предлагает такой, казалось бы, простой принцип визуальной организации, что многие люди не могут устоять перед соблазном его применения. Но использование его для
негеографических данных на самом деле наносит вред как самим данным, так и
вашим читателям.
29
https://en.wikipedia.org/wiki/List_of_U.S._states_and_territories_by_population
Представление данных
|
385
Конечно, существуют прекрасные карты, посвященные действительно географическим явлениям: например, эти карты переписи населения в New York Times предлагают продуманный подход к представлению данных переписи населения30, а работа
ProPublicas по зонам затопления в Хьюстоне31 иллюстрирует, насколько важна география (как природная, так и созданная человеком) во время экстремальных погодных явлений. А для красивого и оригинального представления данных о ветре посмотрите эту карту ветров на hint.fm32.
Учитывая, насколько редко карты оказываются подходящей визуализацией для
поддержки ваших утверждений о данных (а также то, насколько сложным может
быть их построение в Python), не будем приводить здесь пример кода для построения карт. Если вы работаете с данными, которые, по вашему мнению, действительно необходимо картографировать, я рекомендую обратить внимание на библиотеку
geopandas33. Она предназначена для легкого объединения информации о форме,
связанной с картографированием, с DataFrames библиотеки pandas для создания
визуализаций.
Возьмите в руки бумагу
Хотя вашим визуализациям, вероятно, суждено существовать в цифровом
формате, я настоятельно рекомендую, когда придет время начать процесс проектирования, отойти от клавиатуры и взять в руки ручку (карандаш или мелок). Не волнуйтесь, если вы "не умеете рисовать". Ваша цель на этом этапе —
выявить (и, возможно, решить) визуальные проблемы без предвзятого отношения к тому, что могут или не могут сделать ваши цифровые инструменты. Если
ваша визуализация "бумажных прототипов"34 в итоге больше похожа на набросок, чем на иллюстрацию, это даже лучше: исследования пользовательского
опыта показывают, что если образец дизайна слишком подробный или точный,
зрители скорее всего будут зацикливаться на деталях, таких как конкретный
размер шрифта или используемые цвета, а не на "более широкой картине" вопросов о расположении и смысле. Наброски идей визуализации на бумаге (или
доске) в качестве первого шага станут еще и эффективным способом опробовать различные визуальные формы для ваших данных, оставаясь при этом дешевыми и простыми.
Лично я считаю, что создание эскиза помогает быстрее понять, действительно
ли данная визуальная форма (например, гистограмма) иллюстрирует нужную
взаимосвязь между элементами данных.
30
https://nytimes.com/interactive/2015/07/08/us/census-race-map.html
https://projects.propublica.org/graphics/harvey-maps
32
http://hint.fm/wind
33
https://geopandas.org/index.html
34
https://en.wikipedia.org/wiki/Paper_prototyping
31
386
|
Глава 10
Это также облегчает рассмотрение и изменение таких вещей, как выделение
данных, аннотации, шкалы и метки осей, которые часто составляют разницу
между приемлемым и отличным графиком. Я также заметила, что если начать
с рисования визуализации, то когда приходит время приступать к программированию, создание окончательной (цифровой) визуализации происходит в дватри раза быстрее.
Один совет относительно этого процесса: нет необходимости заботиться о
тщательном учете каждой точки данных, когда вы делаете эти наброски. Вам
необходимо получить общее представление о том, какой тип визуализации
лучше всего подходит для ваших данных, и принять ключевые решения о том,
что будет выделено, а что останется за кадром. Даже не имея под рукой точных значений данных, наброски идей позволят сделать обзор, поделиться и
получить мнения о них. Этого нельзя будет сделать, когда вы уже зафиксируете свои решения в коде.
Элементы красноречивых визуальных эффектов
Хотя бóльшую часть своей карьеры я была информационным дизайнером, я не считаю себя настоящим графическим дизайнером. Я не смогу сделать хороший логотип для вашей компании, так же как и вы, получив достаточно знаний по программированию на Python, не сможете устранить неполадки с принтером. К счастью,
благодаря учебе, большому количеству прочитанной информации, нескольким курсам и щедрости многих талантливых дизайнеров, с которыми я работала, я узнала
достаточно много об эффективном визуальном дизайне в целом и информационном
дизайне в частности. Мои выводы, сделанные за несколько десятилетий работы,
изложены здесь.
"Мелкие" детали действительно имеют значение
Миллион лет назад, когда я только начинала работать фронтенд-программистом в
веб-стартапе, моим приоритетом номер один было создание вещей, которые работают, и, честно признаться, я была очень рада, когда они работали. Со временем я
даже доработала и провела рефракторинг своего кода некоторыми из способов,
применявшихся в главе 8, и я была вполне довольна этими артефактами программирования. С точки зрения программиста, моя работа была довольно чистой.
Но я работала в команде по разработке дизайна, и коллеги дизайнеры постоянно
подталкивали меня к исправлению мелких деталей, которые казались мне скорее
"приятным дополнением", чем необходимостью. Неужели действительно так важно, плавно ли сменяются фотографии в слайд-шоу или слегка перескакивают, когда
занимают свои места? В то время программирование подобных эффектов означало
написание и корректировку (очень приблизительную) уравнений физики, чего я
делать не любила. Кроме того, вся эта кастомизация загромождала код.
Представление данных
|
387
Но больше всего в программировании мне нравится решать проблемы, поэтому в
конце концов я смирилась и внесла изменения, о которых они просили. И как только эти изменения были запущены, я поняла, насколько более отшлифованным и
удовлетворительным был конечный результат. Я начала понимать, что качество
дизайна заключается в этих "маленьких" деталях и что такие "мелочи", как изменение цвета при тапе или щелчке мышью, на самом деле делают мои интерфейсы и
графику более понятными и удобными для использования. Другими словами, не
отмахивайтесь от "деталей" дизайна — они нужны не только для того, чтобы все
"выглядело красиво". Дизайн — это то, как все работает.
Доверяйте своим глазам (и экспертам)
Визуальные элементы, отображаемые в цифровом виде, обязательно выражаются в
количественных терминах: математическое происхождение шестнадцатеричных
цветовых кодов35 и позиционирования координат x/y может создать впечатление,
что поиск "правильных" цветов для графика или правильного позиционирования
метки описания — это вопрос правильной математики. Это не так. Например, восприятие цвета является одновременно сложным и очень индивидуальным — мы не
можем быть уверены, что другие люди видят "тот же самый" цвет, что и мы, и, конечно, существует множество видов цветовой "слепоты", которая может помешать
некоторым людям вообще воспринимать разницу между определенными парами
взаимодополняющих цветов (например, красный/зеленый, синий/оранжевый, желтый/фиолетовый). Не существует уравнения, способного это учесть.
На самом деле, каждый "цвет", с которым мы можем работать, характеризуется
тремя различными свойствами: оттенком (например, красный или синий), яркостью
(или светимостью) и насыщенностью. Цвета, которые "сочетаются" друг с другом,
выравнивают или контрастируют по этим признакам определенным образом. Когда
дело доходит до визуализации, необходимо, чтобы цвета не просто хорошо смотрелись вместе, — они должны содержательно кодировать информацию. Но что значит для цвета "на 20% более синий", чем другой? Что бы это ни было, это не просто
вопрос перестановки цифр в шестнадцатеричном значении цвета36.
К счастью, можно обратиться за помощью к экспертам. Более двух десятилетий
все, кто ищет советы по выбору цвета (в основном для картограмм, хотя это также
отличная сфера для других типов диаграмм), могут обратиться к работе Синтии
Брюэр37, картографа и профессора географии в университете Penn State, чей инструмент ColorBrewer38 предоставляет отличные бесплатные распределения цветов
для визуального дизайна. Аналогичным образом, в превосходной книге Донны Вонг
The Wall Street Journal Guide to Information Graphics (Norton) приведены некоторые
из моих личных фаворитов, когда речь идет о цветовых сочетаниях для графики.
35
https://computerhope.com/htmcolor.htm
Поверьте, я пыталась.
37
http://personal.psu.edu/cab38
38
https://colorbrewer2.org/
36
388
|
Глава 10
Если вам очень хочется выбрать свою собственную цветовую палитру, то наиболее
оптимальный подход, который у нас есть, — это обратиться к самому большому
авторитету в области цвета: к природе. Найдите фотографию из мира природы (фотографии цветов работают особенно хорошо) и используйте инструмент захвата
цвета, чтобы выбрать контрастные цвета, если они вам нужны, или несколько оттенков одного цвета. Используйте их по умолчанию для обновления настроек в любом пакете визуализации, и вы оцените, насколько более привлекательной и профессионально выглядящей стала ваша графика.
Когда дело доходит до таких вещей, как размер шрифта и расположение надписей,
здесь также нет никаких уравнений, которым нужно следовать. Обычно достаточно
просто визуально оценить свою графику и подправить ее, если она кажется не совсем удачной. Например, я отчетливо помню, как программировала дизайн для одного из моих коллег в The Wall Street Journal. В том проекте был список элементов
макета, и, естественно, я написала цикл for для их точного размещения. Проблема в
том, что при выполнении кода и его рендеринге что-то выглядело не так. Убедившись, что я просто плохо оценила расстояние между элементами, я спросила его,
сколько пикселей белого пространства должно быть между каждым элементом. "Я
не знаю, — сказал он, — я просто определяю расстояние на глаз".
Хотя я понимаю, что вы можете растеряться от подобных советов, когда только
начинаете заниматься визуальным дизайном, но могу уверить, что если вы найдете
время поэкспериментировать, то в конце концов научитесь доверять собственным
глазам. С таким подходом, практикой и вниманием к (четко определенным) деталям, изложенным в следующих разделах, вы вскоре будете создавать визуализации,
которые будут одновременно и точными, и красноречивыми.
Выбор масштаба
В разд. "Диаграммы, графики и картограммы — вот это да!" мы рассматривали
вопрос масштаба в отношении точности визуализаций. А здесь мы сосредоточимся
на ясности и читаемости. Такие графические пакеты, как seaborn и matplotlib, автоматически выбирают масштабы и пределы осей на основе ваших данных, но эти
значения по умолчанию по разным причинам могут потребовать корректировки.
После определения подходящего числового диапазона для данных необходимо
проанализировать, как на самом деле отображается ваш график, и убедиться, что он
соответствует общим правилам:
пределы осей и отмеченные значения должны быть целыми числами и/или крат-
ными 5 или 10;
для обозначения значений нельзя использовать сугубо научную терминологию;
единицы измерения должны быть указаны только на последнем элементе каж-
дой оси;
метки могут потребовать редактирования или (что менее предпочтительно) на-
клона, чтобы оставаться читаемыми.
Представление данных
|
389
Выбор цветовой гаммы
Помимо обращения к специалисту за советом по выбору конкретного цвета, подумайте о том, сколько цветов должно быть у элементов данных. Цвет может стать
бесценным способом выделить конкретную точку данных или провести различие
между измеренными и прогнозируемыми значениями. Выбирая цвета для диаграммы или графика, помните:
Один цвет для каждой категории данных
Если вы, к примеру, отображаете данные за несколько месяцев о программе кредитования PPP, все столбцы должны быть одного цвета. Аналогично, различные
значения одной и той же переменной должны быть оттенками одного цвета.
Избегайте непрерывного распределения цветов
Хотя настройка цвета каждого визуального элемента в соответствии с его значением может показаться более точной, подобно визуальному сжатию данных, которое мы видели на рис. 10.5, непрерывные цветовые палитры (или рампы) создают настолько незначительные цветовые различия, что человеческий глаз не
может их воспринять. Именно здесь пригодятся ваши расчеты распределения
(вы ведь делали их, верно?): создайте цветовую шкалу (или рампу) до пяти цветов, а затем присвойте каждый из них одному квинтилю ваших данных39.
Используйте расходящиеся цветовые шкалы с осторожностью
Расходящиеся цветовые шкалы подходят только тогда, когда есть истинное
"нейтральное" значение, вокруг которого варьируются данные. В некоторых
случаях этот показатель может быть равен нулю. В других случаях это может
быть согласованное значение в данной области (например, Федеральная резервная система США считает уровень инфляции около 2%40 идеальным).
Никогда не выделяйте цветом более четырех отдельных категорий данных
Но включение контекстных данных в серый фон — это нормально.
Проверяйте цвета на доступность
Такие инструменты, как ColorBrewer, поддерживают возможность создания
комбинаций цветов, различимых для людей с дальтонизмом. Если вы используете собственные цвета, протестируйте выбранные варианты, преобразовав графику в оттенки серого. Если вы все еще можете различать все цвета в своей визуализации, то и ваши читатели должны быть в состоянии это сделать.
Прежде всего делайте аннотации!
Цель процесса визуализации — поделиться знаниями о данных и поддержать свои
утверждения. Хотя заголовок с глаголами действия должен отражать основную
39
Вы можете быстро выполнить это в библиотеке pandas, используя функцию quantile() и передав
значения 0.2, 0.4, 0.6 и т. д. Для более общего ознакомления с тем, как рассчитать эти значения и что
они собой представляют, см. листинг 9.3 в главе 9.
40
https://federalreserve.gov/faqs/5D58E72F066A4DBDA80BBA659C55F774.htm
390
|
Глава 10
идею графика, бывает полезно выделить или добавить контекст к конкретным точкам данных в самой визуализации. Здесь не место для звездочек и сносок. Информация, необходимая для точного или эффективного понимания графика, должна
быть частью основного визуального поля графика. Некоторые из основных способов сделать данные в визуализации более понятными включают следующее:
Непосредственно помечать различия категорий
Вместо того чтобы создавать отдельную легенду, разместите метки данных непосредственно на визуализации. Это избавит читателей от необходимости переходить от одного графика к другому, чтобы понять смысл представленной информации.
Выделять цветом важные точки данных
Если одна ключевая точка данных является основной для вашего общего утверждения, выделите ее контрастным цветом.
Добавлять контекстные аннотации
Эти небольшие объемы текста (при необходимости соединенные с соответствующим элементом данных связующей линией) могут быть надписью, объяснением или важной контекстуальной информацией. Что бы это ни было, убедитесь, что оно отображается как можно ближе к данным и всегда находится в
пределах визуальных границ самой графики.
От базового к красивому: настройка визуализации
с помощью seaborn и matplotlib
И последнее слово о дизайне — визуальном или ином. Хотя в разд. "Элементы
красноречивых визуальных эффектов" я постаралась разложить элементы эффективной визуализации на составные части, по-настоящему красноречивая графика —
это не набор взаимозаменяемых частей. Изменение одной ее части — перемещение
метки, изменение цвета — означает, что многие, если не все, оставшиеся элементы
необходимо скорректировать, чтобы привести целое в равновесие. Именно поэтому
я не привожу примеры кода для каждого отдельного аспекта дизайна, описанного
ранее. Изолированное рассмотрение отдельных элементов может затруднить понимание, почему тот или иной элемент так важен. Но если рассматривать их как часть
единого целого, то (надеюсь) станет ясно, как каждый из них вносит свой вклад в
воздействие графики на человека.
Чтобы реализовать индивидуальные визуальные эффекты в Python, мы попрежнему будем полагаться на библиотеки seaborn и matplotlib. Но хотя в предыдущих случаях мы позволяли seaborn делать бóльшую часть тяжелой работы, в
этом примере именно тонкий контроль, предлагаемый matplotlib, будет действительно блистать. Да, мы по-прежнему будем позволять библиотеке seaborn выполнять задачи высокого уровня, такие как построение графиков данных в масштабе.
Но matplotlib даст нам инструмент, необходимый для указания всех тонкостей: от
размещения и ориентации меток до единиц измерения, меток, аннотаций и выде-
Представление данных
|
391
ляемых значений — т. е. всего, что нам нужно, чтобы действительно адаптировать
нашу визуализацию к своим требованиям.
В этом примере мы отвлечемся от данных PPP и вместо этого будем работать с
данными по COVID-19, собранными группой исследователей на основе данных
Университета Джона Хопкинса и размещенными на сайте Our World in Data41. Цель
этой визуализации — подчеркнуть всплеск подтвержденных случаев заболевания
COVID-19 в США в июле 2020 г., который объяснялся ускоренным возобновлением работы42 во многих штатах, произошедшим весной43, а также массовыми мероприятиями во время празднования 4 июля44. Чтобы понять разницу между использованием значений по умолчанию и настройкой диапазонов, меток и цветов осей,
сравните рис. 10.10 с рис. 10.11. Код для создания рис. 10.11 показан в листинге 10.5.
Рис. 10.10. Гистограмма, созданная библиотекой визуализации
со значениями по умолчанию
41
https://ourworldindata.org/coronavirus-source-data
How Coronavirus Cases Have Risen Since States Reopened, автор Lazaro Gamio, New York Times, 9
июля 2020 г., https://nytimes.com/interactive/2020/07/09/us/coronavirus-cases-reopening-trends.html.
43
Coronavirus Cases Rose by Nearly 50 Percent Last Month, Led by States That Reopened First, авторы Anne Gearan, Derek Hawkins и Siobhа́n O’Grady Washington Post, 1 июля 2020 г.,
https://washingtonpost.com/politics/coronavirus-cases-rose-by-nearly-50-percent-last-month-led-bystatesthat-reopened-first/2020/07/01/3337f1ec-bb96-11ea-80b9-40ece9a701dc_story.html.
44
California COVID-19 Cases Spiked after July 4th. Family Gatherings Helped the Spread, Experts Say.
авторы Mark Olalde и Nicole Hayden, USA Today, 2 августа 2020 г., https://usatoday.com/story/
news/nation/2020/08/02/ covid-19-spike-california-after-july-4-linked-family-gatherings/5569158002.
42
392
|
Глава 10
Рис. 10.11. Кастомная визуализация
Листинг 10.5. refined_covid_barchart.py
# `pandas` для загрузки данных; `seaborn` и `matplotlib` для визуальных эффектов
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
# `FuncFormatter` для форматирования меток осей
from matplotlib.ticker import FuncFormatter
# `datetime` для интерпретации и настройки дат
from datetime import datetime
# загружаем данные
vaccine_data = pd.read_csv('owid-covid-data.csv')
# преобразуем столбец `date` в "реальную" дату
vaccine_data['date']= pd.to_datetime(vaccine_data['date'])
Представление данных
# сгруппируем данные по стране и месяцу
country_and_month = vaccine_data.groupby('iso_code').resample('M',
on='date').sum()
# используем `reset_index()` для "сглаживания" заголовков DataFrame
country_and_month_update = country_and_month.reset_index()
# выберем данные только по Соединенным Штатам
just_USA = country_and_month_update[country_and_month_update['iso_code']=='USA']
# создадим базовую гистограмму с помощью `seaborn`.
ax = sns.barplot(x="date", y="new_cases", palette=['#bababa'], data=just_USA)
# переберем циклом прямоугольники столбцов и установим цвет для столбца
# июля 2020 г. в красный
for i, bar in enumerate(ax.patches):
if i == 6:
bar.set_color('#ca0020')
# устанавливаем максимальное значение оси y на 7 млн
ax.set_ylim(0,7000000)
# устанавливаем метки осей
plt.xlabel('Month')
plt.ylabel('New cases (M)')
# изменяем цвет, расположение и ориентацию "меток осей"
ax.tick_params(direction='out', length=5, width=1, color='#404040',
colors='#404040',pad=4, grid_color='#404040', grid_alpha=1,
rotation=45)
# функции для форматирования "меток осей"
# `millions()` преобразует научные понятия в миллионы случаев заражения
def millions(val, pos):
modified_val = val*1e-6
formatted_val = str(modified_val)
if val == ax.get_ylim()[1]:
formatted_val = formatted_val+'M'
if val == 0:
formatted_val = "0"
return formatted_val
# `custom_dates()` будет сокращать даты для более удобного чтения
def custom_dates(val, pos):
dates_list = just_USA.date.tolist()
date_label = ""
|
393
394
|
Глава 10
if pos is not None:
current_value = dates_list[pos]
current_month = datetime.strftime(current_value, '%b')
date_label = current_month
if date_label == 'Jan':
date_label = date_label + " '"+ datetime.strftime(current_value,
'%y')
return date_label
# назначаем функции formatter
y_formatter = FuncFormatter(millions)
x_formatter = FuncFormatter(custom_dates)
# применяем функции formatter к соответствующей оси
ax.yaxis.set_major_formatter(y_formatter)
ax.xaxis.set_major_formatter(x_formatter)
# создаем и располагаем текст аннотации
ax.text(4, 3000000, "Confirmed cases\noften lag infection\nby several weeks.")
# получаем значение всех столбцов в виде списка
bar_value = just_USA.new_cases.tolist()
# создаем связующую линию
ax.vlines( x = 6, color='#404040', linewidth=1, alpha=.7,
ymin = bar_value[6]+100000, ymax = 3000000-100000)
# устанавливаем заголовок графика
plt.title("COVID-19 cases spike following relaxed restrictions\n" + \
"in the spring of 2020", fontweight="bold")
# выводим график!
plt.show()
Настройка ориентации, цвета и других атрибутов "меток осей", маркирующих
значения на каждой оси графика, может быть выполнена с помощью метода
tick_params() из библиотеки matplotlib45.
Функции FuncFormatter будут назначаться пользовательские функции. Любая из
таких функций будет получать «значение» и позицию «метки оси» в качестве аргументов.
В "интерактивном режиме", если pos будет содержать значение None, эта функция будет выдавать ошибки.
45
https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.tick_params.html
Представление данных
|
395
По умолчанию позиционирование текстовых элементов, расположенных поверх
графика46, осуществляется по "координатам данных", например, значение 1 выравнивает начало текста влево по центральной точке первого столбца. Введенное значение "y" закрепляет нижнюю часть текстового поля.
Как видно из листинга 10.5, процесс кастомизации нашей визуализации в Python
представляется несколько сложным; адаптация внешнего вида и настроек вывода
по умолчанию почти в три раза увеличила количество необходимых строк кода.
В то же время вывод по умолчанию в этом случае был практически нечитаемым.
Смягчив цвета, выделив важные точки данных и (что, возможно, наиболее важно)
усовершенствовав метки данных, нам удалось создать график, который может занять достойное место в большинстве контекстов публикации. И очевидно, что
большую часть приведенного кода можно доработать и перепрофилировать, чтобы
сделать эту степень кастомизации гораздо менее сложной для создания будущей
графики.
Выйдите за рамки основ
Хотя в этой главе я постаралась охватить основные принципы эффективной и точной визуализации данных, правда в том, что ценные идеи для визуализации могут
прийти откуда угодно и далеко не всегда из книг или блогов, посвященных этой
теме. Одним из моих любимых графиков времен работы в Wall Street Journal стала
визуализация безработицы47, представленная на рис. 10.12. На создание этой визуализации меня вдохновила похожая форма, которую я встретила на музейной выставке, посвященной изменению климата, — геометрический формат тепловой карты позволяет читателям наглядно сравнить десятилетия ежемесячных уровней безработицы. Если вы интересуетесь дизайном, вы можете многое узнать о том, что
работает (и не работает), просто внимательно взглянув на средства массовой информации, постоянно окружающие вас — будь то реклама в Интернете или меню в
ресторане. Если что-то не кажется отшлифованным или привлекательным, внимательно изучите все составляющие. Цвета несочетаемы? Трудно ли читать шрифт?
Или просто слишком много элементов теснятся в слишком маленьком пространстве?
Критиковать работу других, конечно, легко. Если вы действительно хотите улучшить свои визуализации, бросьте себе вызов и попробуйте создать лучшие решения, и вы узнаете очень много нового. И если вы заметите, что ищете подходящую
лексику для описания того, что не работает, можете просмотреть некоторые ресурсы в приложении Г. Там указаны некоторые из моих любимых ресурсов для расширения и улучшения работы по визуализации.
46
47
https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.text.html
https://jennifervalentinodevries.com/2009/09/16/grid-graphic-u-s-unemployment-rate
396
|
Глава 10
Рис. 10.12. Безработица в США с течением времени
(первоначально разработано для Wall Street Journal)
Заключение
Как и программирование, визуализация — это прикладное искусство: единственный способ повысить свои навыки — это практика. Если у вас нет под рукой проекта, над которым можно поработать, начните с "найденной" визуализации, которую вы считаете некачественной, и переделайте ее самостоятельно. Можно использовать Python, или другие вычислительные инструменты, или просто карандаш и
бумагу — важно попытаться решить проблемы, которые вы видите в оригинале. На
этом пути вы на собственном опыте убедитесь в компромиссах, присущих каждому
дизайнерскому решению, и начнете развивать навыки, необходимые для минимизации этих компромиссов в интересах достижения ваших целей визуализации. И
когда вы столкнетесь с собственными проблемами визуализации данных, у вас будет портфолио предыдущих работ. К такому архиву можно будет обратиться при
размышлениях о том, как лучше представить имеющиеся данные и выполнить свою
работу.
На данный момент мы выполнили практически все поставленные задачи по работе
с данными с помощью Python. Но за пределами Python существует целый мир инструментов для работы с данными и визуализации, которые могут быть удивительно полезны для поддержки вашей работы по выпасу и повышению качества данных. Мы обратимся к ним в главе 11.
ГЛАВА 11
За пределами Python
Python — исключительно мощный и универсальный инструмент для работы с данными, и если вы следили за упражнениями в этой книге, то, надеюсь, уже чувствуете себя более уверенно, используя его для продвижения своих собственных проектов по выпасу данных. Благодаря активному сообществу Python и постоянно развивающемуся набору полезных библиотек, создаваемых и поддерживаемых членами
сообщества, работа, которую вы затратили на изучение основ, изложенных в этой
книге, будет ценной независимо от того, будет ли ваш следующий проект по выпасу данных завтра или через год. Кроме того, хотя Python как язык программирования во многом уникален, навыки программирования и приобретенный здесь словарный запас дадут вам преимущество при работе с другими языками программирования, особенно такими объектно-ориентированными, как JavaScript.
Тем не менее на протяжении всей книги я старалась разъяснить одну вещь: бывают
случаи, когда "программное" решение проблемы фактически будет не самым эффективным. Например, работа с файлами Excel и XML в главе 4 показала, что иногда попытки сделать что-то программно просто не имеют смысла. Например, хотя в
листинге 4.12 можно было бы написать скрипт на Python для просмотра всего нашего XML-документа с целью выявления его структуры, несомненно, быстрее и
проще было просто просмотреть данные, определить интересующие элементы и
написать программу на Python непосредственно для них. Более того, бывают случаи, когда написание программы на Python может в целом потребовать больше усилий, чем действительно необходимо для конкретного проекта по выпасу данных,
особенно если он меньше или носит исследовательский характер. Хотя pandas —
невероятно полезная библиотека, вам все равно может понадобиться написать достаточно много кода только для того, чтобы получить базовое представление о содержимом нового датасета. Другими словами, хотя я полностью уверена, что мощь
и универсальность Python делает его незаменимым инструментом для работы с
данными, я хочу выделить несколько других бесплатных инструментов и/или инструментов с открытым исходным кодом, которые будут полезными1 для вас как дополнение к Python в проектах по работе с данными.
1
Я определенно их таковыми считаю!
398
|
Глава 11
Дополнительные инструменты для анализа данных
Python обеспечивает высокую скорость и гибкость при работе с данными, но он не
очень хорошо подходит для того, чтобы получить возможность реально посмотреть
на ваши данные. Поэтому на протяжении всей этой книги мы полагались на основные текстовые редакторы (и иногда на веб-браузеры), когда хотели визуально просмотреть датасеты. Хотя текстовые редакторы отлично подходят для первого шага
в этом направлении, вам также стоит освоить хотя бы одну из следующих программ, чтобы получить быстрый первоначальный обзор данных — особенно если
обрабатываемые файлы не слишком большие.
Программы для работы с электронными таблицами
Вполне возможно, что еще до того, как начать читать эту книгу, вы уже были знакомы с программами электронных таблиц — будь то онлайн-версии, такие как
Google Sheets, платные локальные варианты программного обеспечения, такие как
Microsoft Excel, или альтернативы с открытым исходным кодом, такие как Libre
Office Calc. Программы для работы с электронными таблицами обычно поставляются в комплекте с программными пакетами "офисного" типа и предлагают базовые функции расчетов, анализа и построения графиков. В целом, нет большого различия в том, что могут делать эти программы, хотя некоторые из них более гибкие.
Например, я предпочитаю программу LibreOffice2, потому что она бесплатная, с
открытым исходным кодом и работает на всех платформах (включая менее распространенные, такие как Linux). У нее даже есть сертифицированное приложение для
Chromebooks и устройств Android под названием Collabora3. Тем не менее, если у
вас уже есть определенная программа для работы с электронными таблицами или
вы хорошо знакомы с ней, нет причин для перехода на какую-либо другую, если
только вас не разоряет оплата текущего приложения. Что бы вы ни делали, не используйте пиратское программное обеспечение. В мире, где троян-вымогательские
программы распространяются в огромных количествах, риск для ваших устройств
и данных просто огромен, и это не стоит того!
Хотя многие программы для работы с электронными таблицами поддерживают
расширенные функции, которые примерно соответствуют возможностям Python (в
гораздо меньшем масштабе), я обычно обращаюсь к ним для решения очень специфических задач по выпасу и оценке данных. В частности, я часто использую программу электронных таблиц для быстрого выполнения следующих действий:
Изменение форматов файлов
Например, если мои данные предоставлены в виде многолистового файла XLSX,
я могу открыть его в программе обработки электронных таблиц и сохранить
только интересующий меня лист в формате .csv.
2
3
https://libreoffice.org/
https://collaboraoffice.com/press-releases/collabora-office-ships-for-chromebooks
За пределами Python
|
399
Переименование столбцов
Если столбцов не так много, я могу заменить столбцы с неудобными или неясными заголовками на что-то более читабельное и/или интуитивно понятное для
моих целей.
Получение "ощущения" значений данных
Являются ли представленные в столбце "date" значения датами на самом деле?
Или здесь просто обозначены годы? Много ли явно отсутствующих значений
данных? Если мой датасет относительно невелик, простого визуального изучения данных в программе электронных таблиц иногда бывает достаточно, чтобы
определить, есть ли требуемые данные или стоит продолжать поиск.
Создание базовой сводной статистики
Конечно, я могу сделать это в Python, и в основном я так и делаю. Но если у меня всего несколько сотен строк данных, набрать выражение =MEDIAN() и затем
выбрать интересующие ячейки иногда быстрее, особенно если исходный файл
данных содержит метаданные, которые в противном случае пришлось бы удалить (как показано в главе 4 и снова в главах 7 и 8).
Конечно, у каждого инструмента свои недостатки, и предварительный просмотр
данных в программе для работы с электронными таблицами может привести к неожиданным результатам. Как вы могли догадаться из наших продолжительных
приключений с "датами" в стиле XLS, предварительный просмотр файла, содержащего значения, похожие на даты, может привести к тому, что они будут отображаться очень по-разному. Это зависит от конкретной программы электронных таблиц и ее способов обработки и отображения дат по умолчанию. Поэтому всегда надо проверять значения, похожие на дату, с помощью текстового редактора, если
исходный формат данных был текстовым (например, .csv, .tsv или .txt). Аналогичным образом, обязательно проверьте форматирование (по умолчанию или примененное) в любых ячейках, содержащих числа, поскольку сокращение или округление значений может скрыть важные изменения в данных.
OpenRefine
Один из инструментов, к которому я чаще всего обращаюсь для первоначального
изучения больших структурированных датасетов, — это OpenRefine4. По личному
опыту могу сказать, что OpenRefine представляет собой уникальный программный
инструмент, помогающий преодолеть разрыв между традиционными программами
электронных таблиц и полноценными языками программирования, такими как Python. Как и программы для работы с электронными таблицами, OpenRefine работает
через графический интерфейс пользователя (GUI), поэтому бóльшая часть вашей
4
https://openrefine.org/
400
|
Глава 11
работы с ним будет заключаться в наведении и щелчках мышью. Однако, в отличие
от электронных таблиц, вы не прокручиваете строки данных, чтобы получить представление о том, что они содержат. Вместо этого вы можете использовать опции
меню для создания фасетов, которые предоставляют сводную информацию, аналогичную предоставляемой методом value_counts()5 из библиотеки pandas, — но без
необходимости писать какой-либо код. OpenRefine также поддерживает пакетное
редактирование, реализует несколько алгоритмов для сопоставления строк (включая метод фингерпринтинга, который мы использовали в листинге 6.11) и может
импортировать большие файлы по сегментам (например, 100 000 строк за раз). По
правде говоря, OpenRefine часто становится моей первой остановкой при выпасе
нового датасета, поскольку он легко открывает различные форматы данных и даже
предлагает удобный "живой" предварительный просмотр того, как данные, например, будут разобраны на основе вашего выбора разделителей. После загрузки вашего датасета OpenRefine также может практически в один клик дать ответы на вопросы типа "Какое значение наиболее часто встречается в столбце x?". Наконец,
каждый раз, когда вы вносите фактические изменения в файл данных в OpenRefine
(в отличие от кластеризации или фасетирования), он автоматически записывает
ваши действия в экспортируемый файл в формате .json. Его затем можно применить к другому файлу OpenRefine, чтобы эти действия автоматически повторились
(обычно в течение нескольких секунд). Это невероятно полезно, если вам нужно
переименовать или переставить столбцы в датасете, который регулярно обновляется поставщиком данных. При этом он еще более полезен, если вам нужно, чтобы
кто-то другой мог это сделать, а у этого человека нет навыка или возможности использовать Python.
В основном я использую OpenRefine, чтобы легко выполнять следующие действия:
Предварительный просмотр небольших фрагментов больших датасетов
Инструмент OpenRefine позволяет загружать (и пропускать) требуемое количество строк в датасете. Это особенно удобно для больших датасетов, когда я хочу
получить разумное представление о том, что они содержат, но не знаю о них
практически ничего. Я могу начать с загрузки 50 000 или 100 000 строк и использовать фасеты и другие функции, чтобы получить общее представление,
скажем, о типах данных и об организации всего датасета.
Получение быстрой оперативной информации о датасете
Какой тип разрешения на съемки в Нью-Йорке запрашивают чаще всего6? А какой район самый популярный? Как показано на рис. 11.1, OpenRefine может
предоставить эти подсчеты за один или два щелчка мыши и позволяет так же
быстро создавать перекрестные таблицы.
5
6
https://pandas.pydata.org/docs/reference/api/pandas.Series.value_counts.html
https://data.cityofnewyork.us/City-Government/Film-Permits/tg4x-b46p
За пределами Python
|
401
Выполнять основные преобразования, которые не поддерживают программы
электронных таблиц
В некоторых программах по работе с электронными таблицами не хватает функциональности, например, возможности разделить строку на определенные символы, или поддержка регулярных выражений может быть ограничена. Одна из
моих любимых функций OpenRefine — пакетное редактирование, которое можно легко и быстро выполнить с помощью окна фасетного поиска слева.
Автоматическая генерация макросов
Многие программы электронных таблиц позволяют записывать макросы, автоматизирующие определенные действия, но OpenRefine записывает их по умолчанию, что делает его более мощным инструментом с меньшей кривой обучения
для выполнения такого рода задач.
Рис. 11.1. Разрешения на съемку в Нью-Йорке в программе OpenRefine
Конечно, есть некоторые аспекты работы с OpenRefine, к которым нужно привыкнуть. Во-первых, хотя процесс установки становится все более удобным, он зависит
от наличия на вашем компьютере языка программирования под названием Java,
поэтому запуск процесса установки может быть многоэтапным. Запуск программы
после ее установки также немного необычен: вам необходимо щелкнуть (или дважды щелкнуть) мышью на значке программы для запуска и, в некоторых случаях,
открыть окно браузера с указанием адреса вашего локального хоста (обычно
http://127.0.0.1:3333/ или http://localhost:3333). Как и приложение Jupyter Notebook, OpenRefine фактически работает через крошечный веб-сервер на вашем компьютере, а интерфейс — это просто веб-страница, которая ведет себя как мощная
программа обработки электронных таблиц. Несмотря на эти причуды, OpenRefine
невероятно полезен и часто служит отличной стартовой площадкой для проведения
первоначального исследования (потенциально грязного) датасета.
402
|
Глава 11
Дополнительные инструменты для обмена
и представления данных
Глава 10 была посвящена тому, как выбирать и совершенствовать визуализации с
помощью Python и ключевых библиотек seaborn и matplotlib. Хотя степень доступной с помощью этих инструментов кастомизации впечатляет, бывают случаи, когда
необходимо внести небольшие изменения в визуализацию, а вы не хотите или не
можете восстановить ее из исходного источника данных с помощью Python.
Если требуется быстро добавить или изменить что-то незначительное в визуализации, наличие доступа к программному обеспечению для редактирования изображений очень важно. И хотя вы, вероятно, знакомы с очень мощными и очень дорогими коммерческими программными приложениями для редактирования изображений, вы можете не подозревать, что существуют такие же мощные инструменты,
только в бесплатном виде с открытым исходным кодом.
Редактирование изображений в форматах JPG, PNG и GIF
Если вы ищете что-то мощное, но не можете (или не хотите) много за это платить,
для редактирования пиксельных изображений особенно хорошо подойдет GNU Image Manipulation Program (GIMP). Редактор GIMP7 является бесплатным с открытым исходным кодом и работает на всех платформах. Хотя стиль пользовательского интерфейса явно устарел (на момент написания этой книги интерфейс находился
в процессе модернизации), реальность такова, что сама программа может выполнять любое базовое (и не очень базовое) высококачественное редактирование изображений, особенно если вы просто хотите добавить (или удалить) текст или аннотации на изображение, обновить метки осей и т. д.
Не секрет, что у GIMP, возможно, несколько крутая кривая обучения. Сочетания
клавиш могут быть неожиданными, а расположение некоторых меню и внешний
вид их значков не соответствуют тому, что вы видели в коммерческих программах.
Тем не менее, если вы не эксперт в другой программе редактирования изображений
и не готовы платить (и продолжать платить) за доступ к ней, любое затраченное на
изучение GIMP время будет того стоить. Особенно если вам нужен лишь периодический доступ к программам для редактирования изображений, GIMP — это мощный и гибкий вариант.
Программное обеспечение для редактирования SVG
и других векторных форматов
Если планируется использовать визуализации для печати или в других контекстах с
высоким разрешением (или гибким разрешением), вы вполне можете сохранить их
7
https://gimp.org/
За пределами Python
|
403
в векторном формате. Хотя размеры файлов больше, векторная графика гораздо
более гибкая, чем ее пиксельные аналоги. Векторные изображения можно увеличивать и уменьшать без потери качества, без пикселизации или размытости. Однако
их нельзя эффективно редактировать с помощью программ для работы с растровыми изображениями, таких как GIMP.
И снова, если у вас есть бюджет на коммерческое программное обеспечение, стоит
использовать его, но и здесь есть бесплатный вариант с открытым исходным кодом.
Как и GIMP, приложение Inkscape8 — бесплатное, с открытым исходным кодом и
кроссплатформенное. И, как и GIMP, оно обладает почти всеми теми же функциями, что и дорогое коммерческое программное обеспечение для редактирования
векторных изображений. Более того, если вы уделите время работе с векторными
программами, они позволят вам не только подправить свою графику в цифровом
формате, но и станут вашими помощниками для печати на футболках, лазерной
резки и многих других цифро-физических работ. Если вы только начинаете работать, то Inkscape также определенно подходит по цене: бесплатно!
Немного о FOSS
FOSS — это сокращение от Free and Open Source Software (бесплатное программное обеспечение с открытым исходным кодом), и многие программы
FOSS, такие как GIMP и Inkscape, существуют уже несколько десятилетий. Несмотря на то, что большинство из нас больше знакомы с фирменным программным обеспечением таких компаний, как Apple, Microsoft, Google и других, факт заключается в том, что бóльшая часть программного обеспечения, на
котором сегодня работают компьютеры и интернет, основана на бесплатных
программах, пакетах и библиотеках. Пакеты и библиотеки Python, используемый в этой книге, также бесплатны для всех. Во многих случаях даже создание
и продажа продуктов, в основе которых лежит программное обеспечение
FOSS, законно — и этим занимаются многие компании.
Хотя бесплатное программное обеспечение с открытым исходным кодом гарантирует, что инструменты для инноваций не будут закрыты как интеллектуальная собственность крупных компаний, реальность такова, что создание,
поддержка и совершенствование этих инструментов на самом деле не бесплатны. Это стало возможным благодаря работе добровольцев — некоторые из них
могут тратить на это рабочее время, оплачиваемое их компанией, но многие
делают это безвозмездно.
Тот факт, что инструменты FOSS бесплатны для всех, помогает сделать работу
по выпасу данных доступной, и я (разумеется) рекомендую по возможности
работать с ними. В то же время, если вы регулярно пользуетесь ими и имеете
возможность, я также рекомендую делать небольшие пожертвования в пользу
проектов, которыми вы пользуетесь, чтобы помочь поддержать их работу.
8
https://inkscape.org/
404
|
Глава 11
Размышления об этике
Основное внимание в этой книге уделено развитию навыков выпаса данных — в
значительной степени для того, чтобы поддержать интерес к оценке и улучшению
качества данных. Попутно мы затронули более широкое влияние качества данных,
как абстрактное, так и конкретное. Низкое качество данных может привести к анализу, который дает неверное, искаженное или дискриминационное представление о
мире, а в сочетании с масштабным и повсеместным распространением управляемых данными систем9 это может привести к серьезным последствиям и нанести
существенный вред. Хотя вы можете использовать методы, описанные в этой книге, для проверки и улучшения качества ваших данных, к сожалению, остается еще
много возможностей для получения данных "хорошего качества" неэтичным путем.
И, как и в любой другой части процесса выпаса данных, вы сами должны решить, с
какими данными вам удобно работать и для каких целей их использовать.
Одна из стратегий, позволяющих убедиться в том, что ваша работа по выпасу данных не приведет к непреднамеренному нарушению ваших собственных этических
норм, заключается в разработке чеклиста. Разработав список вопросов, которые вы
задаете себе об источниках данных и о том, как будут использоваться результаты
вашего анализа, вы сможете уже на ранней стадии определить, стоит ли вам браться за тот или иной проект по выпасу данных. Следующий чеклист — это выдержка
из списка экспертов по данным Ди Джея Патила, Хилари Мейсон и Майка Лукидеса10. Как и в случае со списком характеристик данных в главе 3, цель здесь не в том,
чтобы отклонить проект данных, если ответ на каждый вопрос "да"; цель — критически осмыслить все аспекты качества данных, включая находящиеся вне нашего
контроля. Правда, мы можем только отклонить (а не изменить) проект, который не
соответствует нашим этическим стандартам, но если вы выскажете свои опасения,
вы можете помочь другим сделать то же самое. В худшем случае проектом займется кто-то другой, и ваша совесть будет (в некоторой степени) чиста. В лучшем случае, вы можете вдохновить других задуматься об этических последствиях своей
работы, прежде чем они возьмутся за следующий проект. Вот некоторые вопросы,
которые вам, возможно, стоит включить в свой чеклист:
1. Отражает ли дизайн сбора данных принципы сообщества, о котором идет речь?
2. Знают ли члены этого сообщества о том, что данные были собраны, и был ли у
них реальный способ отказаться?
3. Оценивались ли данные на предмет репрезентативности?
4. Есть ли способ проверить данные на предвзятость?
9
https://penguinrandomhouse.com/books/241363/weapons-of-math-destruction-by-cathy-oneil
https://oreilly.com/radar/of-oaths-and-checklists
10
За пределами Python
|
405
5. Являются ли предоставленные данные точными прокси для тех явлений, кото-
рые мы хотим описать?
6. Будет ли анализ заменяться, если и когда данные устареют?
В конце концов, вы можете решить, что ваши собственные опасения по поводу
данных имеют другую направленность.
Однако, какие бы вопросы вы ни решили включить в свой собственный чеклист,
вам будет гораздо легче придерживаться принципов работы с данными, если вы
изложите их заранее.
Заключение
В этой книге мы рассмотрели всё — от основ программирования на Python и оценки качества данных до выпаса данных из полудюжины форматов файлов и API. Мы
применили свои навыки к некоторым типично грязным и проблематичным реальным данным и усовершенствовали код, чтобы облегчить будущие проекты. Мы даже изучили, как проводить базовый анализ данных и наглядно представлять данные
в поддержку своих идей.
Если вы дошли до этого этапа, то я полагаю, что вы уже поймали какой-то "баг": в
программировании, в данных, в анализе и визуализации — или, возможно, во всем
вышеперечисленном. Что бы ни привело вас к прочтению этой книги, я надеюсь,
что вы нашли хотя бы часть того, что искали, включая уверенность, чтобы сделать
следующий шаг. Ведь что бы ни изменилось в мире выпаса данных в ближайшие
годы, одно остается неизменным: миру нужно как можно больше людей, делающих
эту работу критически и вдумчиво. Почему бы вам не стать одним из таких людей?
ПРИЛОЖЕНИЕ
А
Другие ресурсы
по программированию на Python
Надеюсь, эта книга показала, что Python — это надежный и гибкий язык программирования с широким спектром применения. Хотя в предыдущих главах я представила множество ключевых концепций и популярных библиотек, это приложение
создавалось, чтобы порекомендовать вам несколько полезных ресурсов и ссылок
для перехода вашей работы на языке Python на новый уровень.
Официальная документация Python
Да, всегда есть поисковые системы и StackOverflow1, но есть смысл научиться читать официальную документацию — будь то по языку Python или по таким популярным библиотекам, как pandas, matplotlib или seaborn. Хотя я не советую вам садиться и читать всю документацию по программированию, просмотр параметров и
опций типов данных или функций, которые вы хотите использовать, может дать
лучшее представление о том, что (в целом) можно сделать с помощью этого элемента, а также представление об устройстве его механизмов. Это может быть особенно полезно, когда вы хотите сделать что-то совершенно новое, потому что эта
информация даст вам представление о том, где искать пути дальнейшего развития.
Например, когда я начинала писать эту книгу, я знала, что seaborn и pandas были
построены на основе библиотеки matplotlib, и я немного поработала над созданием
и настройкой графики с помощью обеих библиотек. Однако только после изучения
документации последней я поняла разницу между объектами figure и axes, на которые я так часто ссылалась в коде примера, и это понимание помогло мне быстрее
найти решение, когда я экспериментировала со способами более полной кастомизации своих визуализаций. Не менее важно и то, что официальная документация
обычно поддерживается в актуальном состоянии, в то время как нередко самые популярные сообщения на форумах по той или иной теме могут быть месячной или
даже многолетней давности, а это означает, что содержащиеся в них советы иногда
могут оказаться сильно устаревшими.
1
https://stackoverflow.com/questions/tagged/python
Другие ресурсы по программированию на Python
|
407
Я также рекомендую регулярно обращаться к стандартной библиотеке Python Standard Library2, потому что вы можете удивиться количеству встроенной в нее функциональности. Многие из методов, которые вы можете узнать из работы с библиотеками, построены на основе (или для имитации) функций, существующих в "ванильном" Python. И хотя библиотеки часто бывают уникальными и полезными, нет
никаких гарантий, что они будут продолжать развиваться или поддерживаться. Если вы можете получить необходимую функциональность, работая с "обычным" Python, то снижается вероятность устаревания кода по причине того, что он полагается на библиотеку, которая больше не обновляется.
Установка ресурсов Python
Существует множество способов установки пакетов Python, в зависимости от вашей среды программирования. Независимо от того, используете ли вы Homebrew
на MacOS, работаете на машине с системой Windows или используете Colab, самым
надежным способом установки пакетов Python практически всегда будет использование какой-либо версии pip3. На самом деле, вы даже можете использовать pip для
установки пакета на ноутбук Google Colab (если вам удастся найти таковой без уже
установленных пакетов), используя следующий синтаксис4:
!pip install имя_библиотеки
Однако, какой вариант бы вы ни выбрали, я советую сделать свой выбор и придерживаться его — ситуация может стать довольно непредсказуемой, если вы начнете
использовать несколько инструментов для установки и обновления среды Python.
Где искать библиотеки
Обычно я рекомендую устанавливать пакеты Python, доступные в Python Package
Index5, также известном как PyPI. У каталога PyPI чистая структура и надежный
интерфейс тегирования и поиска, что позволяет легко находить полезные пакеты
Python, а тот факт, что документация по пакетам PyPI (обычно) выполнена по стандартному макету (как показано на рис. А.1), действительно способствует экономии
времени, если необходимо просмотреть множество вариантов.
Некоторые проекты (например, Beautiful Soup lxml) могут по-прежнему хранить
большую часть своей документации в отдельном месте, но их страница PyPI обычно все же содержит полезное краткое описание того, что делает проект, и даже некоторые советы по началу работы. Мне лично нравится просматривать раздел "История выпуска" (Release History). Здесь показано, когда проект был создан, а также
2
https://docs.python.org/3/library/index.html
https://packaging.python.org/tutorials/installing-packages
4
https://colab.research.google.com/notebooks/snippets/importing_libraries.ipynb
5
https://pypi.org/
3
408
|
Приложение А
как часто и когда в последний раз пакет обновлялся. Конечно, долговечность нельзя считать идеальным показателем для оценки надежности выбранного пакета —
поскольку любой может публиковать пакеты Python (и добавлять их в PyPI) — но
существующие дольше и/или обновляющиеся чаще (и недавно), обычно служат
хорошей отправной точкой.
Рис. А.1. Пример целевой страницы пакета PyPI
Следите за остротой своих инструментов
Инструменты программирования постоянно обновляются, поскольку сообщество
выявляет (и в основном устраняет) новые проблемы или приходит к согласию, что
некоторые аспекты программного инструмента (или даже сам Python!) должны работать по-другому. Если вы следовали инструкциям по установке Python и Jupyter
Notebook, приведенным в главе 1, то можете использовать команду conda для обновления обоих. Вам просто нужно будет время от времени выполнять команды
conda update python и conda update jupyter. Поскольку эта книга предназначена для
начинающих, я не затронула в главе 1 вопрос об окружении Python. В этом контексте определенное окружение определяется в основном базовой версией Python, которая будет использоваться по умолчанию (например, 3.9 или 3.8) при использовании команды python. Хотя выполнение команды conda update python обновит, скажем, версию 3.8.6 до 3.8.11, оно никогда не будет автоматически обновлять вашу
версию до другого базового выпуска (например, 3.9). Часто это не вызывает проблем, если только ваша установка Python не устарела на несколько лет (и, соответственно, базовых версий). Если и когда это произойдет, следует создать новую среду для новой версии Python, главным образом потому, что в противном случае
Другие ресурсы по программированию на Python
|
409
компьютеру может быть трудно разобраться в ситуации. К счастью, когда придет
время обновить Python до следующей базовой версии, вы сможете узнать все необходимое о том, как это сделать, на странице документации conda6.
Где получить больше информации
Поскольку Python стал таким популярным языком программирования, существуют
тысячи ресурсов в Интернете и библиотеках для перехода на следующий уровень
программирования, независимо от того, есть ли у вас конкретный проект для выполнения или вы просто хотите узнать больше о таких продвинутых темах, как машинное обучение и обработка естественных языков.
Для практического и емкого введения в более сложные темы науки о данных я бы
рекомендовала прежде всего книгу "Python для сложных задач"7 Джейка Вандер
Пласа (O’Reilly). Я тоже использовала эту книгу, когда пыталась разобраться в некоторых более сложных методах машинного обучения, доступных в Python. Четкий
стиль изложения и лаконичные примеры Джейка позволяют получить общее представление о машинном обучении на Python и познакомиться с конкретными методами (например, кластеризацией k-means). Более того, вы можете получить доступ
ко всей книге бесплатно онлайн8 — хотя, если у вас есть возможность, я настоятельно рекомендую приобрести печатное издание.
Однако, возможно, еще более ценным, чем поиск нужных книг, учебников или курсов, является поиск сообщества людей, с которыми вы можете работать и учиться,
продолжая свой путь по выпасу данных. Будь то в школе, общественной организации или на встрече, найти небольшую группу людей, с которыми можно обсудить
свои проекты или обратиться за советом (а иногда и за поддержкой!), — это, пожалуй, самый бесценный ресурс для расширения своих навыков в Python и работе с
данными. Существует также множество бесплатных программ, способных помочь
поддержать и продвинуть вашу работу, особенно это касается тех людей из сообществ, которые недостаточно погружены в технические области. Загляните в такие
группы, как Ada Developers Academy9 или Free Code Camp10.
6
https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html
https://www.oreilly.com/library/view/python-data-science/9781491912126/
8
https://jakevdp.github.io/PythonDataScienceHandbook
9
https://adadevelopersacademy.org/
10
https://freecodecamp.org/
7
ПРИЛОЖЕНИЕ
Б
Еще несколько слов о Git
По большей части, работа с Git, когда вы единственный программист в проекте,
довольно проста: вы вносите изменения в свой код, делаете коммит, отправляете на
GitHub (или в другой удаленный репозиторий), и все.
До тех пор, пока... это условие не изменится. Возможно, вы обновили файл
README.md на GitHub и забыли выполнить команду git pull перед внесением изменений в тот же файл на устройстве. Возможно, забыли добавить сообщение, когда выполняли команду git commit. Хотя справиться с подобными трудностями на
самом деле не так уж сложно, некоторые сообщения об ошибках и стандартное поведение Git в терминале могут быть сложными, особенно если вы никогда не сталкивались с ними раньше. Хотя руководство в этом приложении далеко не всеобъемлющее, по моему опыту, базового рабочего знания Git более чем достаточно, если
только вы не работаете с относительно большой командой в крупной организации.
Поэтому, если вы планируете использовать Git с несколькими сотрудниками в
сложных проектах, то следующие примеры охватывают ситуации, в которых чаще
всего могут оказаться разработчики и для выхода из которых требуется помощь.
Вы запускаете команду git push/pull
и оказываетесь в странном текстовом редакторе
Как и я, Git неумолим в вопросах документирования своей работы, вплоть до того,
что он не позволит вам выполнить коммит изменения без сопровождающего сообщения. Это означает, что если выполнить команду commit без указания -m «Сообщение
коммита», например:
git commit имя_файла
вы, скорее всего, обнаружите, что окно терминала занято и показывает текст, подобный тому, что показан на рис. Б.1.
Это может вызвать тревогу, особенно когда происходит в первый раз. Но поскольку
это рано или поздно произойдет, вот как быстро с этим разобраться:
1. Начните с ввода буквы i. Хотя не все редакторы требуют ввода i для входа в
режим INSERT, большинство все же примут первый введенный символ, так что вы
можете начать с него. На рис. Б.2 показано, как выглядит этот режим.
Еще несколько слов о Git
|
411
Рис. Б.1. Редактирование сообщений коммитов с помощью терминала
Рис. Б.2. Редактор терминала в режиме INSERT
2. Чтобы выйти из режима INSERT, нажмите клавишу <Esc> или <Escape>. Затем
введите :x, как показано на рис. Б.3, после чего нажмите клавишу <Enter> или
<Return>. Это сохранит ваше сообщение коммита, а также позволит выйти из
текстового редактора и вернуться в знакомое и любимое окно терминала.
Обратите внимание, что редактор, в котором вы окажетесь, может выглядеть не
так, как показано на рис. Б.2 и Б.3. Если что-то выглядит не так — не паникуйте.
Сейчас самое время поискать в Интернете, как редактировать, сохранять и выходить из программы, в которую вы попали. Независимо от особенностей, цель одна
и та же.
412
|
Приложение Б
Рис. Б.3. Редактор терминала с командой "сохранить и выйти"
Ваша команда git push/pull отклоняется
Это случается с каждым из нас. Вы думаете, что старательно выполняли коммиты
своего кода в конце каждой рабочей сессии, — даже писали отдельные коммитсообщения, чтобы отслеживать изменения, внесенные в определенные файлы, вместо того, чтобы просто выполнить команду git commit -a. Даже в этом случае иногда вы запускаете команду git push и она отклоняется. Что же делать в таком случае?
Если команда git push не сработала, вы, вероятно, увидите ошибку, подобную этой:
! [rejected]
main -> main (non-fast-forward)
error: failed to push some refs to 'https://github.com/your_user_name/your_r
epo_name.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
В общем случае, это происходит, когда один из файлов в вашем локальном репозитории был изменен с момента последнего коммита, но также была изменена версия в вашем удаленном репозитории, так что Git не знает, какой из файлов должен
быть приоритетным. Не волнуйтесь!
Выполните команду git pull
Если изменения в файле(ах) можно объединить автоматически, Git сделает это. Это
разрешит конфликты файлов, но может привести к тому, что вы попадете во встро-
Еще несколько слов о Git
|
413
енный текстовый редактор командной строки, который сам по себе может быть довольно запутанным. Если при запуске команды git pull перед вами внезапно появилось что-то похожее на рис. Б.1, см. раздел «Вы запускаете команду git push/pull
и оказываетесь в странном текстовом редакторе».
Если изменения в файле(ах) невозможно объединить автоматически, вы получите
сообщение примерно следующего вида (вы также увидите это сообщение, если
просто запустите команду git pull без предварительного выполнения git push):
Auto-merging имя_файла
CONFLICT (content): Merge conflict in имя_файла
Automatic merge failed; fix conflicts and then commit the result.
Это означает, что вы должны определить, у какого файла будет приоритет, и либо
вручную привести их в соответствие (путем прямого редактирования одного или
обоих файлов), либо просто заставить один из них перезаписать другой.
Устранение конфликтов вручную
Представьте, что у вас есть репозиторий с файлом README.md. Вы выполняли
коммиты изменений в него, как на GitHub.com, так и на своем устройстве. Вы попытались выполнить команду git push и получили ошибку, поэтому попытались
выполнить git pull, но автоматическое слияние не удалось. Чтобы исправить конфликты вручную, начните с открытия локальной копии файла в предпочитаемом
текстовом редакторе. То, что вы можете увидеть в файле разметки, показано в листинге Б.1.
Листинг Б.1. Пример конфликта в файле разметки
# Заголовок одинаков в обоих файлах
Содержимое также одинаково.
<<<<<<< HEAD
Здесь — то, что в настоящее время находится в локальном файле.
=======
Здесь — то, что в данный момент находится в удаленном файле.
>>>>>>> 1baa345a02d7cbf8a2fcde164e3b5ee1bce84b1b
Чтобы разрешить конфликт, просто отредактируйте содержимое файла по своему
усмотрению, обязательно удалив строки, начинающиеся с <<<<<<< и >>>>>>>, а также
строку =======; если вы оставите эти строки, они появятся в вашем итоговом файле.
Теперь сохраните файл как обычно. Далее выполните команду:
git add имя_файла
Затем выполните:
git commit -m "How I fixed the conflict."
414
|
Приложение Б
Обратите внимание, что нельзя указывать имя файла в команде git commit, иначе Git
пожалуется, что вы попросили его сделать частичный коммит во время слияния.
Наконец, выполните команду:
git push
И все будет готово!
"Устранение" конфликтов путем принудительной перезаписи
Хотя всегда полезно просмотреть все файлы, в которых возникли конфликты, иногда вы выполняете выгрузку в точно устаревший репозиторий и просто хотите перезаписать то, что находится в вашем удаленном репозитории. В этом случае можно перенести изменения без ручной сверки файлов, выполнив команду:
git push --force
Она просто перезапишет удаленный файл вашей локальной версией, но имейте в
виду, что все записи о перезаписанной версии будут потеряны, включая историю
коммитов. Это означает, что после применения команды с выражением --force пути назад уже не будет — содержимое удаленного файла исчезнет. Очевидно, это
означает, что вам никогда не следует использовать эту опцию, если другие люди
вносят вклад в тот же репозиторий, — используйте ее, только если вы единственный, кто работает над файлом, и вы (полностью) уверены, что материал в удаленном репозитории может быть безопасно перезаписан.
Краткое руководство по Git
В табл. Б.1 представлен краткий обзор наиболее полезных/распространенных команд git. Более полный список можно найти на сайте GitHub1.
Таблица Б.1. Наиболее используемые команды терминала git
Текст команды
Результат выполнения команды
git status
Выводит текущее состояние репозитория; ничего не изменяет.
Перечисляет все файлы2 в репозитории, сгруппированные по
признакам: новые, неотслеживаемые или измененные
git add имяфайла
Индексирует определенный файл, который в настоящее время
носит неотслеживаемый статус. Перед выполнением коммита в
репозиторий файл должен быть индексирован командой git add.
После этого команда git status пометит проиндексированные
файлы как новые
1
https://training.github.com/downloads/github-git-cheat-sheet
Если у вас есть активный файл .gitignore для репозитория, все игнорируемые файлы не будут отображаться в перечислении команды git status.
2
Еще несколько слов о Git
|
415
Таблица Б.1 (окончание)
Текст команды
Результат выполнения команды
git add -A
Выполняет индексирование всех файлов в статусе неотслеживаемый сразу. После этого команда git status пометит проиндексированные файлы как новые
git commit -m
Выполняет коммит определенного, уже индексированного файла,
"Сообщение коммита."` прикрепив сообщение, указанное между двойными кавычками
имяфайла
git commit -a -m
"Сообщение коммита."
Выполняет коммит всех проиндексированных на данный момент
файлов, прикрепляя ко всем из них одно и то же сообщение коммита
git push
Выгружает все локальные коммиты в удаленный репозиторий.
Добавление команды --force перезапишет все конфликтующие
коммиты в удаленном репозитории прошедшими локальный
коммит файлами
git pull
Извлекает все удаленные файлы в локальное хранилище. Добавление команды --force перезапишет все конфликтующие коммиты в локальном репозитории удаленными файлами
ПРИЛОЖЕНИЕ
В
Поиск данных
В целом, существует четыре основных "источника" данных, к которым можно обратиться при поиске ответа на вопрос о мире. Я взяла слово "источники" в кавычки,
потому что на самом деле это типы источников, а не конкретные веб-сайты, базы
данных или даже организации. Вместо этого они представляют собой механизмы,
которые журналисты, исследователи и другие специалисты используют для сбора
данных о мире, чтобы получить ответ на свои вопросы.
Репозитории данных и API
Доступ к "открытым данным" все чаще становится характерной чертой многих
правительственных и научных организаций, стремящихся повысить прозрачность,
подотчетность и — особенно в научном сообществе — воспроизводимость. Это
означает, что многие государственные учреждения, некоммерческие организации и
научные журналы поддерживают веб-сайты, на которых можно получить доступ к
структурированным данным, имеющим отношение к их работе. Например, простой
поиск в Интернете по словам "nyc open data" или "baltimore open data" приведет вас
на порталы "открытых данных" этих городов. Аналогичный поиск по словам "johannesburg open data" приведет вас сначала на сайт South African Cities Open Data
Almanac (SCODA), но несколькими ссылками ниже вы найдете больше данных от
организации под названием "DataFirst", а также портал данных Южной Африки,
расположенный по адресу http://opendataforafr ica.org. На всех этих сайтах есть
определенные данные, хотя, как обсуждалось в главе 3, качество этих данных, в том
числе их пригодность для ответа на ваш конкретный вопрос, может сильно различаться.
API — еще один ценный источник данных, поскольку они могут обеспечить доступ
к очень подробным и специализированным датасетам, которые не будут отображаться в результатах поисковых систем. Это связано с тем, что данные API представляют собой часть того, что иногда называют "глубокой сетью" (deep web): данные, которые доступны через Интернет, но только через специально созданные поисковые запросы, отправленные на определенные URL-адреса. Например, поиск в
Интернете по запросу "currently available citibikes, nyc" приведет вас к интерактивной карте, где вы можете нажать на станцию Citi Bike, чтобы увидеть, сколько велосипедов доступно. А запрос к Citi Bike API позволит получить информацию о
Поиск данных
|
417
доступных велосипедах на всех станциях Citi Bike в Нью-Йорке. Более того, API
вернет эти данные в файле, достаточно хорошо структурированном для анализа, в
отличие от интерактивной карты или, если на то пошло, большинства веб-страниц.
Репозитории данных и API обычно представляют собой отличный способ доступа к
большому количеству структурированных данных, относительно чистых и простых
в использовании для анализа. Однако в то же время у этих источников данных есть
существенное ограничение: у компании или агентства, управляющих порталом или
API, полный контроль над тем, какие данные содержатся в API. Это означает, что
данные могут быть устаревшими, неполными или недостаточно контекстуальными,
или в них может просто отсутствовать информация, которую владелец данных хочет скрыть. Другими словами, то, что данные могут быть чистыми и "готовыми к
использованию", не означает, что эти данные обязательно хорошего качества. Критическая оценка их качества по-прежнему необходима.
Однако чаще всего вся информация, необходимая для ответа на ваш вопрос, не может быть доступна через один из предыдущих типов интерфейсов, поэтому вам
придется обратиться к другим источникам.
Эксперты по предметным вопросам
Как уже говорилось в главе 3, все данные так или иначе исходят от людей. Тем не
менее на начальном этапе часто может показаться, что проще положиться на автоматизированные интерфейсы, такие как описанные ранее веб-порталы и API. В
действительности эти ресурсы доступны практически постоянно — при структурировании вызова API нет необходимости беспокоиться о времени ожидания по телефону, рабочих часах или графике отпусков. В то же время общение с экспертами
в предметной области часто становится самым быстрым способом выявления наиболее богатых и релевантных данных для ответа на ваш вопрос. Это связано с тем,
что многие общедоступные источники данных, даже если они доступны в Интернете, являются достаточно нишевыми. По этой причине вы можете столкнуться с
трудностями, не зная оптимальных поисковых запросов, тогда как специалисты
используют их на регулярной основе. Таким образом, они не только смогут указать
вам на порталы и API, которые почти никогда невозможно найти с помощью вебпоиска, но и помогут отличить на первый взгляд похожие источники данных и предоставить важную контекстную информацию об их содержимом. Более того, они
могут добровольно предоставить вам датасеты, которых в противном случае вы
могли бы ждать несколько недель или месяцев в ответ на запрос FOIA/L.
В целом, есть три места, где можно найти экспертов по нужной тематике, способных помочь вам найти или понять данные. Колледжи и университеты служат домом для головокружительного количества экспертов по, казалось бы, бесконечному
кругу вопросов. Начните со звонка или электронного письма в отдел по связям с
общественностью — или просто начните читать онлайн-профили преподавателей и
исследователей. Аналогичным образом, неправительственные организации (НПО),
такие как некоммерческие организации и аналитические центры, нанимают экспер-
418
|
Приложение В
тов в соответствии со своей направленностью. Эти эксперты также могут быть знакомы со специализированными датасетами, которые иначе было бы трудно найти;
у них также могут быть лично собранные и/или проанализированные данные, которыми они готовы поделиться. Наконец, государственные служащие могут быть полезным источником данных и информации, поскольку зачастую именно им приходится собирать и обрабатывать данные от имени правительства. И хотя негативных
стереотипов предостаточно, по моему опыту, государственные служащие — одни
из самых добрых и открытых из всех возможных "помощников".
Запросы FOIA/L
Запросы по Закону о свободе информации (FOIA, Freedom of Information Act или
FOIL, Freedom of Information Law) — это механизм, с помощью которого любой
человек может запросить записи о деятельности правительства на федеральном,
государственном или местном уровне, соответственно1, что может стать источником данных, которые еще не доступны через существующие порталы или публикации. Эти запросы представляют собой специализированное письмо, которое вы
можете отправить в определенный офис или отдельному сотруднику агентства,
указав в нем свой запрос на получение информации.
Ключевое требование для эффективного использования запросов FOIA/L — сформировать запрос максимально конкретно и узко: хотя законы FOIA/L гарантируют
ваше право запрашивать данные, большинство из них лишь обязывают вас получить ответ в течение определенного срока (часто от 5 до 20 рабочих дней). В этих
законах нет требования, чтобы ответ включал запрошенные данные. В большинстве
случаев первоначальный ответ будет таким: 1) отклонять ваш запрос на основании
допустимых исключений и освобождений от обязательств2; 2) признавать ваш запрос, но классифицировать его как "сложный", что требует увеличения сроков его
выполнения; или 3) информировать, что поисковый запрос был выполнен, но не
дал результатов. Для успешного получения нужных вам данных, скорее всего, потребуется как минимум одна "апелляция" и много-много терпения. Всестороннее
введение в запросы FOIA/L выходит за рамки этой книги, но в Интернете можно
найти ряд руководств, содержащих шаблоны и советы о том, как составить запрос
FOIA/L, и даже сайты, такие как MuckRock, которые предоставляют инструменты,
помогающие составлять, подавать и отслеживать поданные запросы. Размышляя
над тем, о чем просить, помните о следующем:
Запросы FOIA/L относятся только к правительственным документам
Вам придется творчески подойти к вопросу о том, как интересующее вас явление
может породить такие записи. Подумайте о любой деятельности, связанной с вашим вопросом, которая может потребовать разрешений, лицензирования или нало1
2
В некоторых штатах требуется, чтобы податель запроса был резидентом этого штата.
https://dhs.gov/foia-exemptions
Поиск данных
|
419
гообложения, — все это приведет к появлению государственных документов. Также следует помнить, что деятельность государственных служащих, включая их
электронную почту, календари и другие записи, также подпадает под действие законов FOIA/L.
Не "объединяйте" запросы
Если вы ищете записи, содержащие несколько терминов, отправьте отдельный запрос FOIA/L для каждого термина и типа файла (например, электронная почта, календарь и т. д.). Это увеличивает количество запросов FOIA/L, которые вам приходится отслеживать, но также уменьшает вероятность того, что ваш запрос будет
задержан из-за того, что он был классифицирован как "сложный".
Используйте общедоступные формы
Если вы не знаете, какую информацию собирает агентство, используйте общедоступные формы — например, те, которые требуются для уплаты налогов, получения
лицензий и разрешений, — чтобы получить представление о данных. Затем вы можете ссылаться на эти конкретные номера и поля формы в своем запросе.
Изучите прошлые запросы
Запросы FOIA/L сами подпадают под действие этих законов. Если вы ищете информацию, которая была частью ранее выполненного запроса, обращение к ней
может ускорить выполнение вашего запроса.
Вежливо обратитесь к соответствующему персоналу
Сотрудники FOIA/L могут помочь вам с запросами или усложнить процесс. Постарайтесь установить с ними теплые, если не дружеские, отношения — по электронной почте или по телефону. Они могут дать вам важные советы о том, как усовершенствовать запрос для достижения успеха.
Кастомные методы сбора данных
Иногда на ваш вопрос просто невозможно ответить с помощью комбинации существующих датасетов, независимо от того, сколько экспертов или API вы используете. Это обстоятельство подчеркивает именно человеческую природу данных: они
существуют только потому, что кто-то где-то решил их собрать, очистить и сделать
доступными. Если ваш вопрос особенно своевременный или уникальный, может
оказаться, что этот человек — вы.
В университетах можно найти множество курсов, полностью посвященных различным методам сбора данных, от опросов до сенсоров. Выбор метода (методов) сбора
данных зависит как от характера вашего вопроса, так и от ресурсов (включая время), которые вы можете выделить на процесс сбора данных. Если вы ищете исследовательские или эпизодические данные, то ваш процесс сбора данных может быть
простым — разместить вопрос или "опрос" в социальных сетях и проанализировать
полученные ответы. С другой стороны, если вы хотите получить возможность делать обобщающие утверждения, то есть утверждения о более масштабных явлени-
420
|
Приложение В
ях, чем те, для которых у вас есть данные, то ваши данные должны быть репрезентативными, как обсуждалось в главе 3.
Какой бы тип данных вы ни собирали — эпизодические или репрезентативные —
для этого существуют два основных метода: интервью и инструментарий. Методы
интервью подразумевают обращение к человеку с просьбой дать ответ, независимо
от того, являются ли эти ответы открытыми, с множественным выбором или в каком-либо другом формате. Инструментальные методы предполагают определенную
степень автоматизированного сбора данных, будь то привычки просмотра вебстраниц или уровень угарного газа. В любом случае, вам необходимо ознакомиться
с передовым опытом или проконсультироваться с экспертами, чтобы понять ограничения своих методов измерения и соответствующие методы анализа и интерпретации данных. Хотя сбор собственных данных может быть сложной задачей, он
также может стать для вас невероятно полезным.
ПРИЛОЖЕНИЕ
Г
Ресурсы для визуализации
и информационного дизайна
Развитие средств визуализации за последнее десятилетие превратило процесс создания красивой, информативной графики из того, что требует дорогостоящего специализированного программного обеспечения и специальной подготовки, в то, что
может сделать практически каждый. Как уже говорилось в главе 10, создание красноречивых визуализаций — это не только инструменты, имеющиеся в вашем распоряжении, но и выбор дизайна, который наилучшим образом улучшит и прояснит
ваше сообщение. Хотя существует множество книг и курсов, посвященных визуализации для конкретных целей (например, для делового или научного общения),
следующий список можно считать скорее универсальным — это отличные книги и
ресурсы для развития и понимания истории, целей и потенциала визуализации и
информационного дизайна, если вы хотите расширить свои знания в этой области.
Основополагающие книги
по визуализации информации
Вероятно, самый известный человек в мире визуализации информации — это Эдвард Тафти. Он написал множество первых и наиболее влиятельных книг по этой
теме. Хотя все они являются прекрасными работами, вероятно, наиболее поучительными будут первые три: "The Visual Display of Quantitative Information",
"Envisioning Information" и "Visual Explanations" (изд-во Graphics Press). Тафти также опубликовал несколько коротких докладов и брошюр, в которых приводятся
выдержки из его книг и дается острая критика программы PowerPoint. Все его публикации можно приобрести на его личном сайте, https://edwardtufte.com, но многие из них доступны в публичных библиотеках.
Хотя формально это книги о типографике, "Thinking with Type" Эллен Луптон1
(Princeton Architectural Press) — это отличное введение в общие принципы графического дизайна, которые применимы везде, где есть текст, — а это большинство
мест, включая визуализации на основе данных. Книга Луптон знакомит читателей
1
http://elupton.com/
422
|
Приложение Г
со всеми основными аспектами проектирования с использованием текста — от выбора шрифта и его насыщенности до размера, цвета и интервалов. Эта книга —
прекрасный учебник и справочник, который стоит иметь под рукой. Если у вас нет
возможности приобрести экземпляр этой книги, многие из основных материалов
доступны бесплатно на сайте http://thinkingwithtype.com.
Краткое руководство, за которым вы потянетесь
Моим справочником номер один по визуализации информации остается "The Wall
Street Journal Guide to Information Graphics" (Norton), написанный моей бывшей
коллегой Доной Вонг. В этой книге собраны все советы и рекомендации, которые
вам понадобятся для точной и красивой визуализации данных. Нужна приятная
цветовая палитра? Есть. Напоминание о том, как рассчитать процентное изменение? Есть. Идеи о том, как обрабатывать отрицательные или отсутствующие значения? Есть. Легко читаемый и бесценный справочник. Если бы нужно было приобрести только одну книгу с информацией по визуализации, то это была бы именно она.
Источники вдохновения
Существуют сотни книг и блогов о визуализации информации, и не все они созданы одинаково — потому что не все дизайнеры сосредоточены на точном представлении данных. Хотя многие предлагают платные семинары по визуализации (некоторые из них могут быть весьма хорошими), есть также много талантливых практиков, которые делятся своей работой и идеями бесплатно.
Блог Кайзера Фунга под названием "Junk Charts"2 — это ресурс по критике и редизайну визуализации с внушительной историей. Здесь вы найдете более чем 15летнюю коллекцию осознанных взглядов Фунга на информационный дизайн,
включая освещение ошибочных данных, которые иногда появляются в визуализациях даже крупных новостных организаций и предприятий. Отличный ресурс для
тех, кто хочет узнать о том, как даже благонамеренные визуализации могут оказаться ошибочными.
Работа Моны Чалаби для The Guardian3 и других изданий демонстрирует, как великолепные визуализации данных могут быть сделаны вручную и отражать индивидуальность и человечность людей, о которых эти данные говорят.
Стефани Посавец4 — художник и дизайнер, создающий объекты и визуализации,
основанные на данных, в различных медиа.
2
https://junkcharts.typepad.com/
https://theguardian.com/profile/mona-chalabi
4
http://stefanieposavec.com/
3
Об авторе
Сьюзен Макгрегор — исследователь Института науки о данных Колумбийского
университета, где она является сопредседателем Центра данных, медиа и общества.
На протяжении более десяти лет она совершенствует свой подход к преподаванию
программирования и анализа данных для студентов, не являющихся специалистами
в области технических наук, на уровне специалистов, аспирантов и студентов. Макгрегор — штатный преподаватель и исследователь Колумбийского университета с
2011 г., когда она присоединилась к Колумбийской школе журналистики и Центру
цифровой журналистики Tow. Там она разработала первую в школе учебную программу по журналистике данных и служила главным академическим консультантом программы двойного диплома по журналистике и компьютерным наукам. Ее
научные исследования посвящены затрагивающим журналистов и медийные организации вопросам безопасности и конфиденциальности. Данные вопросы стали
темой ее первой книги Information Security Essentials: A Guide for Reporters, Editors,
and Newsroom Leaders (CUP, Columbia University Press).
До работы в Columbia Макгрегор несколько лет проработала старшим программистом в группе новостной графики в Wall Street Journal. Она была названа лауреатом
премии Джеральда Леба 2010 г. за работу над оригинальной серией WSJ "What
They Know", а также выступала и публиковалась на ряде ведущих научных конференций по безопасности и конфиденциальности. Ее работа получила поддержку
Национального научного фонда, Фонда Найта, компании Google, нескольких школ
и отделов Колумбийского университета. Макгрегор также интересуется тем, как
искусство может помочь стимулировать критическое мышление и представить новые взгляды на технологические вопросы. Она получила степень магистра в области образовательных коммуникаций и технологий в Нью-Йоркском университете и
степень бакалавра в области интерактивного информационного дизайна в Гарвардском университете.
Колофон
Животное, изображенное на обложке книги "Обработка данных на Python. Data
Wrangling и Data Quality", — подковчатый полоз (Hemorrhois hippocrepis).
Природные места обитания этой змеи расположены в юго-западной Европе и северной Африке, но ее можно встретить в самых разных местах, включая кустарниковую растительность, скалистые и песчаные берега, пастбища, плантации, сельские сады и некоторые городские районы. Взрослые особи могут достигать до 1,5 м
в длину. У подковчатого полоза гладкая чешуя и желтовато-красное тело с рядом
крупных черных или темно-коричневых пятен, окаймляющих спину. Свое название
он получил благодаря светлой подковообразной отметине на шее и затылке.
Подковчатый полоз хорошо лазает и охотится на птиц, мелких рептилий и небольших млекопитающих с верхушек деревьев, крыш или скалистых утесов. Хотя эта
змея может быть агрессивной и обладает сильным укусом, она не ядовита и не
опасна для человека.
Считается, что подковчатый полоз обладает высокой адаптивностью и имеет текущий природоохранный статус — "Вызывающие наименьшие опасения". Многие
животные на обложках O'Reilly находятся под угрозой исчезновения, и все они
важны для окружающего мира.
Иллюстрация на обложке выполнена Хосе Марзаном-младшим на основе старинной гравюры из книги Ричарда Лидеккера1.
1
Royal Natural History, Richard Lydekker. — Примеч. ред.
Предметный указатель
J
JSON:
объект, 161
пара ключ/значение, 161
список, 161
форматер:
JSON Formatter, 164
JSONLint, 164
N
NAICS, 292
R
readlines(), 318
А
Автозаполнение:
команда, 32
Авторизация:
конечная точка, 202
цикл, 193
Арифметическое:
среднее, 111
Атрибуты XML, 151
Аугментация данных, 115
Аутентификация:
ключ, 50
X
XML:
атрибуты, 151
спецификация, 153
EPUB, 154
KML, 154
RSS, 154
SVG, 154
теги, 151
формат:
.atom, 154
элемент, 151
дочерний, 152
корневой, 152
прародитель, 152
родитель, 152
Б
Байткод, 190
Безработица:
показатель:
U3, 133, 134
U6, 134
Библиотека:
argparse, 322
Beautiful Soup, 214
csv, 88, 129
fingerprints, 246, 285
lxml, 154, 155, 156, 157, 159
426
|
Предметный указатель
Matplotlib, 26
NLNK, 26
numbers, 276
NumPy, 26
opencv-python, 167
openpyxl, 137, 139
os, 216
pandas, 26, 255, 291
pdf2image, 167
poppler, 167
pyexcel-ods, 137, 140
pytesseract, 167
requests, 175
Scikit-Learn, 26
Seaborn, 26
tesseract-ocr, 167
time, 216
xlrd, 137, 141, 142, 279
Блокнот Python, 28
В
Веб-извлечение данных, 208
Верблюжий регистр, 62
Видимость:
область, 276
Вложение, 74, 152
Внешнее соединение, 231
Возвращаемые значения, 303
Встроенные функции, 63
Выборка, 102
Выражения регулярные, 265
Г
Глобальные переменные, 312
Гранулярность, 111
Д
Данные:
веб-извлечение, 208
дневник, 45, 116
достоверность, 98, 253
канальные, 118
качество, 19, 22, 95
машиночитаемые, 120
многомерные, 111
надежность, 98, 257
непротиворечивость, 244
неструктурированные, 121, 122
очистка, 115
первичная обработка, 19
происхождение, 106
размерность, 114
репрезентативность, 98, 102
словарь, 145, 239
соответствие, 23, 96, 97, 104, 107
стабильность, 257
структуризация, 119
структурированные, 120, 121
тип:
OrderedDict, 140
кортеж, 139
массив, 59
сложные, 60
список, 59
числовой, 59
точность, 257
файловый, 117
формат:
.csv, 123
json, 119
JSON, 147, 148
XML, 147, 148
целостность, 23, 96, 104
Диспетчер Miniconda, 33, 36, 37, 38
Дневник данных, 45, 116
Документ:
тип:
объявление, 150
Документирование, 96
Достоверность:
данных, 98, 253
конструкта, 99
содержимого, 100
Доступ:
маркер, 202, 204
токен, 193, 202
Дочерний элемент:
XML, 152
З
Запрос:
строка, 179, 184
Предметный указатель
Знаменатель:
проблема, 259
Значения:
возвращаемые, 303
И
Инспектирование:
средства, 211
Инструмент Tabula, 171
Интерфейс:
API, 173
командной строки, 30
Исключение, 82
Итератор, 128
К
Канальные:
данные, 118
форматы:
данных, 118
Качество данных, 19, 22, 95
Ключ:
SSH, 50, 52, 53, 54
аутентификации, 50
Ключевое слово:
def, 66
from, 189
time, 93
Код:
NAICS, 292
редактор Atom, 29, 34, 35, 38, 40, 41
рефакторинг, 170
Коллапс контекста, 104
Команда:
cd, 31
git add, 54
git clone, 53
git push, 54
git status, 54
ls, 31
mkdir, 31
pydoc, 320, 321
автозаполнение, 32
Командная строка:
интерфейс, 30
Комментарии, 44
Конечная точка, 149, 180, 184
авторизации, 202
Конкатенация, 63
Константа символьная, 63
Конструкт:
достоверность, 99
операционализация, 99
Контекст:
коллапс, 104
Корневой элемент:
XML, 152
Кортеж:
тип данных, 139
М
Маркер доступа, 202, 204
Массив:
тип
данных, 59
Машиночитаемые данные, 120
Метаданные, 107, 239
Метод:
.keys(), 156
convert_from_path(), 169
etree.tostring(), 156
items(), 140
iter(), 159
iterdescendants(), 159
join, 170
keys(), 159
kwargs.get(), 303
len(), 227
load_workbook(), 138
max(), 245
min(), 245
post, 202, 203
re.compile(), 268
read_csv(), 237
split(), 63, 263
str(), 227
str.contains(), 255
strftime(), 279
strip(), 281
to_csv(), 229
unique(), 247
value_counts(), 242
weekday(), 270, 272, 307
| 427
428
|
Предметный указатель
writerow(), 140
xldate_as_datetime(), 278
отпечатков пальцев, 245
Методы строковые, 63
Многомерные данные, 111
Н
Надежность данных, 98, 257
Национальное управление по проблемам
океана и атмосферы, 143
Непротиворечивость данных, 244
Неструктурированные данные, 121, 122
Носитель:
токен, 199, 204
О
Область видимости, 276, 298, 299, 300
Обработка данных:
первичная, 19
Объект:
datetime, 270
JSON, 161
Объявление:
типа документа, 150
Операторы, 63
if...else, 73
присваивания, 62
условный:
if...else, 74, 89
Операционализация конструкта, 99
Определяемые пользователем функции, 64
Организация ProPublica, 24
Очистка данных, 115
П
Пакет офисных приложений:
LibreOffice, 125
OpenOffice, 125
Папка:
__pycache__, 190
data_wranging, 32
Linux, 32
текущая, 32
Пара JSON:
ключ/значение, 161
Параметр indicator=True, 232
Первичная обработка данных, 19
Переменная, 59
логическая, 275
флаговая, 275, 276
Показатель:
безработицы
U3, 133, 134
U6, 134
Пользовательские функции, 67
названия, 297
Прародитель:
элемент:
XML, 152
Присваивание:
оператор, 62
Проблема знаменателя, 259
Программа велопроката
Citi Bike, 85
Происхождение данных, 106
Псевдокод, 86
Р
Размерность данных, 114
Разработка среды:
Colab, 42
Jupyter Notebook, 30, 35, 37, 38, 41
Расширение файла
.py, 29, 41
Регистр верблюжий, 62
Регулярные выражения, 265
Редактор кода Atom, 29, 34, 35, 38, 40, 41
Репрезентативность данных, 98, 102
Рефакторинг, 296
кода, 170
Родитель:
элемент XML, 152
С
Сводка статистик, 111
Секьюритизация, 224
Сеть Global Historical Climatology Network,
143
Символы управляющие, 33
Символьная константа, 63
Предметный указатель
Система:
COMPAS, 24
GitHub, 47
Словарь данных, 145, 239
Слово:
ключевое:
def, 66
from, 189
time, 93
Сложные типы данных, 60
Содержимое:
достоверность, 100
Соединение внешнее, 231
Соответствие данных, 23, 96, 97, 104, 107
Спецификации:
XML, 153
EPUB, 154
KML, 154
RSS, 154
SVG, 154
Список:
JSON, 161
Тип данных, 59
Среда разработки:
Colab, 42
Jupyter Notebook, 30, 35, 37, 38, 41
Среднее арифметическое, 111
Средства инспектирования, 211
Средство Git, 50
Стабильность данных, 257
Статистики, сводка, 111
Стек функций, 306
Столбец merge, 232
Строка запроса, 179, 184
Строковые методы, 63
Структура DataFrame, 228
Структуризация данных, 119
Структурированные данные, 120, 121, 122
Т
Теги XML, 151
Текущая папка, 32
Тип:
данных:
OrderedDict, 140
кортеж, 139
массив, 59
сложные, 60
список, 59
числовой, 59
документа:
объявление, 150
Токен:
доступа, 193, 202
носителя, 199, 204
Точка конечная, 149, 180, 184
Точность данных, 257
Транспонирование, 237
Тренды, 118
У
Управляющие символы, 33
Условный оператор if...else, 74, 89
Ф
Файл:
.gitignore, 188, 190
ghcnd-stations.txt, 145
gitignore, 188, 192
MTA_turnstiles_index.html, 211
Twitter_credentials.py, 199
расширение
.py, 29, 41
Файловые данные, 117
Файлы csv, 88
Флаговая переменная, 275, 276
Формат:
PDF, 165
XML:
.atom, 154
данных:
.csv, 123
json, 119
JSON, 147, 148
XML, 147, 148
канальные, 118
Форматер:
JSON Formatter, 164
JSONLint, 163
Функции, 63
встроенные, 63
create_meta_text(), 313
create_table_row(), 313
DictReader(), 129
| 429
430
|
Предметный указатель
enumerate(), 138
get_row(), 142
glob(), 170
greet_me(), 304
is_weekday(), 308
iter_rows(), 139
list(), 163
main(), 308
make_greeting(), 304
next(), 129
open(), 88, 127
pd.merge(), 232
pydoc, 317
range(), 127, 128
set(), 247
sleep(), 217
split(), 170
str(), 170
str(), 205
strip(), 217, 279
sum(), 72
to_datetime(), 226
определяемые пользователем, 64
пользовательские, 67, 297
названия, 297
стек, 306
Ц
Целостность данных, 23, 96, 104
Цикл:
for, 71
for…in, 89
авторизации, 193
Э
Элемент XML, 151
дочерний, 152
корневой, 152
прародитель, 152
родитель, 152