КулЛиб - Классная библиотека! Скачать книги бесплатно 

Python и анализ данных [Уэс Маккини] (pdf) читать онлайн

Книга в формате pdf! Изображения и текст могут не отображаться!


 [Настройки текста]  [Cбросить фильтры]
Уэс Маккини

Python и анализ данных

SECOND EDITION

Python for Data Analysis
Data Wrangling with Pandas, NumPy,
and IPython

Wes McKinney

Beijing • Boston • Farnham • Sebastopol • Tokyo

ВТОРОЕ ИЗДАНИЕ

Python и анализ данных
Первичная обработка данных
с применением pandas, NumPy и IPython

Уэс Маккини

Москва, 2020

УДК 004.438Python:004.6
ББК 32.973.22
М15

Маккини У.
М15 Python и анализ данных / пер. с анг. А. А. Слинкина. – М.: ДМК Пресс,
2020. – 540 с.: ил.
ISBN 978-5-94074-590-5
Второе издание этой книги дает современное практическое введение в разработку научных приложений на Python, ориентированных на обработку данных.
Код переписан под версию Python 3.6, добавлены сведения о последних версиях
библиотек pandas, NumPy, IPython и Jupyter.
Описаны те части языка Python и библиотеки для него, которые необходимы
для эффективного решения широкого круга аналитических задач: интерактивная оболочка IPython и Jupyter-блокноты, библиотеки NumPy и pandas, библиотека для визуализации данных matplotlib и др.
Издание подойдет как аналитикам, только начинающим осваивать обработку
данных, так и опытным программистам на Python, еще не знакомым с научными
приложениями.

УДК 004.438Python:004.6
ББК 32.973.22

Authorized Russian translation of the English edition of Python for Data Analysis, 2nd edition.
ISBN 9781491957660 © 2018 William McKinney.
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.
Все права защищены. Любая часть этой книги не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения
владельцев авторских прав.

ISBN 978-1-491-95766-0 (анг.)

Copyrigth © 2018 William McKinney

ISBN 978-5-94074-590-5 (рус.)

© Оформление, издание, перевод,
ДМК Пресс, 2020

Содержание

Предисловие ........................................................................................................................................................14
Об авторе ................................................................................................................................................................20
Об иллюстрации на обложке .................................................................................................................21
Глава 1. Предварительные сведения ..............................................................................................22
1.1.
1.2.

1.3.

1.4.

О чем эта книга? ......................................................................................................................................22
Какого рода данные?............................................................................................................................22
Почему именно Python?......................................................................................................................23
Python как клей ........................................................................................................................................23
Решение проблемы «двух языков»..............................................................................................24
Недостатки Python .................................................................................................................................24
Необходимые библиотеки для Python ......................................................................................25
NumPy ............................................................................................................................................................25
pandas.............................................................................................................................................................26
matplotlib .....................................................................................................................................................27
IPython и Jupyter ......................................................................................................................................27
SciPy ................................................................................................................................................................28
scikit-learn....................................................................................................................................................28
statsmodels..................................................................................................................................................29
Установка
30
Windows ........................................................................................................................................................30
Apple OS X ...................................................................................................................................................30
GNU/Linux ....................................................................................................................................................31
Установка или
...........................................................................31

6

1.5.
1.6.

Содержание

Python 2 и Python 3 ...............................................................................................................................32
Интегрированные среды разработки (IDE) .............................................................................32
Сообщество и конференции ............................................................................................................33
Структура книги ........................................................................................................................................34
Примеры кода ...........................................................................................................................................34
Данные для примеров .........................................................................................................................35
Соглашения об импорте ......................................................................................................................35
Жаргон ...........................................................................................................................................................35

Глава 2. Основы языка Python, IPython и Jupyter-блокноты .......................................36
2.1.
2.2.

2.3.

Интерпретатор Python .........................................................................................................................37
Основы IPython ........................................................................................................................................38
Запуск оболочки IPython....................................................................................................................38
Запуск Jupyter-блокнота .....................................................................................................................39
Завершение по нажатии клавиши Tab ......................................................................................42
Интроспекция ............................................................................................................................................43
Команда %run ............................................................................................................................................45
Исполнение кода из буфера обмена ..........................................................................................46
Комбинации клавиш .............................................................................................................................47
О магических командах ......................................................................................................................48
Интеграция с matplotlib......................................................................................................................50
Основы языка Python ...........................................................................................................................51
Семантика языка......................................................................................................................................51
Скалярные типы .......................................................................................................................................59
Поток управления ...................................................................................................................................66

Глава 3. Встроенные структуры данных, функции и файлы........................................71
3.1.

3.2.

Структуры данных и последовательности ...............................................................................71
Кортеж ............................................................................................................................................................71
Список ............................................................................................................................................................74
Встроенные функции последовательностей ..........................................................................79
Словарь ..........................................................................................................................................................81
Множество ...................................................................................................................................................85
Списковое, словарное и множественное включения .......................................................87
Функции ........................................................................................................................................................89
Пространства имен, области видимости и локальные функции ...............................90
Возврат нескольких значений ........................................................................................................91
Функции являются объектами .........................................................................................................91
Анонимные (лямбда) функции ........................................................................................................93
Каррирование: фиксирование части аргументов...............................................................94
Генераторы ..................................................................................................................................................94
Обработка исключений .......................................................................................................................97

Содержание

3.3.
3.4.

7

Файлы и операционная система ................................................................................................ 100
Байты и Unicode в применении к файлам ........................................................................... 102
Заключение ............................................................................................................................................. 104

Глава 4. Основы NumPy: массивы и векторные вычисления .................................. 105
4.1.

4.2.
4.3.

4.4.
4.5.
4.6.
4.7.
4.8.

NumPy ndarray: объект многомерного массива ................................................................ 107
Создание ndarray.................................................................................................................................. 108
Тип данных для ndarray ................................................................................................................... 110
Арифметические операции с массивами NumPy ............................................................ 113
Индексирование и вырезание..................................................................................................... 114
Булево индексирование .................................................................................................................. 119
Прихотливое индексирование .................................................................................................... 121
Транспонирование массивов и перестановка осей ....................................................... 123
Универсальные функции: быстрые поэлементные операции
над массивами ....................................................................................................................................... 125
Программирование с применением массивов ................................................................. 127
Запись логических условий в виде операций с массивами...................................... 129
Математические и статистические операции .................................................................... 131
Методы булевых массивов............................................................................................................. 132
Сортировка ............................................................................................................................................... 133
Устранение дубликатов и другие теоретико-множественные операции.......... 134
Файловый ввод-вывод массивов ............................................................................................... 135
Линейная алгебра ................................................................................................................................ 136
Генерация псевдослучайных чисел........................................................................................... 138
Пример: случайное блуждание ................................................................................................... 139
Моделирование сразу нескольких случайных блужданий ........................................ 141
Заключение ............................................................................................................................................. 142

Глава 5. Первое знакомство с pandas........................................................................................... 143
5.1.

5.2.

Введение в структуры данных pandas .................................................................................... 144
Объект Series ........................................................................................................................................... 144
Объект DataFrame ................................................................................................................................ 148
Индексные объекты ............................................................................................................................ 154
Базовая функциональность ........................................................................................................... 156
Переиндексация ................................................................................................................................... 156
Удаление элементов из оси ........................................................................................................... 159
Доступ по индексу, выборка и фильтрация ......................................................................... 161
Целочисленные индексы................................................................................................................. 165
Арифметические операции и выравнивание данных .................................................. 166
Применение функций и отображение .................................................................................... 172
Сортировка и ранжирование ........................................................................................................ 174
Индексы по осям с повторяющимися значениями ......................................................... 177

8
5.3.
5.4.

Содержание

Редукция и вычисление описательных статистик............................................................ 179
Корреляция и ковариация .............................................................................................................. 181
Уникальные значения, счетчики значений и членство ................................................. 183
Заключение ............................................................................................................................................. 186

Глава 6. Чтение и запись данных, форматы файлов ....................................................... 187
6.1.

6.2.
6.3.
6.4.
6.5.

Чтение и запись данных в текстовом формате ................................................................. 187
Чтение текстовых файлов порциями ....................................................................................... 193
Вывод данных в текстовом формате ....................................................................................... 195
Обработка данных в формате с разделителями .............................................................. 196
Данные в формате JSON .................................................................................................................. 198
XML и HTML: разбор веб-страниц............................................................................................. 200
Двоичные форматы данных .......................................................................................................... 203
Формат HDF5 .......................................................................................................................................... 204
Чтение файлов Microsoft Excel .................................................................................................... 206
Взаимодействие с HTML и Web API.......................................................................................... 207
Взаимодействие с базами данных ............................................................................................ 209
Заключение ............................................................................................................................................. 210

Глава 7. Очистка и подготовка данных ....................................................................................... 211
7.1.
7.2.

7.3.

7.4.

Обработка отсутствующих данных ........................................................................................... 211
Фильтрация отсутствующих данных ........................................................................................ 213
Восполнение отсутствующих данных ...................................................................................... 215
Преобразование данных................................................................................................................. 217
Устранение дубликатов .................................................................................................................... 217
Преобразование данных с помощью функции или отображения ........................ 219
Замена значений .................................................................................................................................. 221
Переименование индексов осей................................................................................................ 222
Дискретизация и раскладывание .............................................................................................. 223
Обнаружение и фильтрация выбросов .................................................................................. 226
Перестановки и случайная выборка ........................................................................................ 228
Вычисление индикаторных переменных.............................................................................. 229
Манипуляции со строками ............................................................................................................. 232
Методы строковых объектов......................................................................................................... 232
Регулярные выражения .................................................................................................................... 234
Векторные строковые функции в pandas ............................................................................. 237
Заключение ............................................................................................................................................. 240

Глава 8. Переформатирование данных: соединение,
комбинирование и изменение формы ....................................................................................... 241
8.1.

Иерархическое индексирование ............................................................................................... 241
Переупорядочение и уровни сортировки ............................................................................ 244

Содержание

8.2.

8.3.

8.4.

9

Сводная статистика по уровню ................................................................................................... 245
Индексирование с помощью столбцов DataFrame ........................................................ 246
Комбинирование и слияние наборов данных ................................................................... 247
Слияние объектов DataFrame как в базах данных ......................................................... 247
Соединение по индексу ................................................................................................................... 252
Конкатенация вдоль оси .................................................................................................................. 256
Комбинирование перекрывающихся данных.................................................................... 261
Изменение формы и поворот ...................................................................................................... 263
Изменение формы с помощью иерархического индексирования ....................... 263
Поворот из «длинного» в «широкий» формат ................................................................... 266
Поворот из «широкого» в «длинный» формат .................................................................. 270
Заключение ............................................................................................................................................. 272

Глава 9. Построение графиков и визуализация .................................................................. 273
9.1.

9.2.

9.3.
9.4.

Краткое введение в API библиотеки matplotlib ............................................................... 274
Рисунки и подграфики ...................................................................................................................... 275
Цвета, маркеры и стили линий .................................................................................................... 278
Риски, метки и надписи .................................................................................................................... 281
Аннотации и рисование в подграфике................................................................................... 284
Сохранение графиков в файле.................................................................................................... 286
Конфигурирование matplotlib ..................................................................................................... 288
Построение графиков с помощью pandas и seaborn .................................................... 288
Линейные графики .............................................................................................................................. 289
Столбчатые диаграммы .................................................................................................................... 291
Гистограммы и графики плотности ........................................................................................... 296
Диаграммы рассеяния....................................................................................................................... 299
Фасетные сетки и категориальные данные ......................................................................... 301
Другие средства визуализации для Python......................................................................... 303
Заключение ............................................................................................................................................. 303

Глава 10. Агрегирование данных и групповые операции .......................................... 304
10.1. Механизм GroupBy .............................................................................................................................. 305
Обход групп ............................................................................................................................................. 308
Группировка с помощью словарей и объектов Series................................................... 311
Группировка с помощью функций ............................................................................................. 312
Группировка по уровням индекса ............................................................................................. 313
10.2. Агрегирование данных..................................................................................................................... 313
Применение функций, зависящих от столбца и нескольких функций ............... 315
Возврат агрегированных данных без индексов строк ................................................. 319
10.3. Метод apply: часть общего принципа
разделения-применения-объединения.................................................................................. 319
Подавление групповых ключей .................................................................................................. 322

10

Содержание

Квантильный и интервальный анализы ................................................................................. 322
Пример: подстановка зависящих от группы значений вместо
отсутствующих ....................................................................................................................................... 324
Пример: случайная выборка и перестановка .................................................................... 326
Пример: групповое взвешенное среднее и корреляция ............................................ 328
Пример: групповая линейная регрессия ............................................................................... 330
10.4. Сводные таблицы и перекрестное табулирование......................................................... 331
Таблицы сопряженности .................................................................................................................. 334
10.5. Заключение ............................................................................................................................................. 335

Глава 11. Временные ряды ................................................................................................................... 336
11.1. Типы данных и инструменты, относящиеся к дате и времени................................. 337
Преобразование между строкой и datetime ...................................................................... 338
11.2. Основы работы с временными рядами ................................................................................. 341
Индексирование, выборка, подмножества .......................................................................... 342
Временные ряды с неуникальными индексами ............................................................... 345
11.3. Диапазоны дат, частоты и сдвиг.................................................................................................. 346
Генерация диапазонов дат ............................................................................................................. 347
Частоты и смещения дат .................................................................................................................. 349
Сдвиг данных (с опережением и с запаздыванием) ...................................................... 351
11.4. Часовые пояса........................................................................................................................................ 354
Локализация и преобразование................................................................................................. 355
Операции над объектами Timestamp с учетом часового пояса ............................. 357
Операции между датами из разных часовых поясов ................................................... 358
11.5. Периоды и арифметика периодов ............................................................................................ 359
Преобразование частоты периода............................................................................................ 360
Квартальная частота периода ...................................................................................................... 362
Преобразование временных меток в периоды и обратно ........................................ 363
Создание PeriodIndex из массивов ........................................................................................... 365
11.6. Передискретизация и преобразование частоты .............................................................. 367
Понижающая передискретизация ............................................................................................. 369
Повышающая передискретизация и интерполяция....................................................... 371
Передискретизация периодов..................................................................................................... 373
11.7. Скользящие оконные функции .................................................................................................... 374
Экспоненциально взвешенные функции .............................................................................. 378
Бинарные скользящие оконные функции ............................................................................ 379
Скользящие оконные функции, определенные пользователем ............................. 381
11.8. Заключение ............................................................................................................................................. 382

Глава 12. Дополнительные сведения о библиотеке NumPy ..................................... 383
12.1. Категориальные данные .................................................................................................................. 383
Для чего это нужно ............................................................................................................................. 383

Содержание

11

Категориальные типы в pandas................................................................................................... 385
Вычисления с категориальными значениями .................................................................... 388
Категориальные методы .................................................................................................................. 390
12.2. Дополнительные способы использования GroupBy ....................................................... 393
Групповые преобразования и GroupBy с «развертыванием» ................................. 393
Групповая передискретизация по времени ........................................................................ 397
12.3. Сцепление методов ............................................................................................................................ 399
Метод pipe ................................................................................................................................................ 400
12.4. Заключение ............................................................................................................................................. 401

Глава 13. Введение в библиотеки моделирования на Python ................................ 402
13.1. Интерфейс между pandas и кодом модели ......................................................................... 402
13.2. Описание моделей с помощью Patsy ...................................................................................... 405
Преобразование данных в формулах Patsy ........................................................................ 408
Категориальные данные и Patsy................................................................................................. 410
13.3. Введение в statsmodels ................................................................................................................... 412
Оценивание линейных моделей ................................................................................................ 413
Оценивание процессов с временными рядами................................................................ 416
13.4. Введение в scikit-learn ..................................................................................................................... 417
13.5. Продолжение своего образования ........................................................................................... 420

Глава 14. Примеры анализа данных ............................................................................................. 422
14.1. 1.usa.gov data from Bitly .................................................................................................................. 422
Подсчет часовых поясов на чистом Python ......................................................................... 423
Подсчет часовых поясов с помощью pandas ...................................................................... 425
14.2. Набор данных MovieLens 1M ....................................................................................................... 432
Измерение несогласия в оценках ............................................................................................. 437
14.3. Имена, которые давали детям в США за период с 1880 по 2010 год................. 439
Анализ тенденций в выборе имен ............................................................................................ 444
14.4. База данных о продуктах питания министерства сельского
хозяйства США ....................................................................................................................................... 453
14.5. База данных федеральной избирательной комиссии .................................................. 459
Статистика пожертвований по роду занятий и месту работы .................................. 462
Распределение суммы пожертвований по интервалам............................................... 465
Статистика пожертвований по штатам ................................................................................... 467
14.6. Заключение ............................................................................................................................................. 468

Приложение A. Дополнительные сведения о библиотеке NumPy...................... 469
A.1. Внутреннее устройство объекта ndarray ............................................................................... 469
Иерархия типов данных в NumPy ............................................................................................. 470
A.2. Дополнительные манипуляции с массивами...................................................................... 471

12

A.3.
A.4.
А.5.
A.6.

A.7.
A.8.
A.9.

Содержание

Изменение формы массива........................................................................................................... 472
Упорядочение элементов массива в C и в Fortran.......................................................... 474
Конкатенация и разбиение массива ........................................................................................ 474
Повторение элементов: функции tile и repeat .................................................................. 477
Эквиваленты прихотливого индексирования: функции take и put ...................... 479
Укладывание ........................................................................................................................................... 480
Укладывание по другим осям ...................................................................................................... 482
Установка элементов массива с помощью укладывания............................................ 484
Дополнительные способы использования универсальных функций .................. 485
Методы экземпляра u-функций .................................................................................................. 485
Написание новых u-функций на Python ............................................................................... 488
Структурные массивы ........................................................................................................................ 489
Вложенные типы данных и многомерные поля................................................................ 489
Зачем нужны структурные массивы?....................................................................................... 490
Еще о сортировке................................................................................................................................. 491
Косвенная сортировка: методы argsort и lexsort............................................................. 492
Альтернативные алгоритмы сортировки ............................................................................... 493
Частичная сортировка массивов ................................................................................................ 494
Метод numpy.searchsorted: поиск элементов в отсортированном
массиве ....................................................................................................................................................... 495
Написание быстрых функций для NumPy с помощью Numba ................................ 496
Создание пользовательских объектов numpy.ufunc с помощью Numba .......... 498
Дополнительные сведения о вводе-выводе массивов ................................................ 498
Файлы, спроецированные на память ...................................................................................... 498
HDF5 и другие варианты хранения массива ...................................................................... 500
Замечания о производительности ............................................................................................ 500
Важность непрерывной памяти .................................................................................................. 500

Приложение B. Еще о системе IPython ....................................................................................... 503
B.1. История команд..................................................................................................................................... 503
Поиск в истории команд и повторное выполнение ....................................................... 503
Входные и выходные переменные ........................................................................................... 504
B.2. Взаимодействие с операционной системой ....................................................................... 505
Команды оболочки и псевдонимы ............................................................................................ 506
Система закладок на каталоги..................................................................................................... 507
B.3. Средства разработки программ.................................................................................................. 507
Интерактивный отладчик ................................................................................................................ 507
Хронометраж программы: %time и %timeit........................................................................ 512
Простейшее профилирование: %prun и %run -p.............................................................. 514
Построчное профилирование функции ................................................................................. 516
B.4. Советы по продуктивной разработке кода с использованием IPython............. 518
Перезагрузка зависимостей модуля ........................................................................................ 518

Содержание

13

Советы по проектированию программ .................................................................................. 519
B.5. Дополнительные возможности IPython ................................................................................. 521
Делайте классы дружественными к IPython ....................................................................... 521
Профили и конфигурирование ................................................................................................... 521
B.6. Заключение ............................................................................................................................................. 523

Предметный указатель............................................................................................................................. 524

Предисловие

Что нового во втором издании?
Первое издание этой книги вышло в 2012 году, когда Python-библиотеки для
анализа данных с открытым исходным кодом (в частности, pandas) были
еще внове и быстро развивались. В этом исправленном и дополненном издании я переработал главы, стремясь отразить как несовместимые изменения
и устаревшие возможности, так и новые средства, появившиеся за прошедшие пять лет. Я также добавил новый материал с описанием инструментов,
которые либо еще не существовали в 2012 году, либо были недостаточно зрелыми. Наконец, я старался не писать о новых или активно разрабатываемых
проектах с открытым кодом, которым, возможно, не суждено дожить до зрелости. Хотелось бы представить читателям этого издания средства, которые
не утратят актуальности ни в 2020, ни в 2021 году.
Ниже перечислены основные отличия второго издания.
• Весь код, включая пособие по Python, обновлен и доведен до уровня
версии 3.6 (в первом издании использовалась версия Python 2.7).
• Обновлены инструкции по установке Python из дистрибутива Anaconda
Python Distribution, а также всех дополнительных Python-пакетов.
• Внесены исправления, соответствующие последним версиям библиотеки pandas, существовавшим в 2017 году.
• Добавлена глава о дополнительных средствах pandas, дающая также ряд
других советов по работе с библиотекой.
• Размещено краткое введение в библиотеки statsmodels и scikit-learn.
Кроме того, материал, вошедший в первое издание, подвергнут реорганизации, чтобы книгу было проще читать начинающим пользователям.

О примерах кода

15

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

Так обозначается замечание общего характера.

Так обозначается предостережение или предупреждение.

О примерах кода
Файлы данных и прочие материалы, организованные по главам, можно найти
в репозитории книги на GitHub: http://github.com/wesm/pydata-book.
Эта книга призвана помогать вам в работе, поэтому вы можете использовать приведенный в ней код в собственных программах и в документации.
Спрашивать у нас разрешения необязательно, если только вы не собираетесь воспроизводить значительную часть кода. Например, не возбраняется
включить в свою программу несколько фрагментов кода из книги, однако
для продажи или распространения примеров из книг издательства O’Reilly
на компакт-диске разрешение требуется. Цитировать книгу и примеры в ответах на вопросы можно без ограничений, но для включения значительных
объемов кода в документацию по собственному продукту нужно получить
позволение.
Мы высоко ценим (хотя и не требуем их размещения) ссылки на наши издания. В ссылке обычно указывается название книги, имя автора, издатель-

16

Предисловие

ство и ISBN, например: «Python for Data Analysis by Wes McKinney (O’Reilly).
Copyright 2017 Wes McKinney, 978-1-491-95766-0».
Если вы полагаете, что планируемое использование кода выходит за рамки
изложенной выше лицензии, пожалуйста, обратитесь к нам по адресу permissions@oreilly.com.

Отзывы и пожелания
Мы всегда рады отзывам наших читателей. Расскажите нам, что вы думаете
об этой книге – что понравилось или, может быть, не понравилось. Отзывы
важны для нас, чтобы выпускать книги, которые будут для вас максимально
полезны.
Вы можете написать отзыв на нашем сайте www.dmkpress.com, зайдя на
страницу книги и оставив комментарий в разделе «Отзывы и рецензии». Также можно послать письмо главному редактору по адресу dmkpress@gmail.com;
при этом укажите название книги в теме письма.
Если вы являетесь экспертом в какой-либо области и заинтересованы в написании новой книги, заполните форму на нашем сайте по адресу http://
dmkpress.com/authors/publish_book/ или напишите в издательство по адресу
dmkpress@gmail.com.

Скачивание исходного кода примеров
Скачать файлы с дополнительной информацией для книг издательства «ДМК
Пресс» можно на сайте www.dmkpress.com или www.дмк.рф на странице с описанием соответствующей книги.

Список опечаток
Хотя мы приняли все возможные меры для того, чтобы обеспечить высокое
качество наших текстов, ошибки все равно случаются. Если вы найдете ошибку в одной из наших книг – возможно, ошибку в основном тексте или программном коде, – мы будем очень благодарны, если вы сообщите нам о ней.
Сделав это, вы избавите других читателей от недопонимания и поможете нам
улучшить последующие издания этой книги.
Если вы найдете какие-либо ошибки в коде, пожалуйста, сообщите о них
главному редактору по адресу dmkpress@gmail.com, и мы исправим это в следующих тиражах.

Нарушение авторских прав
Пиратство в интернете по-прежнему остается насущной проблемой. Издательства «ДМК Пресс» и Packt очень серьезно относятся к вопросам защиты

Благодарности

17

авторских прав и лицензирования. Если вы столкнетесь в интернете с незаконной публикацией какой-либо из наших книг, пожалуйста, пришлите нам
ссылку на интернет-ресурс, чтобы мы могли применить санкции.
Ссылку на подозрительные материалы можно прислать по адресу электронной почты dmkpress@gmail.com.
Мы высоко ценим любую помощь по защите наших авторов, благодаря
которой мы можем предоставлять вам качественные материалы.

Благодарности
Эта работа – плод многолетних плодотворных обсуждений и совместной работы с многочисленными людьми со всего света. Хочу поблагодарить некоторых из них.

Памяти Джона Д. Хантера (1968–2012)
В августе 2012 года после многолетней борьбы с раком толстой кишки ушел
из жизни наш дорогой друг и коллега Джон Д. Хантер. Это произошло почти
сразу после того, как я закончил рукопись первого издания книги.
Роль и влияние Джона на сообщества, специализирующиеся на применении Python в научных приложениях и обработке данных, трудно переоценить. Помимо разработки библиотеки matplotlib в начале 2000-х годов (время, когда Python был далеко не так популярен, как сейчас) он помогал формировать культуру целого поколения разработчиков открытого кода, ставших
впоследствии столпами экосистемы Python, которую мы часто считаем само
собой разумеющейся.
Мне повезло познакомиться с Джоном в начале своей работы над открытым кодом в январе 2010 года, сразу после выхода версии pandas 0.1. Его
вдохновляющее руководство помогало мне даже в самые тяжелые моменты
не отказываться от своего видения pandas и Python как полноправного языка
для анализа данных.
Джон был очень близок с Фернандо Пересом (Fernando Perez) и Брайаном
Грейнджером (Brian Granger), которые заложили основы IPython и Jupyter
и были авторами многих других инициатив в сообществе Python. Мы надеялись работать над книгой вчетвером, но в итоге только у меня оказалось
достаточно свободного времени. Я уверен, что он гордился бы тем, чего мы
достигли, порознь и совместно, за прошедшие пять лет.

Благодарности ко второму изданию (2017)
Прошло почти пять лет с того времени, как я закончил рукопись первого
издания книги. Случилось это в июле 2012 года. С тех пор многое изменилось.
Сообщество Python неизмеримо выросло, а сложившаяся вокруг него экосистема программных продуктов с открытым исходным кодом процветает.

18

Предисловие

Новое издание не появилось бы на свет без неустанных усилий разработчиков ядра pandas, благодаря которым этот проект и сложившееся вокруг него сообщество превратились в один из краеугольных камней экосистемы Python в области науки о данных. Назову лишь некоторых: Том Аугспургер (Tom
Augspurger), Йорис ван ден Боше (Joris van den Bossche), Крис Бартак (Chris
Bartak), Филлип Клауд (Phillip Cloud), gfyoung, Энди Хэйден (Andy Hayden),
Масааки Хорикоши (Masaaki Horikoshi), Стивен Хойер (Stephan Hoyer), Адам
Клейн (Adam Klein), Воутер Овермейр (Wouter Overmeire), Джэфф Ребэк (Jeff
Reback), Чань Ши (Chang She), Скиппер Сиболд (Skipper Seabold), Джефф Трэтнер (Jeff Tratner) и дp.
Что касается собственно подготовки издания, то я благодарю сотрудников
издательства O’Reilly, которые терпеливо помогали мне на протяжении всего
процесса работы над книгой, а именно Мари Божуро (Marie Beaugureau), Бена
Лорика (Ben Lorica) и Коллин Топорек (Colleen Toporek). В очередной раз у меня были замечательные технические редакторы: Том Аугспургер, Пол Бэрри
(Paul Barry), Хью Браун (Hugh Brown), Джонатан Коу (Jonathan Coe) и Андреас
Маллер (Andreas Muller). Спасибо вам.
Первое издание книги переведено на ряд иностранных языков, в том числе
на китайский, французский, немецкий, японский, корейский и русский. Перевод этого текста с целью сделать его доступным более широкой аудитории –
трудное и зачастую неблагодарное занятие. Благодарю вас за то, что помогаете людям во всем мире учиться программировать и использовать средства
анализа данных.
Мне также повезло пользоваться на протяжении нескольких последних лет
поддержкой своих трудов по разработке ПО с открытым исходным кодом
со стороны сайта Cloudera и фонда Two Sigma Investments. В то время как
открытые проекты получают все меньший объем ресурсов, несопоставимый
с количеством пользователей, очень важно, чтобы коммерческие компании
поддерживали разработку ключевых программных проектов. Это было бы
правильно.

Благодарности к первому изданию (2012)
Мне было бы трудно написать эту книгу без поддержки многих людей.
Из сотрудников издательства O’Reilly я крайне признателен редакторам –
Меган Бланшетт (Meghan Blanchette) и Джулии Стил (Julie Steele), которые направляли меня на протяжении всего процесса. Майк Лоукидес (Mike Loukides)
также работал со мной на стадии подачи предложения и помогал с выпуском
книги в свет.
В техническом рецензировании книги многие принимали участие. Мартин
Лас (Martin Blais) и Хью Браун (Hugh Brown) оказали неоценимую помощь
в повышении качества примеров, ясности изложения и улучшении организации книги в целом. Джеймс Лонг (James Long), Дрю Конвей (Drew Conway),
Фернандо Перес, Брайан Грейнджер, Томас Клюйвер (Thomas Kluyver), Адам

Благодарности

19

Клейн, Джон Клейн, Чань Ши и Стефан ван дер Вальт (Stefan van der Walt) отрецензировали по одной или по нескольку глав и сделали ценные замечания
с разных точек зрения.
Я почерпнул немало отличных идей для примеров и наборов данных в беседах с друзьями и коллегами, в том числе с Майком Дьюаром (Mike Dewar),
Джеффом Хаммербахером (Jeff Hammerbacher), Джеймсом Джондроу (James
Johndrow), Кристианом Ламом (Kristian Lum), Адамом Клейном, Хилари Мейсон (Hilary Mason), Чань Ши и Эшли Вильямсом (Ashley Williams).
Конечно, я в долгу у многих лидеров сообщества, применяющего открытое ПО на Python в научных приложениях, поскольку именно они заложили
фундамент моей работы и воодушевляли меня, пока я писал книгу. Это те,
кто разрабатывал ядро IPython (Фернандо Перес, Брайан Грейнджер, Мин
Рэган-Келли, Томас Клюйвер и др.), Джон Хантер, Скиппер Сиболд, Трэвис
Олифант (Travis Oliphant), Питер Вонг (Peter Wang), Эрик Джонс (Eric Jones),
Робер Керн (Robert Kern), Джозеф Перктольд (Josef Perktold), Франческ Альтед
(Francesc Alted), Крис Фоннесбек (Chris Fonnesbeck) и многие, многие другие.
Еще несколько человек оказывали мне значительную поддержку, делились
идеями и подбадривали на протяжении всего пути: Дрю Конвей, Шон Тэйлор
(Sean Taylor), Джузеппе Палеолого (Giuseppe Paleologo), Джаред Дандер (Jared
Lander), Дэвид Эпштейн (David Epstein), Джон Кроуос (John Krowas), Джошуа
Блум (Joshua Bloom), Дэн Пилсуорт (Den Pilsworth), Джон Майлз-Уайт (John
Myles-White) и многие другие, о которых я забыл.
Я также благодарен всем, кто повлиял на становление меня как ученого. В первую очередь это мои бывшие коллеги по компании AQR, которые
поддерживали мою работу над pandas в течение многих лет: Алекс Рейфман
(Alex Reyfman), Майкл Вонг (Michael Wong), Тим Сарджен (Tim Sargen), Октай
Курбанов (Oktay Kurbanov), Мэтью Щанц (Matthew Tschantz), Рони Израэлов
(Roni Israelov), Майкл Кац (Michael Katz), Крис Уга (Chris Uga), Прасад Раманан
(Prasad Ramanan), Тэд Сквэр (Ted Square) и Хун Ким (Hoon Kim). И наконец,
благодарю моих университетских наставников Хэйнса Миллера (МТИ) и Майка Уэста (университет Дьюк).
Если говорить о личной жизни, то я благодарен Кэйси Динкин (Casey Dinkin), чью каждодневную поддержку невозможно переоценить. Спасибо той,
кто терпел перепады моего настроения, когда я пытался собрать окончательный вариант рукописи, несмотря на свой и так уже перегруженный график.
Благодарю и моих родителей, Билла и Ким, которые учили меня никогда не
отступать от мечты и не соглашаться на меньшее.

Об авторе
Уэс Маккини – разработчик программного обеспечения и предприниматель из Нью-Йорка. После получения степени бакалавра математики в МТИ
в 2007 году его приняли на работу в компанию AQR Capital Management
в Гринвиче, где занимался финансовой математикой. Неудовлетворенный
малопригодными средствами анализа данных, Уэс изучил язык Python и приступил к созданию того, что в будущем стало проектом pandas. Сейчас он
активный член сообщества обработки данных на Python и агитирует за использование Python в анализе данных, финансовых задачах и математической статистике.
Впоследствии Уэс стал сооснователем и генеральным директором компании DataPad, технологические активы и коллектив которой в 2014 году приобрела компания Cloudera. С тех пор он занимается технологиями больших
данных и является членом комитетов по управлению проектами Apache
Arrow и Apache Parquet, курируемыми фондом Apache Software Foundation.
В 2016 году автор перешел в компанию Two Sigma Investments из Нью-Йорка,
где продолжает трудиться над тем, чтобы средствами ПО с открытым исходным кодом сделать анализ данных быстрее и проще.

Об иллюстрации на обложке
На обложке книги изображена перохвостая тупайя (Ptilocercus lowii). Это единственный представитель своего вида в семействе Ptilocercidae рода Ptilocercus; остальные тупайи принадлежат семействуTupaiidae. Тупайи отличаются
длинным хвостом и мягким буро-желтым мехом. У перохвостой тупайи хвост
напоминает птичье перо, за что она и получила свое название. Тупайи всеядны, питаются преимущественно насекомыми, фруктами, семенами и небольшими позвоночными животными.
Эти дикие млекопитающие, обитающие в основном в Индонезии, Малайзии и Таиланде, известны хроническим потреблением алкоголя. Как выяснилось, малайские тупайи несколько часов в сутки пьют естественно ферментированный нектар пальмы Eugeissona tristis, что эквивалентно употреблению
10–12 стаканов вина, содержащего 3,8 % алкоголя. Но это не приводит к интоксикации их организма благодаря развитой способности расщеплять этиловый спирт, включая его в обмен веществ способами, недоступными человеку.
Кроме того, поражает отношение массы мозга к массе тела – оно больше, чем
у всех прочих млекопитающих, в том числе у человека.
Несмотря на название, перохвостая тупайя не является настоящей тупайей,
а ближе к приматам. Вследствие такого родства перохвостые тупайи стали
альтернативой приматам в медицинских экспериментах по изучению миопии, психосоциального стресса и гепатита.

Глава 1. Предварительные сведения

1.1. О чем эта книга?
Книга посвящена вопросам преобразования, обработки, очистки данных
и вычислениям на языке Python. Моя цель – предложить руководство по тем
частям языка программирования Python и экосистемы его библиотек и инструментов, относящихся к обработке данных, которые помогут вам стать хорошим аналитиком данных. Хотя в названии книги фигурируют слова «анализ
данных», основной упор сделан на программировании на Python, на библиотеках и инструментах, а не на методологии анализа данных как таковой. Речь
идет о программировании на Python, необходимом для анализа данных.

Какого рода данные?
Говоря «данные», я имею в виду прежде всего структурированные данные. Это намеренно расплывчатый термин, охватывающий различные часто
встречающиеся виды данных, как то:
• табличные данные – в разных столбцах они могут иметь разный тип
(строки, числа, даты или еще что-то). Сюда относятся данные, которые
обычно хранятся в реляционных базах или в файлах с запятой в качестве разделителя;
• многомерные списки (матрицы);
• данные, представленные в виде нескольких таблиц, связанных между
собой по ключевым столбцам (то, что в SQL называется первичными
и внешними ключами);
• равноотстоящие и неравноотстоящие временные ряды.

Почему именно Python?

23

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

1.2. Почему именно Python?
Для многих людей (и для меня в том числе) Python – язык, в который нельзя не влюбиться. С момента появления в 1991 году Python стал одним из
самых популярных динамических языков программирования наряду с Perl,
Ruby и др. Относительно недавно Python и Ruby приобрели особую популярность как средства создания веб-сайтов в многочисленных каркасах, например Rails (Ruby) и Django (Python). Такие языки часто называют скриптовыми, потому что они используются для быстрого написания небольших
программ – скриптов. Лично мне термин «скриптовый язык» не нравится,
потому что он наводит на мысль, будто для создания ответственного программного обеспечения язык не годится. Из всех интерпретируемых языков Python выделяется большим и активным сообществом научных расчетов
и анализа данных. За последние десять лет Python превратился из ультрасовременного языка научных расчетов, которым пользуются на свой страх
и риск, в один из самых важных языков, применяемых в науке о данных,
в машинном обучении и разработке ПО общего назначения в академических
учреждениях и промышленности.
При анализе данных и интерактивных научно-исследовательских расчетов
с визуализацией результатов Python неизбежно приходится сравнивать со
многими предметно-ориентированными языками программирования и инструментами – с открытым исходным кодом и коммерческими, такими как R,
MATLAB, SAS, Stata и др. Сравнительно недавно появились улучшенные библиотеки для Python (прежде всего pandas), и он стал серьезным конкурентом
в решении задач манипулирования данными. А так как Python еще – и универсальный язык программирования, то это отличный выбор для создания
приложений обработки данных.

Python как клей
Своим успехом в области научных расчетов Python отчасти обязан простоте интеграции с кодом на C, C++ и FORTRAN. Во многих современных вычислительных средах применяется общий набор унаследованных библиотек,

24

Предварительные сведения

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

Решение проблемы «двух языков»
Во многих организациях принято для научных исследований, создания
опытных образцов и проверки новых идей использовать предметно-ориентированные языки типа MATLAB или R, а затем переносить удачные разработки в производственную систему, написанную на Java, C# или C++. Но все
чаще люди приходят к выводу, что Python подходит не только для исследования и создания прототипа, но и для построения самих производственных
систем. Полагаю, что компании большей частью будут выбирать этот путь,
потому что использование учеными и технологами одного и того же набора
программных средств, несомненно, выгодно для организации.

Недостатки Python
Python – великолепная среда для создания приложений для научных расчетов и большинства систем общего назначения, но тем не менее существуют
задачи, которым Python не очень подходит.
Поскольку Python – интерпретируемый язык программирования, в общем случае написанный на нем код работает значительно медленнее, чем
эквивалентный код на компилируемом языке типа Java или C++. Но поскольку время программиста обычно стоит гораздо дороже времени процессора, многих такой компромисс устраивает. Однако в приложениях, где
задержка должна быть очень мала (например, в торговых системах с большим количеством транзакций), время, потраченное на программирование
на низкоуровневом и не обеспечивающем максимальную продуктивность
языке типа C++ во имя достижения максимальной производительности, будет потрачено не зря.
Python не идеальный язык для программирования многопоточных приложений с высокой степенью параллелизма, особенно при наличии многих
потоков, активно использующих процессор. Проблема связана с наличием
глобальной блокировки интерпретатора (GIL) – механизма, который не дает
интерпретатору исполнять более одной команды байт-кода Python в каждый

Необходимые библиотеки для Python

25

момент времени. Объяснение технических причин существования GIL выходит за рамки этой книги, но на данный момент представляется, что GIL
вряд ли скоро исчезнет. И хотя во многих приложениях обработки больших
объектов данных, для того чтобы сократить срок работы, приходится организовывать кластер машин, встречаются все же ситуации, когда более желательна однопроцессная многопоточная система.
Я не хочу сказать, что Python вообще непригоден для исполнения многопоточного параллельного кода. Написанные на C или C++ расширения Python,
пользующиеся платформенной многопоточностью, могут исполнять код параллельно и не ограничены механизмом GIL, при условии что им не нужно
регулярно взаимодействовать с Python-объектами.

1.3. Необходимые библиотеки для Python
Для читателей, плохо знакомых с экосистемой Python и используемыми
в книге библиотеками, я сделаю краткий обзор библиотек.

NumPy
NumPy, сокращение от «Numerical Python», – основной пакет для выполнения научных расчетов на Python. Большая часть этой книги базируется на
NumPy и построенных поверх него библиотеках. В числе прочего он предоставляет:
• быстрый и эффективный объект многомерного массива ndarray;
• функции для выполнения вычислений над элементами одного массива
или математических операций с несколькими массивами;
• средства для чтения и записи на диски наборов данных, представленных в виде массивов;
• операции линейной алгебры, преобразование Фурье и генератор случайных чисел;
• зрелый C API, позволяющий обращаться к структурам данных и вычислительным средствам NumPy из расширений Python и кода на C или
C++.
Помимо ускорения работы с массивами, одной из основных целей NumPy
в части анализа данных является организация контейнера для передачи данных между алгоритмами. Как средство хранения и манипуляции данными
массивы NumPy куда эффективнее встроенных в Python структур данных.
Кроме того, библиотеки, написанные на низкоуровневом языке типа C или
Fortran, могут работать с данными, хранящимися в массиве NumPy, вообще
без копирования в другое представление. Таким образом, многие средства
вычислений, ориентированные на Python, либо используют массивы NumPy
в качестве основной структуры данных, либо каким-то иным способом организуют интеграцию с NumPy.

26

Предварительные сведения

pandas
Библиотека pandas предоставляет структуры данных и функции, призванные сделать работу со структурированными данными простой, быстрой и выразительной. С момента появления в 2010 году она способствовала превращению Python в мощную и продуктивную среду анализа данных. Основные
объекты pandas, используемые в книге, – это DataFrame – двумерная таблица,
в которой строки и столбцы имеют метки, и Series – объект одномерного
массива с метками.
В библиотеке pandas сочетаются высокая производительность средств работы с массивами, присущая NumPy, и гибкие возможности манипулирования данными, свойственные электронным таблицам и реляционным базам
данных (например, на основе SQL). Она предоставляет развитые средства
индексирования, позволяющие без труда изменять форму наборов данных,
формировать продольные и поперечные срезы, выполнять агрегирование
и выбирать подмножества. Поскольку манипулирование данными, их подготовка и очистка играют огромную роль в анализе данных, в этой книге
библиотека pandas будет одним из основных инструментов.
Если кому интересно, я начал разрабатывать pandas в начале 2008 года,
когда работал в компании AQR Capital Management, занимающейся управлением инвестициями. Тогда я сформулировал специфический набор требований, которым не удовлетворял ни один отдельно взятый инструмент,
имевшийся в моем распоряжении:
• структуры данных с помеченными осями, которые поддерживали бы автоматическое или явное выравнивание данных, – это предотвратило бы
появление типичных ошибок при работе с невыровненными данными
и данными из разных источников, которые по-разному индексированы;
• встроенная функциональность временных рядов;
• одни и те же структуры данных должны поддерживать как временные
ряды, так и данные других видов;
• арифметические операции и упрощения должны сохранять метаданные;
• гибкая обработка отсутствующих данных;
• поддержка соединения и других реляционных операций, имеющихся
в популярных базах данных (например, на основе SQL).
Я хотел, чтобы вся эта функциональность находилась в одном месте и предпочтительно была реализована на языке, хорошо приспособленном для разработки ПО общего назначения. Python выглядел хорошим кандидатом на
эту роль, но в то время в нем не было подходящих встроенных структур
данных и средств. Поскольку изначально библиотека pandas создавалась для
решения финансовых задач и задач бизнес-аналитики, в ней особенно глубоко проработаны средства работы с временными рядами, ориентированные
на обработку данных с временными метками, которые порождаются бизнеспроцессами.

Необходимые библиотеки для Python

27

Пользователям языка статистических расчетов R название DataFrame покажется знакомым, потому что оно выбрано по аналогии с объектом data.
frame в R. В отличие от Python, фреймы данных уже встроены в язык R и его
стандартную библиотеку, поэтому многие средства, присутствующие в pandas, либо являются частью ядра R, либо предоставляются дополнительными
пакетами.
Само название pandas образовано как от panel data (панельные данные),
применяемого в эконометрике термина для обозначения многомерных
структурированных наборов данных, так и от фразы Python data analysis.

matplotlib
Библиотека matplotlib – самый популярный в Python инструмент для создания графиков и других способов визуализации двумерных данных. Первоначально она была написана Джоном Д. Хантером (John D. Hunter), а теперь
сопровождается большой группой разработчиков. Она отлично подходит для
создания графиков, пригодных для публикации. Хотя программистам на Python доступны и другие библиотеки визуализации, matplotlib используется
чаще всего и потому хорошо интегрирована с другими частями экосистемы.
На мой взгляд, если вам нужно средство визуализации, то это самый безопасный выбор.

IPython и Jupyter
Проект IPython (http://ipython.org/) начал реализовывать в 2001 году Фернандо Перес (Fernando Perez) в качестве побочного, имеющего целью создать
более удобный интерактивный интерпретатор Python. За прошедшие с тех
пор 16 лет он стал одним из самых важных элементов современного инструментария Python. Хотя IPython сам по себе не содержит никаких средств
вычислений или анализа данных, он изначально спроектирован, чтобы обеспечивать максимальную продуктивность интерактивных вычислений и разработки ПО. Он поощряет цикл выполнить – исследовать вместо привычного
цикла редактировать – компилировать – выполнить, свойственного многим
другим языкам программирования. Кроме того, он позволяет легко обращаться к оболочке и файловой системе операционной системы. Поскольку
написание кода анализа данных часто подразумевает исследование методом
проб и ошибок и опробование разных подходов, то благодаря IPython работу
удается выполнить быстрее.
В 2014 году Фернандо и команда разработки IPython анонсировали проект Jupyter (https://jupyter.org/) – широкую инициативу проектирования языково-независимых средств интерактивных вычислений. Веб-блокнот IPython
превратился в Jupyter-блокнот, который ныне поддерживает более 40 языков
программирования. Систему IPython теперь можно использовать как ядро
(языковой режим) для совместной работы Python и Jupyter.

28

Предварительные сведения

Сам IPython стал компонентом более широкого проекта Jupyter с открытым исходным кодом, предоставляющего продуктивную среду для интерактивных исследовательских вычислений. В своем самом старом и простом режиме это улучшенная оболочка Python, имеющая целью ускорить написание,
тестирование и отладку кода на Python. Систему IPython можно использовать
также через Jupyter-блокнот, интерактивный веб-блокнот, поддерживающий
десятки языков программирования. Оболочка IPython и Jupyter-блокноты
особенно полезны для исследования и визуализации данных.
Кроме того, Jupyter-блокноты позволяют создавать контент на языках разметки Markdown и HTML, т. е. готовить комбинированные документы, содержащие код и текст. На других языках программирования также реализованы
ядра для Jupyter, благодаря чему их можно использовать вместо Python.
Лично я при работе с Python использую в основном IPython – для выполнения, отладки и тестирования кода.
В сопроводительных материалах к книге (https://github.com/wesm/pydatabook) вы найдете Jupyter-блокноты, содержащие примеры кода к каждой главе.

SciPy
SciPy – собрание пакетов, предназначенных для решения различных стандартных вычислительных задач. Вот некоторые из них:
• scipy.integrate – подпрограммы численного интегрирования и решения
дифференциальных уравнений;
• scipy.linalg – подпрограммы линейной алгебры и разложения матриц,
дополняющие те, что включены в numpy.linalg;
• scipy.optimize – алгоритмы оптимизации функций (нахождения минимумов) и поиска корней;
• scipy.signal – средства обработки сигналов;
• scipy.sparse – алгоритмы работы с разреженными матрицами и решения разреженных систем линейных уравнений;
• scipy.special – обертка вокруг SPECFUN, написанной на Fortran-библиотеке, содержащей реализации многих стандартных математических
функций, в том числе гамма-функции;
• scipy.stats – стандартные непрерывные и дискретные распределения
вероятностей (функции плотности вероятности, формирования выборки, функции непрерывного распределения вероятности), различные
статистические критерии и дополнительные описательные статистики.
Совместно NumPy и SciPy достаточно полно заменяют значительную часть
системы MATLAB и многочисленные дополнения к ней.

scikit-learn
Проект scikit-learn (https://scikit-learn.org/stable/), запущенный в 2010 году,
с самого начала стал основным инструментарием машинного обучения про-

Необходимые библиотеки для Python

29

граммистов на Python. Всего за семь лет к нему присоединилось 1500 разработчиков со всего мира. В нем имеются подмодули для следующих моделей:
• классификация: метод опорных векторов, метод ближайших соседей,
случайные леса, логистическая регрессия и т. д.;
• регрессия: Lasso, гребневая регрессия и т. д.;
• кластеризация: метод k средних, спектральная кластеризация и т. д.;
• понижение размерности: метод главных компонент, отбор признаков,
матричная факторизация и т. д.;
• выбор модели: поиск на сетке, перекрестный контроль, метрики;
• предобработка: выделение признаков, нормировка.
Наряду с pandas, statsmodels и IPython библиотека scikit-learn сыграла
важнейшую роль для превращения Python в продуктивный язык программирования для науки о данных. Я не смогу включить в эту книгу полное
руководство по scikit-learn, но все же предложу краткое введение в некоторые используемые в ней модели и объясню, как их использовать совместно
с другими средствами.

statsmodels
Пакет статистического анализа statsmodels (http://www.statsmodels.org/stable/index.html) начал разрабатываться по инициативе профессора статистики
из Стэнфордского университета Джонатана Тэйлора (Jonathan Taylor), который реализовал ряд моделей регрессионного анализа, популярных в языке
программирования R. Скиппер Сиболд (Skipper Seabold) и Джозеф Перктольд
(Josef Perktold) формально создали новый проект statsmodels в 2010 году,
и с тех пор он набрал критическую массу заинтересованных пользователей
и соразработчиков. Натаниэль Смит (Nathaniel Smith) разработал проект
Patsy, который предоставляет средства для задания формул и моделей для
statsmodels по образцу системы формул в R.
По сравнению со scikit-learn, пакет statsmodels содержит алгоритмы классической (прежде всего частотной) статистики и эконометрики. В него входят
следующие подмодули:
• регрессионные модели: линейная регрессия, обобщенные линейные модели, робастные линейные модели, линейные модели со смешанными
эффектами и т. д.;
• дисперсионный анализ (ANOVA);
• анализ временных рядов: AR, ARMA, ARIMA, VAR и другие модели;
• непараметрические методы: ядерная оценка плотности, ядерная регрессия;
• визуализация результатов статистического моделирования.
Пакет statsmodels ориентирован в большей степени на статистический вывод, он дает оценки неопределенности и p-значения параметров. Напротив,
scikit-learn ориентирован главным образом на предсказание.

30

Предварительные сведения

Как и для scikit-learn, я создам краткое введение в statsmodels и объясню,
как им пользоваться в сочетании с NumPy и pandas.

1.4. Установка и настройка
Поскольку Python используется в самых разных приложениях, не существует
единственно верной процедуры установки Python и необходимых дополнительных пакетов. У многих читателей, скорее всего, нет среды, подходящей
для научных применений Python и проработки этой книги, поэтому я дам
подробные инструкции для разных операционных систем. Рекомендую использовать бесплатный дистрибутив Anaconda. На момент написания книги
Anaconda предлагается для версий Python 2.7 и 3.6, хотя в будущем это может
измениться. В книге используется Python 3.6, и я настоятельно рекомендую
работать именно с этой или более старшей версией.

Windows
В Windows начните со скачивания установщика Anaconda (https://www.
anaconda.com/distribution/). Рекомендую следовать инструкциям, опубликованным на странице скачивания Anaconda, при этом надо понимать, что после
выхода книги из печати они могли измениться.
По завершении установки проверьте, все ли сконфигурировано правильно. Откройте окно командной строки (это приложение называется cmd.exe),
для чего щелкните правой кнопкой мыши по меню Пуск и выберите пункт
Командная строка1. Попробуйте запустить интерпретатор Python, набрав
команду python. Должно появиться сообщение, соответствующее установленной версии Anaconda:
C:\Users\wesm>python
Python 3.5.2 |Anaconda 4.1.1 (64–bit)| (default, Jul 5 2016, 11:41:13)
[MSC v.1900 64 bit (AMD64)] on win32
>>>

Чтобы выйти из оболочки, нажмите Ctrl+Z или введите команду exit()
и щелкните по Enter.

Apple OS X
Скачайте установщик Anaconda для OS X Anaconda. Он должен называться
как-то вроде Anaconda3-4.1.0-MacOSX-x86_64.pkg. Дважды щелкните по pkgфайлу для запуска установщика. Установщик автоматически добавит путь
к исполняемому файлу Anaconda в ваш файл .bash_profile, полный путь к которому имеет вид /Users/$USER/.bash_profile.
1

В Windows 10 этого пункта в меню Пуск по умолчанию нет. Чтобы открыть окно
командной строки, найдите программу cmd.exe с помощью средства поиска. – Прим.
перев.

Установка и настройка

31

Для проверки работоспособности попробуйте запустить IPython из оболочки системы (для получения командной строки откройте приложение Terminal):
$ ipython

Чтобы выйти из оболочки, нажмите Ctrl+D или введите команду exit()
и щелкните по Enter.

GNU/Linux
Детали установки в Linux варьируются в зависимости от дистрибутива.
Я опишу процедуру, работающую в дистрибутивах Debian, Ubuntu, CentOS
и Fedora. Установка в основных чертах производится так же, как для OS X,
отличается только порядок установки Anaconda. Установщик представляет
собой скрипт оболочки, запускаемый из терминала. В зависимости от разрядности системы нужно выбрать установщик типа x86 (32-разрядный) или
x86_64 (64-разрядный). Имя соответствующего файла имеет вид Anaconda34.1.0-Linux-x86_64.sh. Для установки нужно выполнить такую команду:
$ bash Anaconda3–4.1.0–Linux–x86_64.sh
В некоторых дистрибутивах Linux менеджеры пакетов, например apt, располагают всеми необходимыми Python-пакетами. Ниже описывается установка
с помощью Anaconda, поскольку она одинакова во всех дистрибутивах и упрощает обновление пакетов до последней версии.

После подтверждения согласия с лицензией вам будет предложено указать
место установки файлов Anaconda. Я рекомендую устанавливать их в свой
домашний каталог, например /home/$USER/anaconda (вместо $USER подставьте
свое имя пользователя).
Установщик Anaconda может спросить, хотите ли вы добавить каталог bin/
в начало переменной $PATH. Если после установки возникнут проблемы, это
можно сделать вручную, модифицировав файл .bashrc (или .zshrc, если вы
пользуетесь оболочкой zsh) примерно таким образом:
export PATH=/home/$USER/anaconda/bin:$PATH

Затем запустите новый процесс терминала или повторно выполните файл
.bashrc командой source ~/.bashrc.

Установка или обновление Python-пакетов
Рано или поздно вам могут понадобиться дополнительные Python-пакеты,
не включенные в дистрибутив Anaconda. В общем случае они устанавливаются такой командой:
conda install package_name

Если это не работает, то, возможно, удастся установить пакет с помощью
менеджера пакетов pip:

32

Предварительные сведения

pip install package_name

Для обновления пакетов служит команда conda update:
conda update package_name

pip также поддерживает обновление, нужно только задать флаг ––upgrade:
pip install ––upgrade package_name

В этой книге вам не раз представится возможность попробовать эти команды в деле.
Если для установки пакетов вы используете и conda, и pip, то не следует пытаться обновлять пакеты conda с помощью pip, поскольку из-за этого может
повредиться среда. При работе с Anaconda или Miniconda всегда рекомендуется сначала попробовать обновить пакет командой conda.

Python 2 и Python 3
Первая версия в линейке интерпретаторов Python 3.x была выпущена
в конце 2008 года. В нее включены изменения, несовместимые с ранее написанным кодом для версий Python 2.x. Поскольку с момента выхода первой
версии Python в 1991 году прошло уже 17 лет, создание несовместимой версии Python 3 рассматривалось как благо – принимая во внимание все уроки
прошлого.
В 2012 году разработчики и пользователи приложений Python для научных
расчетов и анализа данных работали в основном с версиями 2.x, поскольку
многие пакеты еще не были переведены на Python 3. Поэтому в первом издании книги использовалась версия Python 2.7. Но теперь пользователи могут
выбирать между Python 2.x и 3.x, так как большинство библиотек поддерживают обе ветви.
Однако поддержка Python 2.x прекратится в 2020 году (это относится
и к критическим исправлениям безопасности), так что начинать новые проекты на Python 2.7 не стоит. Поэтому в книге используется Python 3.6 – стабильная, хорошо поддерживаемая и широко распространенная версия. Все
уже начали называть Python 2.x унаследованным Python, а Python 3.x – просто Python. Призываю и вас последовать этому примеру.
Я взял за основу версию Python 3.6. У вас может быть более свежая версия,
но все примеры кода должны быть совместимы с ней. В Python 2.7 некоторые
примеры могут работать иначе или не работать вовсе.

Интегрированные среды разработки (IDE)
Когда меня спрашивают о том, какой средой разработки я пользуюсь,
я почти всегда отвечаю: «IPython плюс текстовый редактор». Обычно я пишу программу и итеративно тестирую и отлаживаю ее по частям в IPython
или Jupyter-блокнотах. Полезно также иметь возможность интерактивно экс-

Сообщество и конференции

33

периментировать с данными и визуально проверять, получается ли в результате определенных манипуляций ожидаемый результат. Библиотеки pandas
и NumPy спроектированы с учетом простоты использования в оболочке.
Однако некоторые пользователи предпочитают разрабатывать программы
в полноценной IDE, а не в сравнительно примитивном текстовом редакторе
типа Emacs или Vim. Вот некоторые доступные варианты:
• PyDev (бесплатная) – IDE, построенная на платформе Eclipse;
• PyCharm от компании JetBrains (на основе подписки для коммерческих
компаний, бесплатна для разработчиков ПО с открытым исходным кодом);
• Python Tools для Visual Studio (для работающих в Windows);
• Spyder (бесплатная) – IDE, которая в настоящий момент поставляется
в составе Anaconda;
• Komodo IDE (коммерческая).

1.5. Сообщество и конференции
Помимо поиска в интернете можно пользоваться полезными рассылками,
посвященными применению Python в научных расчетах и для обработки
данных. Их участники быстро отвечают на вопросы. Вот некоторые из таких
ресурсов:
• pydata: группа Google по вопросам, относящимся к использованию Python для анализа данных и pandas;
• pystatsmodels: вопросы, касающиеся statsmodels и pandas;
• numpy-discussion: вопросы, касающиеся NumPy;
• рассылка по scikit-learn (scikit-learn@python.org) и машинному обучению
на Python вообще;
• scipy-user: общие вопросы использования SciPy и Python для научных
расчетов.
Я сознательно не публикую URL-адреса, потому что они часто меняются.
Поиск в интернете вам в помощь.
Ежегодно в разных странах проходят конференции для программистов на
Python. Если вы захотите пообщаться с другими программистами, которые
разделяют ваши интересы, то имеет смысл посетить какое-нибудь мероприятие (если есть такая возможность). Многие конференции оказывают финансовую поддержку тем, кто не может позволить себе вступительный взнос или
транспортные расходы. Приведу неполный перечень конференций:
• PyCon and EuroPython: две самые крупные, проходящие соответственно
в Северной Америке и в Европе;
• SciPy и EuroSciPy: конференции, ориентированные на научные применения Python, проходящие соответственно в Северной Америке и в Европе;

34

Предварительные сведения

• PyData: мировая серия региональных конференций, посвященных науке
о данных и анализу данных;
• международные и региональные конференции PyCon (полный список
см. на сайте http://pycon.org).

1.6. Структура книги
Если вы раньше никогда не программировали на Python, то имеет смысл потратить время на знакомство с главами 2 и 3, где я поместил очень краткое
руководство по языковым средствам Python, а также по оболочке IPython
и Jupyter-блокнотам. Эти знания необходимы для чтения книги. Если у вас
уже есть опыт работы с Python, то можете вообще пропустить эти главы или
пролистать их по диагонали.
Далее дается краткое введение в основные возможности NumPy, а более
подробное изложение имеется в приложении A. Затем мы познакомимся
с pandas и посвятим оставшуюся часть книги анализу данных с применением
pandas, NumPy и matplotlib (для визуализации). Я старался построить изложение по возможности поступательно, хотя иногда главы немного пересекаются,
и есть несколько случаев, когда используются еще не описанные концепции.
У разных читателей могут быть разные цели, но, вообще говоря, можно
предложить следующую классификацию задач.
• Взаимодействие с внешним миром – чтение и запись в файлы и хранилища данных различных форматов.
• Подготовка – очистка, переформатирование, комбинирование, нормализация, изменение формы, получение продольных и поперечных срезов, трансформация данных для анализа.
• Преобразование – применение математических и статистических операций к группам наборов данных для получения новых наборов (например, агрегирование большой таблицы по некоторым переменным).
• Моделирование и вычисления – связывание данных со статистическими
моделями, алгоритмами машинного обучения и иными вычислительными средствами.
• Презентация – создание интерактивных или статических графических
визуализаций или текстовых сводных отчетов.

Примеры кода
Примеры кода в большинстве случаев показаны так, как выглядят в оболочке IPython или Jupyter-блокнотах: ввод и вывод.
In [5]: КОД
Out[5]: РЕЗУЛЬТАТ

Это означает, что вы должны ввести код в блоке In в своей рабочей среде
и выполнить его, нажав клавишу Enter (или Shift-Enter в Jupyter). Результат
должен быть таким, как показано в блоке Out.

Структура книги

35

Данные для примеров
Наборы данных для примеров из каждой главы находятся в репозитории
на сайте GitHub: https://github.com/wesm/pydata-book. Вы можете получить их
либо с помощью командной утилиты системы управления версиями git, либо
скачав zip-файл репозитория с сайта. Если возникнут проблемы, заходите на
мой сайт (https://wesmckinney.com/), где выложены актуальные инструкции по
получению материалов к книге.
Я стремился сделать так, чтобы в репозиторий попало все необходимое для
воспроизведения примеров, но мог где-то ошибиться или что-то пропустить.
В таком случае пишите мне на адрес book@wesmckinney.com. Самый лучший
способ сообщить об ошибках, найденных в книге, – описать их на странице
опечаток на сайте издательства O’Reilly (https://www.oreilly.com/catalog/errata.
csp?isbn=0636920050896).

Соглашения об импорте
В сообществе Python принят ряд соглашений об именовании наиболее употребительных модулей:
import
import
import
import
import

numpy as np
matplotlib.pyplot as plt
pandas as pd
seaborn as sns
statsmodels as sm

Это означает, что np.arange – ссылка на функцию arange в пакете NumPy.
Так делается, потому что импорт всех имен из большого пакета, каким является NumPy (from numpy import *), считается среди разработчиков на Python
дурным тоном.

Жаргон
Я употребляю некоторые термины, встречающиеся как в программировании, так и в науке о данных, с которыми вы, возможно, незнакомы. Поэтому
приведу краткие определения.
• Переформатирование (Munge/Munging/Wrangling) – процесс приведения
неструктурированных и (или) замусоренных данных к структурированной или чистой форме. Слово вошло в лексикон многих современных
специалистов по анализу данных.
• Псевдокод – описание алгоритма или процесса в форме, напоминающей
код, хотя фактически это не есть корректный исходный код на каком-то
языке программирования.
• Синтаксический сахар – синтаксическая конструкция, которая не добавляет новую функциональность, а лишь вносит дополнительное удобство
или позволяет сделать код короче.

Глава 2. Основы языка Python,
IPython и Jupyter-блокноты
В 2011 и 2012 годах, когда я писал первое издание книги, ресурсов для изучения анализа данных с применением Python было гораздо меньше. Тут мы
имеем что-то похожее на проблему яйца и курицы: многие библиотеки, наличие которых мы сейчас считаем само собой разумеющимся, в том числе
pandas, scikit-learn и statsmodels, тогда были еще относительно незрелыми.
В 2017 году количество литературы по науке о данных, по анализу данных
и машинному обучению неуклонно растет, дополняя прежние работы по научным расчетам, предназначенные для специалистов по информатике, физике и другим дисциплинам. Есть также замечательные книги о самом языке
программирования Python и о том, как стать эффективным программистом.
Поскольку книга задумана как введение в работу с данными на Python,
считаю полезным дать замкнутый обзор некоторых наиболее важных особенностей встроенных в Python структур данных и библиотек с точки зрения
манипулирования данными. Поэтому в этой и следующей главах приводится
лишь информация, необходимая для чтения книги.
На мой взгляд, для продуктивного анализа данных вовсе необязательно
профессионально заниматься разработкой ПО на Python. Я призываю вас экспериментировать с примерами кода и изучать документацию по различным
типам, функциям и методам, используя оболочку IPython и Jupyter-блокноты.
Хотя я изо всех сил старался излагать материал поступательно, иногда могут
встретиться вещи, которые еще не объяснялись.
Книга посвящена в основном инструментам табличного анализа и подготовки данных для работы с большими наборами. Чтобы применить эти
инструменты, зачастую необходимо сначала преобразовать беспорядочные

Интерпретатор Python

37

данные низкого качества в более удобную табличную (или структурную)
форму. К счастью, Python – идеальный язык для быстрого приведения данных к нужному виду. Чем свободнее вы владеете языком, тем проще будет
подготовить новый набор данных для анализа.
Некоторые описанные в книге инструменты лучше изучать в интерактивном сеансе IPython или Jupyter. После того как научитесь запускать IPython
и Jupyter, я рекомендую проработать примеры, экспериментируя и пробуя
разные подходы. Как и в любом окружении, ориентированном на работу
с клавиатурой, полезно запомнить наиболее употребительные команды на
подсознательном уровне.
Некоторые базовые понятия Python, например классы и объектно-ориентированное программирование, в этой главе не рассматриваются, хотя их полезно
включить в арсенал средств для анализа данных. Желающим углубить свои
знания рекомендую дополнить эту главу официальным пособием по Python
(https://docs.python.org/3/) и, возможно, одной из многих замечательных книг
по программированию на Python вообще. Начать можно, например, с таких
книг:
• Python Cookbook, Third Edition, by David Beazley and Brian K. Jones (O’Reilly);
• Fluent Python by Luciano Ramalho (O’Reilly)1;
• Effective Python by Brett Slatkin (Pearson)2.

2.1. Интерпретатор Python
Python – интерпретируемый язык. Интерпретатор Python исполняет программу по одному предложению за раз. Стандартный интерактивный интерпретатор Python запускается из командной строки командой python:
$ python
Python 3.6.0 | packaged by conda–forge | (default, Jan 13 2017, 23:17:12)
[GCC 4.8.2 20140120 (Red Hat 4.8.2–15)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> a = 5
>>> print(a)
5

Строка >>> – это приглашение к вводу выражения. Для выхода из интерпретатора Python и возврата в командную строку нужно либо ввести команду
exit(), либо нажать Ctrl+D.
Для выполнения Python-программы нужно просто набрать команду python,
указав в качестве первого аргумента имя файла с расширением .py. Допустим, вы создали файл hello_world.py с таким содержимым:
print 'Hello world'
1
2

Рамальо Л. Python. К вершинам мастерства. М.: ДМК Пресс, 2016.
Слаткин Б. Секреты Python. М.: Вильямс, 2017.

38

Основы языка Python, IPython и Jupyter-блокноты

Чтобы выполнить его, достаточно ввести следующую команду (файл hello_
world.py должен находиться в текущем каталоге):
$ python hello_world.py
Hello world

Многие программисты выполняют свой код на Python именно таким образом, но в мире научных приложений и анализа данных принято использовать IPython, улучшенный и дополненный интерпретатор Python, или вебблокноты Jupyter, первоначально разработанные как часть проекта IPython.
Введение в IPython и Jupyter будет дано в этой главе, а углубленное описание
возможностей IPython – в приложении 3. С помощью команды %run IPython
исполняет код в указанном файле в том же процессе, что позволяет интерактивно изучать результаты по завершении выполнения.
$ ipython
Python 3.6.0 | packaged by conda–forge | (default, Jan 13 2017, 23:17:12)
Type "copyright", "credits" or "license" for more information.
IPython 5.1.0 –– An enhanced Interactive Python.
?
–> Introduction and overview of IPython's features.
%quickref –> Quick reference.
help
–> Python's own help system.
object? –> Details about 'object', use 'object??' for extra details.
In [1]: %run hello_world.py
Hello world
In [2]:

По умолчанию приглашение IPython содержит не стандартную строку >>>,
а строку вида In [2]:, включающую порядковый номер предложения.

2.2. Основы IPython
В этом разделе мы научимся запускать оболочку IPython и Jupyter-блокнот,
а также познакомимся с некоторыми важнейшими понятиями.

Запуск оболочки IPython
IPython можно запустить из командной строки, как и стандартный интерпретатор Python, только для этого служит команда ipython:
$ ipython
Python 3.6.0 | packaged by conda–forge | (default, Jan 13 2017, 23:17:12)
Type "copyright", "credits" or "license" for more information.
IPython 5.1.0 –– An enhanced Interactive Python.
?
–> Introduction and overview of IPython's features.
%quickref –> Quick reference.

Основы IPython
help
object?

39

–> Python's own help system.
–> Details about 'object', use 'object??' for extra details.

In [1]: a = 5
In [2]: a
Out[2]: 5

Чтобы выполнить произвольное предложение Python, нужно ввести его
и нажать клавишу Enter. Если ввести только имя переменной, то IPython
выведет строковое представление объекта:
In [5]: import numpy as np
In [6]: data = {i : np.random.randn() for i in range(7)}
In [7]: data
Out[7]:
{0: –0.20470765948471295,
1: 0.47894333805754824,
2: –0.5194387150567381,
3: –0.55573030434749,
4: 1.9657805725027142,
5: 1.3934058329729904,
6: 0.09290787674371767}

Здесь первые две строки содержат код на Python; во второй строке создается переменная data, ссылающаяся на только что созданный словарь Python.
В последней строке значение data выводится на консоль.
Многие объекты Python форматируются для удобства чтения; такая красивая печать отличается от обычного представления методом print. Тот же
словарь, напечатанный в стандартном интерпретаторе Python, выглядел бы
куда менее презентабельно:
>>> from numpy.random import randn
>>> data = {i : randn() for i in range(7)}
>>> print data
{0: –1.5948255432744511, 1: 0.10569006472787983, 2: 1.972367135977295,
3: 0.15455217573074576, 4: –0.24058577449429575, 5: –1.2904897053651216,
6: 0.3308507317325902}

IPython предоставляет также средства для исполнения произвольных блоков кода (путем копирования и вставки) и целых Python-скриптов. Эти вопросы будут рассмотрены чуть ниже.

Запуск Jupyter-блокнота
Одним из основных компонентов Jupyter-проекта является блокнот – интерактивный документ, содержащий код, текст (простой или размеченный),
визуализации и другие результаты выполнения кода. Jupyter-блокнот взаимодействует с ядрами – реализациями протокола интерактивных вычислений на

40

Основы языка Python, IPython и Jupyter-блокноты

различных языках программирования. В ядре Jupyter для Python в качестве
основы используется IPython.
Для запуска Jupyter выполните в терминале команду jupyter notebook:
$ jupyter notebook
[I 15:20:52.739 NotebookApp] Serving notebooks from local directory:
/home/wesm/code/pydata–book
[I 15:20:52.739 NotebookApp] 0 active kernels
[I 15:20:52.739 NotebookApp] The Jupyter Notebook is running at:
http://localhost:8888/
[I 15:20:52.740 NotebookApp] Use Control–C to stop this server and shut down
all kernels (twice to skip confirmation).
Created new window in existing browser session.

На многих платформах Jupyter автоматически открывается в браузере по
умолчанию (если только при запуске не был указан флаг ––no–browser). Если
это не так, то можете сами ввести URL при запуске блокнота, в данном случае
http://localhost:8888/. На рис. 12.1 показано, как выглядит блокнот в браузере
Google Chrome.
Многие используют Jupyter в качестве локальной среды вычислений, но его можно также развернуть на сервере и работать с ним удаленно. Я не буду вдаваться
в детали, при необходимости вы сможете найти информацию в интернете.

Рис. 2.1. Начальная страница Jupyter-блокнота

Для создания нового блокнота нажмите кнопку New и выберите «Python 3»
или «conda [default]». На экране появится окно, показанное на рис. 2.2. Если вы здесь впервые, попробуйте щелкнуть по пустой ячейке кода и ввести
строку кода на Python. Для выполнения нажмите Shift-Enter.

41

Основы IPython

Рис. 2.2. Так выглядит новый Jupyter-блокнот

После сохранения блокнота (команда Save and Checkpoint в меню File)
будет создан файл с расширением .ipynb. В нем содержится все, что сейчас
находится в блокноте (включая все результаты выполнения кода). Чтобы загрузить имеющийся блокнот, поместите файл в тот каталог, из которого был
запущен блокнот, или в его подкаталог и дважды щелкните по имени файла
на начальной странице. Можете попробовать проделать это с моими блокнотами, находящимися в репозитории wesm/pydata-book на GitHub (рис. 2.3).

Рис. 2.3. Пример существующего Jupyter-блокнота

42

Основы языка Python, IPython и Jupyter-блокноты

Хотя может показаться, что работа с Jupyter-блокнотами отличается от работы в оболочке IPython, на самом деле почти все описанные в этой главе
команды и инструменты можно использовать в обеих средах.

Завершение по нажатии клавиши Tab
На первый взгляд оболочка IPython очень похожа на стандартный интерпретатор Python (вызываемый командой python) с мелкими косметическими
изменениями. Одно из существенных преимуществ над стандартной оболочкой Python – завершение по нажатии клавиши Tab, реализованное в большинстве IDE и других средах интерактивных вычислений. Если во время ввода
выражения нажать , то оболочка произведет поиск в пространстве имен
всех переменных (объектов, функций и т. д.), имена которых начинаются
с введенной к этому моменту строки:
In [1]: an_apple = 27
In [2]: an_example = 42
In [3]: an
an_apple and an_example any

Обратите внимание, что IPython вывел обе определенные выше переменные, а также ключевое слово Python and и встроенную функцию any. Естественно, можно также завершать имена методов и атрибутовлюбого объекта,
если предварительно ввести точку:
In [3]: b = [1, 2, 3]
In [4]: b.
b.append b.extend b.insert b.remove b.sort
b.count b.index b.pop b.reverse

То же самое относится и к модулям:
In [1]: import datetime
In [2]: datetime.
datetime.date
datetime.MAXYEAR
datetime.datetime
datetime.MINYEAR
datetime.datetime_CAPI datetime.time

datetime.timedelta
datetime.tzinfo

В Jupyter-блокноте и новых версиях IPython (5.0 и старше) варианты автозавершения отображаются не в выпадающем окне, а в текстовом виде.
Отметим, что IPython по умолчанию скрывает методы и атрибуты, начинающиеся
знаком подчеркивания, например магические методы и внутренние «закрытые»
методы и атрибуты, чтобы не загромождать экран (и не смущать неопытных
пользователей). На них автозавершение также распространяется, нужно только
сначала набрать знак подчеркивания. Если вы предпочитаете всегда видеть такие методы при автозавершении, измените соответствующий режим в конфигурационном файле IPython. О том, как это сделать, см. документацию по IPython.

43

Основы IPython

Завершение по нажатии Tab работает во многих контекстах, помимо поиска в интерактивном пространстве имен и завершения атрибутов объекта
или модуля. Если нажать при вводе чего-то, похожего на путь к файлу
(даже внутри строки Python), то будет произведен поиск в файловой системе:
In [7]: datasets/movielens/
datasets/movielens/movies.dat
datasets/movielens/README
datasets/movielens/ratings.dat datasets/movielens/users.dat
In [7]: path = 'datasets/movielens/
datasets/movielens/movies.dat
datasets/movielens/README
datasets/movielens/ratings.dat datasets/movielens/users.dat

В сочетании с командой %run (см. ниже) эта функция несомненно позволит
вам меньше лупить по клавиатуре.
Автозавершение позволяет также сэкономить время при вводе именованных аргументов функции (в том числе самого знака =). См. рис. 2.4.

Рис. 2.4. Автозавершение именованных аргументов функции
в Jupyter-блокноте

Ниже мы еще поговорим о функциях.

Интроспекция
Если ввести вопросительный знак (?) до или после имени переменной, то
будет напечатана общая информация об объекте:
In [8]: b = [1, 2, 3]
In [9]: b?
Type:
list
String Form:[1, 2, 3]
Length:
3
Docstring:
list() –> new empty list
list(iterable) –> new list initialized from iterable's items
In [10]: print?

44

Основы языка Python, IPython и Jupyter-блокноты

Docstring:
print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)
Prints the values to a stream, or to sys.stdout by default.
Optional keyword arguments:
file: a file–like object (stream); defaults to the current sys.stdout.
sep: string inserted between values, default a space.
end: string appended after the last value, default a newline.
flush: whether to forcibly flush the stream.
Type: builtin_function_or_method

Это называется интроспекцией объекта. Если объект представляет собой
функцию или метод экземпляра, то будет показана строка документации,
если она существует. Допустим, мы написали такую функцию (этот код можно
ввести в IPython или Jupyter):
def add_numbers(a, b):
"""
Сложить два числа
Возвращает
––––––––––
the_sum : тип аргументов
"""
return a + b

Тогда при вводе знака ? мы увидим строку документации:
In [11]: add_numbers?
Signature: add_numbers(a, b)
Docstring:
Сложить два числа
Возвращает
––––––––––
the_sum : type of arguments
File:

Type:
function

Два вопросительных знака ?? покажут также исходный код функции, если
это возможно:
In [12]: add_numbers??
Signature: add_numbers(a, b)
Source:
def add_numbers(a, b):
"""
Сложить два числа
Возвращает
––––––––––
the_sum : тип аргументов

Основы IPython

45

"""
return a + b
File:
book_scripts/
Type:
function

И последнее применение ? – поиск в пространстве имен IPython по аналогии со стандартной командной строкой UNIX или Windows. Если ввести
несколько символов в сочетании с метасимволом *, то будут показаны все
имена по указанной маске. Например, вот как можно получить список всех
функций в пространстве имен верхнего уровня NumPy, имена которых содержат строку load:
In [13]: np.*load*?
np.__loader__
np.load
np.loads
np.loadtxt
np.pkgload

Команда %run
Команда %run позволяет выполнить любой файл как Python-программу
в контексте текущего сеанса IPython. Предположим, что в файле ipython_
script_test.py хранится такой простенький скрипт:
def f(x, y, z):
return (x + y) / z
a = 5
b = 6
c = 7.5
result = f(a, b, c)

Этот скрипт можно выполнить, передав имя файла команде %run:
In [14]: %run ipython_script_test.py

Скрипт выполняется в пустом пространстве имен (в которое ничего не
импортировано и в котором не определены никакие переменные), поэтому его поведение должно быть идентично тому, что получается при запуске
программы из командной строки командой python script.py. Все переменные
(импортированные, функции, глобальные объекты), определенные в файле
(до момента исключения, если таковое произойдет), будут доступны оболочке
IPython:
In [15]: c
Out[15]: 7.5
In [16]: result
Out[16]: 1.4666666666666666

46

Основы языка Python, IPython и Jupyter-блокноты

Если Python-скрипт ожидает передачи аргументов из командной строки
(которые должны попасть в массив sys.argv), то их можно перечислить после
пути к файлу, как в командной строке.
Если вы хотите дать скрипту доступ к переменным, уже определенным в интерактивном пространстве имен IPython, используйте команду %run –i, а не
просто %run.

В Jupyter-блокноте можно также использовать магическую функцию %load,
которая импортирует скрипт в ячейку кода:
>>> %load ipython_script_test.py
def f(x, y, z):
return (x + y) / z
a = 5
b = 6
c = 7.5
result = f(a, b, c)

Прерывание выполняемой программы

Нажатие Ctrl+C во время выполнения кода, запущенного с помощью %run
или просто долго работающей программы, приводит к возбуждению исключения KeyboardInterrupt. В этом случае почти все Python-программы немедленно прекращают работу, если только не возникло очень редкое стечение
обстоятельств.
Если Python-код вызвал откомпилированный модуль расширения, то нажатие
Ctrl+C не всегда приводит к немедленному завершению. В таких случаях нужно либо дождаться возврата управления интерпретатору Python, либо (если
случилось что-то ужасное) принудительно снять процесс Python.

Исполнение кода из буфера обмена
В Jupyter-блокноте можно скопировать код в любую ячейку и выполнить
его. В оболочке IPython также можно выполнять код, находящийся в буфере
обмена. Предположим, что в каком-то другом приложении имеется такой код:
x = 5
y = 7
if x > 5:
x += 1
y = 8

Проще всего воспользоваться магическими функциями %paste и %cpaste.
Функция %paste берет текст, находящийся в буфере обмена, и выполняет его
в оболочке как единый блок:
In [17]: %paste
x = 5
y = 7

47

Основы IPython
if x > 5:
x += 1
y = 8
## –– End pasted text ––

Функция %cpaste аналогична, но выводит специальное приглашение для
вставки кода:
In [18]: %cpaste
Pasting code; enter '––' alone on the line to stop or use Ctrl–D.
:x = 5
:y = 7
:if x > 5:
: x += 1
:
: y = 8
:––

При использовании %cpaste вы можете вставить сколько угодно кода, перед
тем как начать его выполнение. Например, %cpaste может пригодиться, если
вы хотите посмотреть на вставленный код до выполнения. Если окажется, что
случайно вставлен не тот код, то из %cpaste можно выйти нажатием Ctrl+C.

Комбинации клавиш
В IPython есть много комбинаций клавиш для навигации по командной
строке (они знакомы пользователям текстового редактора Emacs или оболочки UNIX bash) и взаимодействия с историей команд (см. следующий раздел).
В табл. 2.1 перечислены наиболее употребительные комбинации, а на рис. 2.5
некоторые из них, например перемещение курсора, проиллюстрированы.
Таблица 2.1. Стандартные комбинации клавиш IPython
Комбинация клавиш

Описание

Ctrl+P или ↑

Просматривать историю команд назад в поисках команд, начинающихся
с введенной строки
Просматривать историю команд вперед в поисках команд, начинающихся
с введенной строки
Обратный поиск в истории в духе Readline (частичное соответствие)
Вставить текст из буфера обмена
Прервать исполнение программы
Переместить курсор в начало строки
Переместить курсор в конец строки
Удалить текст от курсора до конца строки
Отбросить весь текст в текущей строке
Переместить курсор на один символ вперед
Переместить курсор на один символ назад
Очистить экран

Ctrl+N или ↓
Ctrl+R
Ctrl+Shift+V
Ctrl+C
Ctrl+A
Ctrl+E
Ctrl+K
Ctrl+U
Ctrl+F
Ctrl+B
Ctrl+L

48

Основы языка Python, IPython и Jupyter-блокноты

Рис. 2.5. Иллюстрация некоторых комбинаций клавиш IPython

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

О магических командах
В IPython есть много специальных команд, называемых магическими, цель
которых – упростить решение типичных задач и облегчить контроль над поведением всей системы IPython. Магической называется команда, которой
предшествует знак процента %. Например, магическая функция %timeit (мы
подробно рассмотрим ее ниже) позволяет замерить время выполнения любого предложения Python, например умножения матриц:
In [20]: a = np.random.randn(100, 100)
In [20]: %timeit np.dot(a, a)
10000 loops, best of 3: 20.9 us per loop

Магические команды можно рассматривать как командные утилиты, исполняемые внутри IPython. У многих из них имеются дополнительные параметры командной строки, список которых можно распечатать с помощью
? (вы ведь так и думали, правда?)1:
In [21]: %debug?
Docstring:
::
%debug [––breakpoint FILE:LINE] [statement [statement ...]]
Активировать интерактивный отладчик.
Эта магическая команда поддерживает два способа активации отладчика.
Первый – активировать отладчик до выполнения кода. Тогда вы сможете
поставить точку прерывания и пошагово выполнять код, начиная с этой
точки. В данном режиме команде передаются подлежащие выполнению
предложения и необязательная точка прерывания.

1

Сообщения выводятся на английском языке, но для удобства читателя переведены. – Прим. перев.

49

Основы IPython
Второй способ – активировать отладчик в постоперационном режиме, для
чего нужно просто выполнить команду %debug без аргументов. В случае
исключения это позволит интерактивно просматривать кадры стека. Отметим,
что в таком случае всегда используется последняя трасса стека, так что
анализировать ее надо сразу после возникновения исключения, поскольку
следующее исключение затрет предыдущее.
Если вы хотите, чтобы IPython автоматически делал это при каждом
исключении, то обратитесь к документации по магической команде %pdb.
позиционные аргументы:
statement
Код, подлежащий выполнению в отладчике. При работе
в режиме ячейки можно опускать.
факультативные аргументы:
––breakpoint , –b
Установить точку прерывания на строке LINE файла
FILE.

Магические функции по умолчанию можно использовать и без знака процента, если только нигде не определена переменная с таким же именем, как
у магической функции. Этот режим называется автомагическим, его можно
включить или выключить с помощью функции %automagic.
Некоторые магические функции ведут себя как функции Python, и их результат можно присваивать переменной:
In [22]: %pwd
Out[22]: '/home/wesm/code/pydata–book'
In [23]: foo = %pwd
In [24]: foo
Out[24]: '/home/wesm/code/pydata–book'

Поскольку к документации по IPython легко можно обратиться из системы,
я рекомендую изучить все имеющиеся специальные команды, набрав %quick­
ref или %magic. В табл. 2.2 описаны некоторые команды, наиболее важные для
продуктивной работы в области интерактивных вычислений и разработки
в среде IPython.
Таблица 2.2. Часто используемые магические команды IPython
Команда

Описание

%quickref
%magic
%debug

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

%hist
%pdb
%paste

50

Основы языка Python, IPython и Jupyter-блокноты

Таблица 2.2 (окончание)
Команда

Описание

Открыть специальное приглашение для ручной вставки Python-кода,
подлежащего выполнению
%reset
Удалить все переменные и прочие имена, определенные в интерактивном
пространстве имен
%page OBJECT
Сформировать красиво отформатированное представление объекта и вывести
его постранично
%run script.py
Выполнить Python-скрипт из IPython
%prun предложение
Выполнить предложение под управлением cProfile и вывести результаты
профилирования
%time предложение
Показать время выполнения одного предложения
%timeit предложение Выполнить предложение несколько раз и усреднить время выполнения. Полезно
для хронометража кода, который выполняется очень быстро
%who, %who_ls, %whos Вывести переменные, определенные в интерактивном пространстве имен,
с различной степенью детализации
%xdel переменная
Удалить переменную и попытаться очистить все ссылки на объект во внутренних
структурах данных IPython
%сpaste

Интеграция с matplotlib
IPython так популярен в сообществе анализа данных отчасти потому, что
он хорошо интегрируется с библиотеками визуализации данных и организации графического интерфейса типа matplotlib. Если вы раньше никогда
не работали с matplotlib, ничего страшного; ниже мы обсудим данную библиотеку во всех подробностях. Магическая функция %matplotlib задает способ ее интеграции с оболочкой IPython или Jupyter-блокнотом. Это важно,
поскольку в противном случае созданные вами графики либо вообще не
будут видны (блокнот), либо захватят управление сеансом до его завершения (оболочка).
В оболочке IPython команда %matplotlib настраивает интеграцию, так чтобы можно было создавать несколько окон графиков, не мешая консольному
сеансу:
In [26]: %matplotlib
Using matplotlib backend: Qt4Agg

В Jupyter команда выглядит немного иначе (рис. 2.6):
In [26]: %matplotlib inline

Основы языка Python

51

Рис. 2.6. Графики matplotlib, встроенные в окно Jupyter

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

Семантика языка
Язык Python отличается удобочитаемостью, простотой и ясностью. Некоторые даже называют написанный на Python код исполняемым псевдокодом.

Отступы вместо скобок
В Python для структурирования кода используются пробелы (или знаки
табуляции), а не фигурные скобки, как во многих других языках, например
R, C++, Java и Perl. Вот как выглядит цикл в алгоритме быстрой сортировки:
for x in array:
if x < pivot:
less.append(x)
else:
greater.append(x)

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

52

Основы языка Python, IPython и Jupyter-блокноты
Я настоятельно рекомендую использовать четыре пробела в качестве величины отступа по умолчанию и настроить редактор так, чтобы он заменял знаки табуляции четырьмя пробелами. Некоторые используют знаки табуляции
непосредственно или задают другое число пробелов – например, довольно
часто встречаются отступы шириной в два пробела. Но четыре пробела – соглашение, принятое подавляющим большинством программистов на Python,
и я советую его придерживаться, если нет каких-то противопоказаний.

Вы уже поняли, что предложения в Python не обязаны завершаться точкой
с запятой. Но ее можно использовать, чтобы отделить друг от друга предложения, находящиеся в одной строчке:
a = 5; b = 6; c = 7

Впрочем, писать несколько предложений в одной строчке не рекомендуется, потому что код из-за этого труднее читается.

Всё является объектом
Важная характеристика языка Python – последовательность его объектной
модели. Все числа, строки, структуры данных, функции, классы, модули и т. д.
в интерпретаторе заключены в «ящики», которые называются объектами Python. С каждым объектом ассоциирован тип (например, строка или функция)
и внутренние данные. На практике это делает язык более гибким, потому что
даже функции можно рассматривать как объекты.

Комментарии
Интерпретатор Python игнорирует текст, которому предшествует знак решетки #. Часто этим пользуются, чтобы включить в код комментарии. Иногда
желательно исключить какие-то блоки кода, не удаляя их. Самое простое
решение – закомментировать такой код:
results = []
for line in file_handle:
# пока оставляем пустые строки
# if len(line) == 0:
# continue
results.append(line.replace('foo', 'bar'))

Комментарий может располагаться также в конце строки с исполняемым
кодом. Иногда это полезно, хотя некоторые программисты предпочитают помещать комментарий в строке, предшествующей исполняемому коду:
print("Дошли до этой строки") # простой отчет о состоянии

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

53

Основы языка Python
result = f(x, y, z)
g()

Почти со всеми объектами в Python ассоциированы функции, которые
имеют доступ к внутреннему состоянию объекта и называются методами.
Синтаксически вызов методов выглядит так:
obj.some_method(x, y, z)

Функции могут принимать позиционные и именованные аргументы:
result = f(a, b, c, d=5, e='foo')

Подробнее об этом ниже.

Переменные и передача аргументов
Присваивание значения переменной (или имени) в Python приводит к созданию ссылки на объект, стоящий в правой части присваивания. Рассмотрим
список целых чисел:
In [8]: a = [1, 2, 3]

Предположим, что мы присвоили значение a новой переменной b:
In [8]: b = a

В некоторых языках такое присваивание приводит к копированию данных
[1, 2, 3]. В Python a и b указывают на один и тот же объект – исходный список [1, 2, 3] (схематически изображено на рис. 2.7). Чтобы убедиться в этом,
добавим в список a еще один элемент и проверим список b:
In [10]: a.append(4)
In [11]: b
Out[11]: [1, 2, 3, 4]

Рис. 2.7. Две ссылки на один объект

Понимать семантику ссылок в Python и знать, когда, как и почему данные
копируются, особенно важно при работе с большими наборами данных.
Операцию присваивания называют также связыванием, потому что мы связываем имя с объектом. Имена переменных, которым присвоено значение, иногда
называют связанными переменными.

Когда объекты передаются функции в качестве аргументов, создаются новые локальные переменные, ссылающиеся на исходные объекты, – копирова-

54

Основы языка Python, IPython и Jupyter-блокноты

ние не производится. Если новый объект связывается с переменной внутри
функции, его изменение никак не отражается на родительской области видимости. Следовательно, функция может модифицировать внутреннее содержимое изменяемого аргумента. Пусть имеется такая функция:
def append_element(some_list, element):
some_list.append(element)

Тогда:
In [27]: data = [1, 2, 3]
In [28]: append_element(data, 4)
In [29]: data
Out[30]: [1, 2, 3, 4]

Динамические ссылки, строгие типы
В отличие от многих компилируемых языков, в частности Java и C++, со
ссылкой на объект в Python не связан никакой тип. Следующий код не приведет к ошибке:
In [12]: a = 5
In [13]: type(a)
Out[13]: int
In [14]: a = 'foo'
In [15]: type(a)
Out[15]: str

Переменные – это имена объектов в некотором пространстве имен; информация о типе хранится в самом объекте. Таким образом, некоторые делают
поспешный вывод, будто Python не является «типизированным языком». Это
не так, рассмотрим следующий пример:
In [16]: '5' + 5
–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
TypeError
Traceback (most recent call last)
in ()
––––> 1 '5' + 5
TypeError: must be str, not int

В некоторых языках, например в Visual Basic, строка '5' могла бы быть
неявно преобразована (приведена) в целое число, и это выражение было бы
вычислено как 10. А бывают и такие языки, например JavaScript, где целое
число 5 преобразуется в строку, после чего производится конкатенация –
'55'. В этом отношении Python считается строго типизированным языком,
т. е. у любого объекта есть конкретный тип (или класс), а неявные преобразования разрешены только в некоторых не вызывающих сомнений случаях,
например:

Powered by TCPDF (www.tcpdf.org)

55

Основы языка Python
In [17]: a = 4.5
In [18]: b = 2
# Форматирование строки, см. ниже
In [19]: print 'a is %s, b is %s' % (type(a), type(b))
a is , b is
In [20]: a / b
Out[20]: 2.25

Знать тип объекта важно, и полезно также уметь писать функции, способные обрабатывать входные параметры различных типов. Проверить, является
ли объект экземпляром определенного типа, позволяет функция isinstance:
In [21]: a = 5
In [22]: isinstance(a, int)
Out[22]: True

Функция isinstance может также принимать кортеж типов. Тогда она проверяет, присутствует ли в кортеже тип переданного объекта:
In [23]: a = 5; b = 4.5
In [24]: isinstance(a, (int, float))
Out[24]: True
In [25]: isinstance(b, (int, float))
Out[25]: True

Атрибуты и методы
Объекты в Python обычно имеют атрибуты – другие объекты, хранящиеся
«внутри» данного, и методы – ассоциированные с объектом функции, имеющие доступ к внутреннему состоянию объекта. Обращение к тем и другим
синтаксически выглядит как obj.attribute_name:
In [1]: a = 'foo'
In [2]: a.
a.capitalize a.format
a.center
a.index
a.count
a.isalnum
a.decode
a.isalpha
a.encode
a.isdigit
a.endswith
a.islower
a.expandtabs a.isspace
a.find
a.istitle

a.isupper
a.join
a.ljust
a.lower
a.lstrip
a.partition
a.replace
a.rfind

a.rindex
a.rjust
a.rpartition
a.rsplit
a.rstrip
a.split
a.splitlines
a.startswith

a.strip
a.swapcase
a.title
a.translate
a.upper
a.zfill

К атрибутам и методам можно обращаться также с помощью функции ge­
tattr:
In [27]: getattr(a, 'split')
Out[27]:

56

Основы языка Python, IPython и Jupyter-блокноты

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

Динамическая типизация
Часто нас интересует не тип объекта, а лишь наличие у него определенных
методов или поведения. Иногда это называют динамической, или «утиной»,
типизацией, имея в виду поговорку «если кто-то ходит как утка и крякает как
утка, то это утка и есть». Например, объект поддерживает итерирование, если
он реализует протокол итератора. Для многих объектов это означает, что
имеется «магический метод» __iter__, хотя есть другой – и лучший – способ
проверки: попробовать воспользоваться функцией iter:
def isiterable(obj):
try:
iter(obj)
return True
except TypeError: # не является итерируемым
return False

Эта функция возвращает True для строк, а также для большинства типов
коллекций в Python:
In [29]: isiterable('a string')
Out[29]: True
In [30]: isiterable([1, 2, 3])
Out[30]: True
In [31]: isiterable(5)
Out[31]: False

Эту функциональность я постоянно использую для написания функций,
принимающих параметры разных типов. Типичный случай – функция, принимающая любую последовательность (список, кортеж, ndarray) или даже
итератор. Можно сначала проверить, является ли объект списком (или массивом NumPy), и если нет, то преобразовать его в таковой:
if not isinstance(x, list) and isiterable(x):
x = list(x)

Импорт
В Python модуль – это просто файл с расширением .py, который содержит
функции и различные определения, в том числе импортированные из других
py-файлов. Пусть имеется следующий модуль:
# some_module.py
PI = 3.14159

Основы языка Python

57

def f(x):
return x + 2
def g(a, b):
return a + b

Если бы мы захотели обратиться к переменным или функциям, определенным в some_module.py, из другого файла в том же каталоге, то должны
были бы написать:
import some_module
result = some_module.f(5)
pi = some_module.PI

Или эквивалентно:
from some_module import f, g, PI
result = g(5, PI)

Ключевое слово as позволяет переименовать импортированные сущности:
import some_module as sm
from some_module import PI as pi, g as gf
r1 = sm.f(pi)
r2 = gf(6, pi)

Бинарные операторы и операции сравнения
Большинство математических операций и операций сравнения именно
таково, как мы и ожидаем:
In [32]: 5 – 7
Out[32]: –2
In [33]: 12 + 21.5
Out[33]: 33.5
In [34]: 5 = b
a is b
a is not b

Поскольку функция list всегда создает новый список Python (т. е. копию
исходного), то имеется уверенность, что c и a – различные объекты. Сравнение с помощью оператора is не то же самое, что с помощью оператора ==,
потому что в данном случае мы получим:
In [40]: a == c
Out[40]: True

Операторы is и is not очень часто употребляются, чтобы проверить, равна
ли некоторая переменная None, потому что существует ровно один экземпляр
None:
In [41]: a = None
In [42]: a is None
Out[42]: True

Изменяемые и неизменяемые объекты
Большинство объектов в Python: списки, словари, массивы NumPy и почти
все определенные пользователем типы (классы) – изменяемо. Это означает,
что объект или значения, которые в нем хранятся, можно модифицировать.
In [43]: a_list = ['foo', 2, [4, 5]]
In [44]: a_list[2] = (3, 4)
In [45]: a_list
Out[45]: ['foo', 2, (3, 4)]

Основы языка Python

59

Но некоторые объекты, например строки и кортежи, неизменяемы:
In [46]: a_tuple = (3, 5, (4, 5))
In [47]: a_tuple[1] = 'four'
–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
TypeError Traceback (most recent call last)
in ()
––––> 1 a_tuple[1] = 'four'
TypeError: 'tuple' object does not support item assignment

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

Скалярные типы
В Python есть небольшой набор встроенных типов для работы с числовыми
данными, строками, булевыми значениями (True и False), датами и временем.
Эти типы «с одним значением» иногда называются скалярными, а мы будем
называть их просто скалярами. Перечень основных скалярных типов приведен в табл. 2.4. Работа с датами и временем будет рассмотрена отдельно,
потому что эти типы определены в стандартном модуле datetime.
Таблица 2.4. Стандартные скалярные типы в Python
Тип

Описание

None
str
bytes

Значение «null» в Python (существует только один экземпляр объекта None)
Тип строки. Может содержать любые символы Unicode
Неинтерпретируемые ASCII-байты (или символы Unicode, закодированные
последовательностями байтов)
Число с плавающей точкой двойной точности (64-разрядное). Отдельный тип double
не предусмотрен
Значение True или False
Целое со знаком, максимальное значение зависит от платформы

float
bool
int

Числовые типы

Основные числовые типы в Python – int и float. Тип int способен представить сколь угодно большое целое число.
In [48]: ival = 17239871
In [49]: ival ** 6
Out[49]: 26254519291092456596965462913230729701102721

Числа с плавающей точкой представляются типом Python float, который
реализован в виде значения двойной точности (64-разрядного). Такие числа
можно записывать и в научной нотации:

60

Основы языка Python, IPython и Jupyter-блокноты

In [50]: fval = 7.243
In [51]: fval2 = 6.78e–5

Деление целых чисел, результатом которого не является целое число, всегда дает число с плавающей точкой:
In [52]: 3 / 2
Out[52]: 1.5

Для выполнения целочисленного деления в духе языка C (когда дробная
часть результата отбрасывается) служит оператор деления с отбрасыванием
//:
In [53]: 3 // 2
Out[53]: 1

Строки
Многие любят Python за его мощные и гибкие средства работы со строками.
Строковый литерал записывается в одиночных (') или двойных (") кавычках:
a = 'one way of writing a string'
b = "another way"

Для записи многострочных строк, содержащих разрывы, используются
тройные кавычки – ''' или """:
c = """
Это длинная строка,
занимающая несколько строчек
"""

Возможно, вы удивитесь, узнав, что строка c в действительности содержит
четыре строчки текста: разрывы строки после """ и после слова строчек являются частью строки. Для подсчета количества знаков новой строки можно
воспользоваться методом count объекта c:
In [55]: c.count('\n')
Out[55]: 3

Строки в Python неизменяемы, при любой модификации создается новая
строка:
In [56]: a = 'this is a string'
In [57]: a[10] = 'f'
–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
TypeError Traceback (most recent call last)
in ()
––––> 1 a[10] = 'f'
TypeError: 'str' object does not support item assignment
In [58]: b = a.replace('string', 'longer string')

Основы языка Python

61

In [59]: b
Out[59]: 'this is a longer string'

После этой операции переменная a не изменилась:
In [60]: a
Out[60]: 'this is a string'

Многие объекты Python можно преобразовать в строку с помощью функции str:
In [61]: a = 5.6
In [62]: s = str(a)
In [63]: print(s)
5.6

Строки – это последовательности символов Unicode и потому могут рассматриваться как любые другие последовательности, например списки или
кортежи (которые будут подробно рассмотрены в следующей главе):
In [64]: s = 'python'
In [65]: list(s)
Out[65]: ['p', 'y', 't', 'h', 'o', 'n']
In [66]: s[:3]
Out[66]: 'pyt'

Синтаксическая конструкция s[:3] называется срезом и реализована для
многих типов последовательностей в Python. Позже мы подробно объясним,
как она работает, поскольку будем часто использовать ее в книге.
Знак обратной косой черты \ играет роль управляющего символа, он предшествует специальным символам, например знаку новой строки или символам Unicode. Чтобы записать строковый литерал, содержащий знак обратной
косой черты, этот знак необходимо повторить дважды:
In [67]: s = '12\\34'
In [68]: print s
12\34

Если строка содержит много знаков обратной косой черты и ни одного
специального символа, то при такой записи она становится совершенно неразборчивой. По счастью, есть другой способ: поставить перед начальной
кавычкой букву r, которая означает, что все символы должны интерпретироваться буквально:
In [69]: s = r'this\has\no\special\characters'
In [70]: s
Out[70]: 'this\\has\\no\\special\\characters'

Буква r здесь сокращение от raw.

62

Основы языка Python, IPython и Jupyter-блокноты

Сложение двух строк означает конкатенацию, при этом создается новая
строка:
In [71]: a = 'this is the first half '
In [72]: b = 'and this is the second half'
In [73]: a + b
Out[73]: 'this is the first half and this is the second half'

Еще одна важная тема – форматирование строк. С появлением Python 3 диапазон возможностей в этом плане расширился, здесь я лишь вкратце опишу
один из основных интерфейсов. У строковых объектов имеется метод format,
который можно использовать для подстановки в строку отформатированных
аргументов, в результате чего рождается новая строка:
In [74]: template = '{0:.2f} {1:s} are worth US${2:d}'

Здесь:
• {0:.2f} означает, что первый аргумент нужно отформатировать как число с плавающей точкой с двумя знаками после точки;
• {1:s} означает, что второй аргумент нужно отформатировать как строку;
• {2:d} означает, что третий аргумент нужно отформатировать как целое
число.
Для подстановки значений вместо спецификаторов формата мы передаем
методу format последовательность аргументов:
In [75]: template % (4.5560, 'Argentine Pesos', 1)
Out[75]: '4.56 Argentine Pesos are worth US$1'

Форматирование строк – обширная тема. Существует несколько методов
и многочисленные параметры и ухищрения, призванные контролировать,
как именно форматируются значения, подставляемые в результирующую
строку. Подробные сведения можно найти в официальной документации по
Python (https://docs.python.org/3.6/library/string.html).
В главе 8 обсуждаются общие вопросы работы со строками в контексте
анализа данных.

Байты и Unicode
В современном Python (т. е. Python 3.0 и выше) Unicode стал полноправным
типом строки, обеспечивающим единообразную обработку любых текстов,
а не только в кодировке ASCII. В прежних версиях строка рассматривалась как
совокупность байтов без явного предположения о кодировке Unicode. Строку
можно было преобразовать в Unicode, если была известна кодировка символов. Рассмотрим пример:
In [76]: val = "espańol"
In [77]: val
Out[77]: 'espańol'

Основы языка Python

63

Мы можем преобразовать эту Unicode-строку в последовательность байтов
в кодировке UTF-8, вызвав метод encode:
In [78]: val_utf8 = val.encode('utf–8')
In [79]: val_utf8
Out[79]: b'espa\xc3\xb1ol'
In [80]: type(val_utf8)
Out[80]: bytes

В предположении, что известна Unicode-кодировка объекта bytes, мы можем обратить эту операцию методом decode:
In [81]: val_utf8.decode('utf–8')
Out[81]: 'espańol'

Хотя в наши дни обычно используют кодировку UTF-8 для любых текстов,
в силу исторических причин иногда можно встретить данные и в других кодировках:
In [82]: val.encode('latin1')
Out[82]: b'espa\xf1ol'
In [83]: val.encode('utf–16')
Out[83]: b'\xff\xfee\x00s\x00p\x00a\x00\xf1\x00o\x00l\x00'
In [84]: val.encode('utf–16le')
Out[84]: b'e\x00s\x00p\x00a\x00\xf1\x00o\x00l\x00'

Чаще всего объекты типа bytes встречаются при работе с файлами, когда
неявно перекодировать все данные в Unicode-строки нежелательно.
Несмотря на то что нужда в этом возникает редко, при необходимости
можно определить байтовые литералы, добавив к строке префикс b:
In [85]: bytes_val = b'this is bytes'
In [86]: bytes_val
Out[86]: b'this is bytes'
In [87]: decoded = bytes_val.decode('utf8')
In [88]: decoded # теперь это объект типа str (Unicode)
Out[88]: 'this is bytes'

Булевы значения
Два булевых значения записываются в Python как True и False. Результатом
сравнения и вычисления условных выражений является True или False. Булевы
значения объединяются с помощью ключевых слов and и or:
In [89]: True and True
Out[89]: True
In [90]: False or True
Out[90]: True

64

Основы языка Python, IPython и Jupyter-блокноты

Приведение типов
Типы str, bool, int и float являются также функциями, которые можно использовать для приведения значения к соответствующему типу:
In [91]: s = '3.14159'
In [92]: fval = float(s)
In [93]: type(fval)
Out[93]: float
In [94]: int(fval)
Out[94]: 3
In [95]: bool(fval)
Out[95]: True
In [96]: bool(0)
Out[96]: False

Тип None
None – это тип значения null в Python. Если функция явно не возвращает
никакого значения, то неявно она возвращает None.
In [97]: a = None
In [98]: a is None
Out[98]: True
In [99]: b = 5
In [100]: b is not None
Out[100]: True

None также часто применяется в качестве значения по умолчанию для необязательных аргументов функции:
def add_and_maybe_multiply(a, b, c=None):
result = a + b
if c is not None:
result = result * c
return result

Хотя это вопрос чисто технический, стоит иметь в виду, что None не зарезервированное слово языка, а единственный экземпляр класса NoneType.
In [101]: type(None)
Out[101]: NoneType

Дата и время
Стандартный модуль Python datetime предоставляет типы datetime, date
и time. Тип datetime, как нетрудно сообразить, объединяет информацию, хранящуюся в date и time. Именно он чаще всего и используется:

65

Основы языка Python
In [102]: from datetime import datetime, date, time
In [103]: dt = datetime(2011, 10, 29, 20, 30, 21)
In [104]: dt.day
Out[104]: 29
In [105]: dt.minute
Out[105]: 30

Имея экземпляр datetime, можно получить из него объекты date и time путем
вызова одноименных методов:
In [106]: dt.date()
Out[106]: datetime.date(2011, 10, 29)
In [107]: dt.time()
Out[107]: datetime.time(20, 30, 21)

Метод strftime форматирует объект datetime, представляя его в виде строки:
In [108]: dt.strftime('%m/%d/%Y %H:%M')
Out[108]: '10/29/2011 20:30'

Чтобы разобрать строку и представить ее в виде объекта datetime, нужно
вызвать функцию strptime:
In [109]: datetime.strptime('20091031', '%Y%m%d')
Out[109]: datetime.datetime(2009, 10, 31, 0, 0)

В табл. 2.5 приведен полный перечень спецификаций формата.
Таблица 2.5. Спецификации формата даты в классе datetime
(совместима со стандартом ISO C89)
Спецификатор

Описание

%Y
%y
%m
%d
%H
%I
%M
%S
%w
%U

Год с четырьмя цифрами
Год с двумя цифрами
Номер месяца с двумя цифрами [01, 12]
Номер дня с двумя цифрами [01, 31]
Час (в 24-часовом формате) [00, 23]
Час (в 12-часовом формате) [01, 12]
Минута с двумя цифрами [01, 59]
Секунда [00, 61] (секунды 60 и 61 високосные)
День недели в виде целого числа [0 (воскресенье), 6]
Номер недели в году [00, 53]. Первым днем недели считается воскресенье,
а дни, предшествующие первому воскресенью, относятся к неделе 0
Номер недели в году [00, 53]. Первым днем недели считается понедельник,
а дни, предшествующие первому понедельнику, относятся к неделе 0
Часовой пояс UTC в виде +HHMM или -HHMM; пустая строка, если часовой
пояс не учитывается
Сокращение для %Y–%m–%d, например 2012–4–18
Сокращение для %m/%d/%y, например 04/18/2012

%W
%z
%F
%D

66

Основы языка Python, IPython и Jupyter-блокноты

При агрегировании или еще какой-то группировке временных рядов иногда бывает полезно заменить некоторые компоненты даты или времени, например обнулить минуты и секунды, создав новый объект:
In [110]: dt.replace(minute=0, second=0)
Out[110]: datetime.datetime(2011, 10, 29, 20, 0)

Поскольку тип datetime.datetime неизменяемый, эти и другие подобные методы порождают новые объекты.
Вычитание объектов datetime дает объект типа datetime.timedelta:
In [111]: dt2 = datetime(2011, 11, 15, 22, 30)
In [112]: delta = dt2 – dt
In [113]: delta
Out[113]: datetime.timedelta(17, 7179)
In [114]: type(delta)
Out[114]: datetime.timedelta

Сложение объектов timedelta и datetime дает новый объект datetime, отстоящий от исходного на указанный промежуток времени:
In [115]: dt
Out[115]: datetime.datetime(2011, 10, 29, 20, 30, 21)
In [116]: dt + delta
Out[116]: datetime.datetime(2011, 11, 15, 22, 30)

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

if, elif, else
Предложение if – одно из самых хорошо известных предложений управления потоком выполнения. Оно вычисляет условие и, если получилось True,
исполняет код в следующем далее блоке:
if x < 0:
print 'Отрицательно'

После предложения if может находиться один или несколько блоков elif
и блок else, который выполняется, если все остальные условия оказались равны False:
if x < 0:
print 'Отрицательно'
elif x == 0:
print 'Равно нулю'

Основы языка Python

67

elif 0 < x < 5:
print 'Положительно, но меньше 5'
else:
print 'Положительно и больше или равно 5'

Если какое-то условие равно True, последующие блоки elif и else даже не
рассматриваются. В случае составного условия, в котором есть операторы and
или or, частичные условия вычисляются слева направо с закорачиванием:
In [117]: a = 5; b = 7
In [118]: c = 8; d = 4
In [119]: if a < b or c > d:
.....:
print 'Сделано'
Сделано

В этом примере условие c > d не вычисляется, потому что уже первое сравнение a < b равно True.
Можно также строить цепочки сравнений:
In [120]: 4 > 3 > 2 > 1
Out[120]: True

Циклы for
Циклы for предназначены для обхода коллекции (например, списка или
кортежа) или итератора. Стандартный синтаксис выглядит так:
for value in collection:
# что–то сделать с value

Ключевое слово continue позволяет сразу перейти к следующей итерации
цикла, не доходя до конца блока. Рассмотрим следующий код, который суммирует целые числа из списка, пропуская значения None:
sequence = [1, 2, None, 4, None, 5]
total = 0
for value in sequence:
if value is None:
continue
total += value

Ключевое слово break осуществляет выход из самого внутреннего цикла,
объемлющие циклы продолжают работать:
In [121]: for i in range(4):
.....:
for j in range(4):
.....:
if j > i:
.....:
break
.....:
print((i, j))
.....:
(0, 0)
(1, 0)

68
(1,
(2,
(2,
(2,
(3,
(3,
(3,
(3,

Основы языка Python, IPython и Jupyter-блокноты

1)
0)
1)
2)
0)
1)
2)
3)

Как мы вскоре увидим, если элементы коллекции или итераторы являются
последовательностями (например, кортежем или списком), то их можно распаковать в переменные, воспользовавшись циклом for:
for a, b, c in iterator:
# что–то сделать

Циклы while
Цикл while состоит из условия и блока кода, который выполняется до тех
пор, пока условие не окажется равным False или не произойдет выход из
цикла в результате предложения break:
x = 256
total = 0
while x > 0:
if total > 500:
break
total += x
x = x // 2

Ключевое слово pass
Предложение pass Python является пустышкой. Его можно использовать
в тех блоках, где не требуется никакого действия; нужно оно только потому,
что в Python ограничителем блока выступает пробел:
if x < 0:
print 'отрицательно!'
elif x == 0:
# TODO: сделать тут что–нибудь разумное
pass
else:
print 'положительно!'

Функция range
Функция range порождает список равноотстоящих целых чисел:
In [122]: range(10)
Out[122]: range(0, 10)
In [123]: list(range(10))
Out[123]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Основы языка Python

69

Можно задать начало и конец диапазона и шаг (который может быть отрицательным):
In [124]: list(range(0, 20, 2))
Out[124]: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
In [125]: list(range(5, 0, –1))
Out[125]: [5, 4, 3, 2, 1]

Как видите, конечная точка в порождаемый диапазон range не включается.
Типичное применение range – обход последовательности по индексу:
seq = [1, 2, 3, 4]
for i in range(len(seq)):
val = seq[i]

Для хранения всех целых чисел, сгенерированных функцией range, можно
использовать список list, но часто задачу проще решить с помощью итератора по умолчанию. Следующий код вычисляет сумму тех чисел от 0 до 9999,
которые кратны 3 или 5:
sum = 0
for i in xrange(10000):
# % — оператор деления по модулю
if x % 3 == 0 or x % 5 == 0:
sum += i

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

Тернарные выражения
Тернарное выражение в Python позволяет записать блок if–else, порождающий единственное значение, в виде однострочного выражения. Синтаксически это выглядит так:
value = true-expr if condition else false-expr

Здесь true-expr и false-expr могут быть произвольными выражениями Python. Результат такой же, как при выполнении более пространной конструкции:
if condition:
value = true-expr
else:
value = false-expr

Вот более конкретный пример:
In [126]: x = 5
In [127]: 'Неотрицательно' if x >= 0 else 'Отрицательно'
Out[127]: 'Неотрицательно'

70

Основы языка Python, IPython и Jupyter-блокноты

Как и в случае блоков if–else, вычисляется только одно из двух подвыражений. То есть в обеих частях («if» и «else») тернарного выражения могут
находиться сложные выражения, но вычисляется только одно из них – в «истинной» ветви.
И хотя возникает искушение использовать тернарные выражения всегда,
чтобы сократить длину программы, нужно понимать, что если подвыражения
очень сложны, то таким образом вы приносите в жертву понятность кода.

Глава 3. Встроенные структуры
данных, функции и файлы
В этой главе мы обсудим встроенные в язык Python средства, которыми постоянно будем пользоваться в книге. Библиотеки типа pandas и NumPy добавляют функциональность для работы с большими наборами данных, но опираются они на уже имеющиеся в Python инструменты манипуляции данными.
Мы начнем с базовых структур данных: кортежей, списков, словарей
и множеств, затем обсудим, как создавать на Python собственные функции,
допускающие повторное использование, и наконец, рассмотрим механизмы
работы с файлами и взаимодействия с локальным диском.

3.1. Структуры данных и последовательности
Структуры данных в Python просты, но эффективны. Чтобы стать хорошим
программистом на Python, необходимо овладеть ими в совершенстве.

Кортеж
Кортеж – это одномерная неизменяемая последовательность объектов Python фиксированной длины. Проще всего создать кортеж, записав последовательность значений через запятую:
In [1]: tup = 4, 5, 6
In [2]: tup
Out[2]: (4, 5, 6)

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

72

Встроенные структуры данных, функции и файлы

In [3]: nested_tup = (4, 5, 6), (7, 8)
In [4]: nested_tup
Out[4]: ((4, 5, 6), (7, 8))

Любую последовательность или итератор можно преобразовать в кортеж
с помощью функции tuple:
In [5]: tuple([4, 0, 2])
Out[5]: (4, 0, 2)
In [5]: tup = tuple('string')
In [5]: tup
Out[5]: ('s', 't', 'r', 'i', 'n', 'g')

К элементам кортежа можно обращаться с помощью квадратных скобок [],
как и для большинства других типов последовательностей. Как и в C, C++,Java
и многих других языках, нумерация элементов последовательностей в Python
начинается с нуля:
In [6]: tup[0]
Out[6]: 's'

Хотя объекты, хранящиеся в кортеже, могут быть изменяемыми, сам кортеж после создания изменить (т. е. записать что-то другое в существующую
позицию) невозможно:
In [9]: tup = tuple(['foo', [1, 2], True])
In [10]: tup[2] = False
–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
TypeError
Traceback (most recent call last)
in ()
––––> 1 tup[2] = False
TypeError: 'tuple' object does not support item assignment

Если какой-то объект кортежа изменяемый, например является списком,
то его можно модифицировать на месте:
In [11]: tup[1].append(3)
In [12]: tup
Out[12]: ('foo', [1, 2, 3], True)

Кортежи можно конкатенировать с помощью оператора +, получая в результате более длинный кортеж:
In [13]: (4, None, 'foo') + (6, 0) + ('bar',)
Out[13]: (4, None, 'foo', 6, 0, 'bar')

Умножение кортежа на целое число, как и в случае списка, приводит к конкатенации нескольких копий кортежа.

Структуры данных и последовательности

73

In [14]: ('foo', 'bar') * 4
Out[14]: ('foo', 'bar', 'foo', 'bar', 'foo', 'bar', 'foo', 'bar')

Отметим, что копируются не сами объекты, а только ссылки на них.

Распаковка кортежей
При попытке присвоить значение похожему на кортеж выражению, состоящему из нескольких переменных, интерпретатор пытается распаковать
значение в правой части оператора присваивания:
In [15]: tup = (4, 5, 6)
In [16]: a, b, c = tup
In [17]: b
Out[17]: 5

Распаковать можно даже вложенный кортеж:
In [18]: tup = 4, 5, (6, 7)
In [19]: a, b, (c, d) = tup
In [20]: d
Out[20]: 7

Эта функциональность позволяет без труда решить задачу обмена значений переменных, которая во многих других языках решается так:
tmp = a
a = b
b = tmp

Однако в Python обменять значения можно и так:
In [21]: a, b = 1, 2
In [22]: a
Out[22]: 1
In [23]: b
Out[23]: 2
In [24]: b, a = a, b
In [25]: a
Out[25]: 2
In [26]: b
Out[26]: 1

Одно из распространенных применений распаковки переменных – обход
последовательности кортежей или списков:
In [27]: seq = [(1, 2, 3), (4, 5, 6), (7, 8, 9)]
In [28]: for a, b, c in seq:

74

Встроенные структуры данных, функции и файлы

....:
print('a={0}, b={1}, c={2}'.format(a, b, c))
a=1, b=2, c=3
a=4, b=5, c=6
a=7, b=8, c=9

Другое применение – возврат нескольких значений из функции. Подробнее
об этом ниже.
Недавно в Python были включены дополнительные средства распаковки, на
случай когда требуется «отщепить» несколько элементов из начала кортежа.
Для этого применяется специальный синтаксис *rest, используемый также
в сигнатурах функций, чтобы обозначить сколь угодно длинный список позиционных аргументов:
In [29]: values = 1, 2, 3, 4, 5
In [30]: a, b, *rest = values
In [31]: a, b
Out[31]: (1, 2)
In [32]: rest
Out[32]: [3, 4, 5]

Часть rest иногда требуется отбросить; в самом имени rest нет ничего
специального, оно может быть любым. По соглашению многие программисты используют для обозначения ненужных переменных знак подчеркивания (_):
In [33]: a, b, *_ = values

Методы кортежа
Поскольку ни размер, ни содержимое кортежа нельзя модифицировать,
методов экземпляра у него совсем немного. Пожалуй, наиболее полезен метод count (имеется также у списков), возвращающий количество вхождений
значения:
In [34]: a = (1, 2, 2, 2, 3, 4, 2)
In [35]: a.count(2)
Out[35]: 4

Список
В отличие от кортежей, списки имеют переменную длину, а их содержимое
можно модифицировать. Список определяется с помощью квадратных скобок
[] или конструктора типа list:
In [36]: a_list = [2, 3, 7, None]
In [37]: tup = ('foo', 'bar', 'baz')
In [38]: b_list = list(tup)

Структуры данных и последовательности

75

In [39]: b_list
Out[39]: ['foo', 'bar', 'baz']
In [40]: b_list[1] = 'peekaboo'
In [41]: b_list
Out[41]: ['foo', 'peekaboo', 'baz']

Семантически списки и кортежи схожи, поскольку те и другие являются
одномерными последовательностями объектов. Следовательно, во многих
функциях они взаимозаменяемы.
Функция list часто используется при обработке данных, чтобы материализовать итератор или генераторное выражение:
In [42]: gen = range(10)
In [43]: gen
Out[43]: range(0, 10)
In [44]: list(gen)
Out[44]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Добавление и удаление элементов
Для добавления элемента в конец списка служит метод append:
In [45]: b_list.append('dwarf')
In [46]: b_list
Out[46]: ['foo', 'peekaboo', 'baz', 'dwarf']

Метод insert позволяет вставить элемент в указанную позицию списка:
In [47]: b_list.insert(1, 'red')
In [48]: b_list
Out[48]: ['foo', 'red', 'peekaboo', 'baz', 'dwarf']

Индекс позиции вставки должен принадлежать диапазону от 0 до длины
списка включительно.
Метод insert вычислительно сложнее, чем append, так как, чтобы освободить
место для нового элемента, приходится сдвигать ссылки на элементы, следующие за ним. Если необходимо вставлять элементы как в начало, так и в конец
последовательности, то лучше использовать объект collections.deque, специально предназначенный для этой цели.

Операцией, обратной к insert, является pop, она удаляет из списка элемент,
находившийся в указанной позиции, и возвращает его:
In [49]: b_list.pop(2)
Out[49]: 'peekaboo'
In [50]: b_list
Out[50]: ['foo', 'red', 'baz', 'dwarf']

76

Встроенные структуры данных, функции и файлы

Элементы можно удалять также по значению методом remove, который находит и удаляет из списка первый элемент с указанным значением:
In [51]: b_list.append('foo')
In [52]: b_list
Out[52]: ['foo', 'red', 'baz', 'dwarf', 'foo']
In [53]: b_list.remove('foo')
In [54]: b_list
Out[54]: ['red', 'baz', 'dwarf', 'foo']

Если снижение производительности из-за использования методов append
и remove не составляет проблемы, то список Python вполне можно использовать в качестве структуры данных «мультимножество».
Чтобы проверить, содержит ли список некоторое значение, используется
ключевое слово in:
In [55]: 'dwarf' in b_list
Out[55]: True

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

Конкатенация и комбинирование списков
Как и в случае с кортежами, операция сложения конкатенирует списки:
In [57]: [4, None, 'foo'] + [7, 8, (2, 3)]
Out[57]: [4, None, 'foo', 7, 8, (2, 3)]

Если уже имеется список, то добавить в его конец несколько элементов
позволяет метод extend:
In [58]: x = [4, None, 'foo']
In [59]: x.extend([7, 8, (2, 3)])
In [60]: x
Out[60]: [4, None, 'foo', 7, 8, (2, 3)]

Отметим, что конкатенация – сравнительно дорогая операция, потому что
нужно создать новый список и скопировать в него все объекты. Обычно предпочтительнее использовать extend для добавления элементов в имеющийся
список, особенно если строится длинный перечень. Таким образом,
everything = []
for chunk in list_of_lists:
everything.extend(chunk)

Структуры данных и последовательности

77

быстрее, чем эквивалентная конкатенация:
everything = []
for chunk in list_of_lists:
everything = everything + chunk

Сортировка
Список можно отсортировать на месте (без создания нового объекта), вызвав его метод sort:
In [61]: a = [7, 2, 5, 1, 3]
In [62]: a.sort()
In [63]: a
Out[63]: [1, 2, 3, 5, 7]

У метода sort есть несколько удобных возможностей. Одна из них – возможность передать ключ сортировки, т. е. функцию, порождающую значение,
по которому должны сортироваться объекты. Например, вот как можно отсортировать коллекцию строк по длине:
In [64]: b = ['saw', 'small', 'He', 'foxes', 'six']
In [65]: b.sort(key=len)
In [66]: b
Out[66]: ['He', 'saw', 'six', 'small', 'foxes']

Вскоре мы познакомимся с функцией sorted, которая умеет порождать отсортированную копию любой последовательности.

Двоичный поиск и поддержание списка в отсортированном состоянии
В стандартном модуле bisect реализованы операции двоичного поиска
и вставки в отсортированный список. Метод bisect.bisect находит позицию,
в которую следует вставить новый элемент, чтобы список остался отсортированным, а метод bisect.insort производит вставку в эту позицию:
In [67]: import bisect
In [68]: c = [1, 2, 2, 2, 3, 4, 7]
In [69]: bisect.bisect(c, 2)
Out[69]: 4
In [70]: bisect.bisect(c, 5)
Out[70]: 6
In [71]: bisect.insort(c, 6)
In [72]: c
Out[72]: [1, 2, 2, 2, 3, 4, 6, 7]

78

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

Вырезание
Из большинства последовательностей можно вырезать участки с помощью
нотации, которая в простейшей форме сводится к передаче пары start:stop
оператору доступа по индексу []:
In [73]: seq = [7, 2, 3, 7, 5, 6, 0, 1]
In [74]: seq[1:5]
Out[74]: [2, 3, 7, 5]

Срезу также можно присваивать последовательность:
In [75]: seq[3:4] = [6, 3]
In [76]: seq
Out[76]: [7, 2, 3, 6, 3, 5, 6, 0, 1]

Элемент с индексом start включается в срез, элемент с индексом stop не
включается, поэтому количество элементов в результате равно stop – start.
Любой член пары, как start, так и stop, можно опустить, тогда по умолчанию подразумевается начало и конец последовательности соответственно:
In [77]: seq[:5]
Out[77]: [7, 2, 3, 6, 3]
In [78]: seq[3:]
Out[78]: [6, 3, 5, 6, 0, 1]

Если индекс в срезе отрицателен, то он отсчитывается от конца последовательности:
In [79]: seq[–4:]
Out[79]: [5, 6, 0, 1]
In [80]: seq[–6:–2]
Out[80]: [6, 3, 5, 6]

К семантике вырезания надо привыкнуть, особенно если вы раньше работали с R или MATLAB. На рис. 3.1 показано, как происходит вырезание при
положительном и отрицательном индексах. В левом верхнем углу каждой
ячейки проставлены индексы, чтобы было проще понять, где начинается
и заканчивается срез при положительных и отрицательных индексах.
Допускается и вторая запятая, после которой можно указать шаг, например
взять каждый второй элемент:
In [81]: seq[::2]
Out[81]: [7, 3, 3, 6, 1]

Структуры данных и последовательности

79

Рис. 3.1. Иллюстрация соглашений о вырезании в Python

Если задать шаг –1, то список или кортеж будут инвертированы:
In [82]: seq[::–1]
Out[82]: [1, 0, 6, 5, 3, 6, 3, 2, 7]

Встроенные функции последовательностей
У последовательностей в Python есть несколько полезных функций, которые следует знать и применять при любой возможности.

enumerate
При обходе последовательности часто бывает необходимо следить за индексом текущего элемента. «Ручками» это можно сделать так:
i = 0
for value in collection:
# что–то сделать с value
i += 1

Но, поскольку эта задача встречается очень часто, в Python имеется встроенная функция enumerate, которая возвращает последовательность кортежей
(i, value):
for i, value in enumerate(collection):
# что–то сделать с value

Функция enumerate нередко используется для построения словаря, отображающего значения в последовательности (предполагаемые уникальными) на
их позиции:
In [83]: some_list = ['foo', 'bar', 'baz']
In [84]: mapping = {}
In [85]: for i, v in enumerate(some_list):
....:
mapping[v] = i

80

Встроенные структуры данных, функции и файлы

In [86]: mapping
Out[86]: {'bar': 1, 'baz': 2, 'foo': 0}

sorted
Функция sorted возвращает новый отсортированный список, построенный
из элементов произвольной последовательности:
In [87]: sorted([7, 1, 2, 6, 0, 3, 2])
Out[87]: [0, 1, 2, 2, 3, 6, 7]
In [88]: sorted('horse race')
Out[88]: [' ', 'a', 'c', 'e', 'e', 'h', 'o', 'r', 'r', 's']

Функция sorted принимает те же аргументы, что метод списков sort.

zip
Функция zip «сшивает» элементы нескольких списков, кортежей или других последовательностей в пары, создавая список кортежей:
In [89]: seq1 = ['foo', 'bar', 'baz']
In [90]: seq2 = ['one', 'two', 'three']
In [91]: zipped = zip(seq1, seq2)
In [92]: list(zipped)
Out[92]: [('foo', 'one'), ('bar', 'two'), ('baz', 'three')]

Функция zip принимает любое число аргументов, а количество порождаемых ей кортежей определяется длиной самой короткой последовательности:
In [93]: seq3 = [False, True]
In [94]: zip(seq1, seq2, seq3)
Out[94]: [('foo', 'one', False), ('bar', 'two', True)]

Очень распространенное применение zip – одновременный обход нескольких последовательностей, возможно, в сочетании с enumerate:
In [95]: for i, (a, b) in enumerate(zip(seq1, seq2)):
....:
print('{0}: {1}, {2}'.format(i, a, b))
....:
0: foo, one
1: bar, two
2: baz, three

Если имеется «сшитая» последовательность, то zip можно использовать,
чтобы «распороть» ее. Это можно также представить себе как преобразование
списка строк в список столбцов. Синтаксис, несколько причудливый, выглядит следующим образом:
In [96]: pitchers = [('Nolan', 'Ryan'), ('Roger', 'Clemens'),
.....:
('Schilling', 'Curt')]

Структуры данных и последовательности

81

In [97]: first_names, last_names = zip(*pitchers)
In [98]: first_names
Out[98]: ('Nolan', 'Roger', 'Schilling')
In [99]: last_names
Out[99]: ('Ryan', 'Clemens', 'Curt')

reversed
Функция reversed перебирает элементы последовательности в обратном
порядке:
In [100]: list(reversed(range(10)))
Out[100]: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

Имейте в виду, что reversed – это генератор (данное понятие мы рассмотрим ниже), следовательно, он не создает инвертированную последовательность, если только не будет материализован (например, с помощью функции
list или в цикле for).

Словарь
Словарь, пожалуй, является самой важной из встроенных в Python структур данных. Его также называют хешем, отображением или ассоциативным
массивом. Он представляет собой коллекцию пар ключ–значение переменного размера, в которой и ключ, и значение – объекты Python. Создать словарь можно с помощью фигурных скобок {}, отделяя ключи от значений
двоеточием:
In [101]: empty_dict = {}
In [102]: d1 = {'a' : 'some value', 'b' : [1, 2, 3, 4]}
In [103]: d1
Out[103]: {'a': 'some value', 'b': [1, 2, 3, 4]}

Для доступа к элементам, вставки и присваивания применяется такой же
синтаксис, как в случае списка или кортежа:
In [104]: d1[7] = 'an integer'
In [105]: d1
Out[105]: {7: 'an integer', 'a': 'some value', 'b': [1, 2, 3, 4]}
In [106]: d1['b']
Out[106]: [1, 2, 3, 4]

Проверка наличия ключа в словаре тоже производится как для кортежа
или списка:
In [107]: 'b' in d1
Out[107]: True

82

Встроенные структуры данных, функции и файлы

Для удаления ключа можно использовать либо ключевое слово del, либо
метод pop (который не только удаляет ключ, но и возвращает ассоциированное с ним значение):
In [108]: d1[5] = 'some value'
In [109]: d1
Out[109]:
{'a': 'some value',
'b': [1, 2, 3, 4],
7: 'an integer',
5: 'some value'}
In [110]: d1['dummy'] = 'another value'
In [111]: d1
Out[111]:
{'a': 'some value',
'b': [1, 2, 3, 4],
7: 'an integer',
5: 'some value',
'dummy': 'another value'}
In [112]: del d1[5]
In [113]: d1
Out[113]:
{'a': 'some value',
'b': [1, 2, 3, 4],
7: 'an integer',
'dummy': 'another value'}
In [114]: ret = d1.pop('dummy')
In [115]: ret
Out[115]: 'another value'
In [116]: d1
Out[116]: {'a': 'some value', 'b': [1, 2, 3, 4], 7: 'an integer'}

Методы keys и values возвращают соответственно список ключей и список
значений. Хотя точный порядок пар ключ–значение не определен, эти методы
возвращают ключи и значения в одном и том же порядке:
In [117]: list(d1.keys())
Out[117]: ['a', 'b', 7]
In [118]: list(d1.values())
Out[118]: ['some value', [1, 2, 3, 4], 'an integer']

Два словаря можно объединить методом update:
In [119]: d1.update({'b' : 'foo', 'c' : 12})
In [120]: d1
Out[120]: {7: 'an integer', 'a': 'some value', 'b': 'foo', 'c': 12}

Структуры данных и последовательности

83

Метод update модифицирует словарь на месте, т. е. старые значения существующих ключей, переданных update, стираются.

Создание словаря из последовательностей
Нередко имеются две последовательности, которые естественно рассматривать как ключи и соответствующие им значения, а значит, требуется построить из них словарь. Первая попытка могла бы выглядеть так:
mapping = {}
for key, value in zip(key_list, value_list):
mapping[key] = value

Поскольку словарь – это, по существу, коллекция 2-кортежей, функция dict
принимает список 2-кортежей:
In [121]: mapping = dict(zip(range(5), reversed(range(5))))
In [122]: mapping
Out[122]: {0: 4, 1: 3, 2: 2, 3: 1, 4: 0}

Ниже мы рассмотрим словарное включение – еще один элегантный способ
построения словарей.

Значения по умолчанию
Очень часто можно встретить код, реализующий такую логику:
if key in some_dict:
value = some_dict[key]
else:
value = default_value

Поэтому методы словаря get и pop могут принимать значение, возвращаемое по умолчанию, так что этот блок if–else можно упростить:
value = some_dict.get(key, default_value)

Метод get по умолчанию возвращает None, если ключ не найден, тогда как
pop в этом случае возбуждает исключение. Часто бывает, что значениями
в словаре являются другие коллекции, например списки. Так, можно классифицировать слова по первой букве и представить их набор в виде словаря
списков:
In [123]: words = ['apple', 'bat', 'bar', 'atom', 'book']
In [124]: by_letter = {}
In [125]: for word in words:
.....:
letter = word[0]
.....:
if letter not in by_letter:
.....:
by_letter[letter] = [word]
.....:
else:
.....:
by_letter[letter].append(word)
.....:

84

Встроенные структуры данных, функции и файлы

In [126]: by_letter
Out[126]: {'a': ['apple', 'atom'], 'b': ['bat', 'bar', 'book']}

Метод setdefault предназначен специально для этой цели. Цикл for выше
можно переписать так:
for word in words:
letter = word[0]
by_letter.setdefault(letter, []).append(word)

В стандартном модуле collections есть полезный класс defaultdict, который еще больше упрощает решение этой задачи. Его конструктору передается
тип или функция, генерирующие значение по умолчанию для каждой пары
в словаре:
from collections import defaultdict
by_letter = defaultdict(list)
for word in words:
by_letter[word[0]].append(word)

Допустимые типы ключей словаря
Значениями словаря могут быть произвольные объекты Python, но ключами должны быть неизменяемые объекты, например скалярные типы (int,
float, строка) или кортежи (причем все объекты кортежа тоже должны быть
неизменяемыми). Технически это свойство называется хешируемостью. Проверить, является ли объект хешируемым (и, стало быть, может быть ключом
словаря), позволяет функция hash:
In [127]: hash('string')
Out[127]: –9167918882415130555
In [128]: hash((1, 2, (2, 3)))
Out[128]: 1097636502276347782
In [129]: hash((1, 2, [2, 3])) # ошибка, списки изменяемы
–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
TypeError
Traceback (most recent call last)
in ()
––––> 1 hash((1, 2, [2, 3])) # ошибка, списки изменяемы
TypeError: unhashable type: 'list'

Чтобы использовать список в качестве ключа, достаточно преобразовать
его в кортеж, который допускает хеширование, если это верно для его элементов:
In [130]: d = {}
In [131]: d[tuple([1, 2, 3])] = 5
In [132]: d
Out[132]: {(1, 2, 3): 5}

85

Структуры данных и последовательности

Множество
Множество – это неупорядоченная коллекция уникальных элементов. Можно считать, что это словари, не содержащие значений. Создать множество
можно двумя способами: с помощью функции set или задав множество-литерал в фигурных скобках:
In [133]: set([2, 2, 2, 1, 3, 3])
Out[133]: set([1, 2, 3])
In [134]: {2, 2, 2, 1, 3, 3}
Out[134]: set([1, 2, 3])

Множества поддерживают теоретико-множественные операции: объединение, пересечение, разность и симметрическую разность. Рассмотрим следующие два примера множеств:
In [135]: a = {1, 2, 3, 4, 5}
In [136]: b = {3, 4, 5, 6, 7, 8}

Их объединение – это множество, содержащее неповторяющиеся элементы, встречающиеся хотя бы в одном множестве. Вычислить его можно с помощью метода union или бинарного оператора |:
In [137]: a.union(b)
Out[137]: {1, 2, 3, 4, 5, 6, 7, 8}
In [138]: a | b
Out[138]: {1, 2, 3, 4, 5, 6, 7, 8}

Пересечение множеств содержит элементы, встречающиеся в обоих множествах. Вычислить его можно с помощью метода intersection или бинарного
оператора &:
In [139]: a.intersection(b)
Out[139]: {3, 4, 5}
In [140]: a & b
Out[140]: {3, 4, 5}

Наиболее употребительные методы множеств перечислены в табл. 3.1.
Таблица 3.1. Операции над множествами в Python
Функция
a.add(x)
a.clear(x)
a.remove(x)
a.pop(x)

Альтернативный
синтаксис
Нет
Нет
Нет
Нет

Описание
Добавить элемент x в множество a
Опустошить множество, удалив из него все элементы
Удалить элемент x из множества a
Удалить какой-то элемент x из множества a и возбудить
исключение KeyError, если множество пусто

86

Встроенные структуры данных, функции и файлы

Таблица 3.1 (окончание)

a.union(b)

Альтернативный
синтаксис
a|b

a.update(b)
a.intersection(b)
a.intersection_update(b)
a.difference(b)
a.difference_update(b)

a |= b
a&b
a &= b
a–b
a –= b

a.symmetric_
difference(b)
a.symmetric_difference_
update(b)
a.issubset(b)
a.issuperset(b)
a.isdisjoint(b)

a^b

Функция

a ^= b
a = b
Нет

Описание
Найти все уникальные элементы, входящие либо в a,
либо в b
Присвоить a объединение элементов a и b
Найти все элементы, входящие и в a, и в b
Присвоить a пересечение элементов a и b
Найти элементы, входящие в a, но не входящие в b
Записать в a элементы, которые входят в a, либо в b,
но не входят в b
Найти элементы, входящие либо в a, либо в b,
но не в a и b одновременно
Записать в a элементы, которые входят либо в a, либо в b,
но не в a и b одновременно
True, если все элементы a входят также и в b
True, если все элементы b входят также и в a
True, если у a и b нет ни одного общего элемента

У всех логических операций над множествами имеются варианты с обновлением на месте, которые позволяют заменить содержимое множества
в левой части результатом операции. Для очень больших множеств это может
оказаться эффективнее:
In [141]: c = a.copy()
In [142]: c |= b
In [143]: c
Out[143]: {1, 2, 3, 4, 5, 6, 7, 8}
In [144]: d = a.copy()
In [145]: d &= b
In [146]: d
Out[146]: {3, 4, 5}

Как и в случае со словарями, элементы множества, вообще говоря, должны быть неизменяемыми. Чтобы включить в множество элементы, подобные
списку, необходимо сначала преобразовать их в кортеж.
Можно также проверить, является ли множество подмножеством (содержится в) или надмножеством (содержит) другого множества:
In [150]: a_set = {1, 2, 3, 4, 5}
In [151]: {1, 2, 3}.issubset(a_set)
Out[151]: True
In [152]: a_set.issuperset({1, 2, 3})
Out[152]: True

Структуры данных и последовательности

87

Множества называются равными, если состоят из одинаковых элементов:
In [153]: {1, 2, 3} == {3, 2, 1}
Out[153]: True

Списковое, словарное и множественное включения
Списковое включение1 – одна из самых любимых особенностей Python. Этот
механизм позволяет кратко записать создание нового списка, образованного
фильтрацией элементов коллекции с одновременным преобразованием элементов, прошедших через фильтр. Основная синтаксическая форма такова:
[expr for val in collection if condition]

Это эквивалентно следующему циклу for:
result = []
for val in collection:
if condition:
result.append(expr)

Условие фильтрации можно опустить, оставив только выражение. Например, если задан список строк, то мы могли бы выделить из него строки длиной больше 2 и попутно преобразовать их в верхний регистр:
In [154]: strings = ['a', 'as', 'bat', 'car', 'dove', 'python']
In [155]: [x.upper() for x in strings if len(x) > 2]
Out[155]: ['BAT', 'CAR', 'DOVE', 'PYTHON']

Словарное и множественное включения – естественные обобщения, которые предлагают аналогичную идиому для порождения словарей и множеств.
Словарное включение выглядит так:
dict_comp = {key-expr : value-expr for value in collection if condition}

Множественное включение очень похоже на списковое, и квадратные скобки заменяются фигурными:
set_comp = {expr for value in collection if condition}

Все виды включений не более чем синтаксическая глазурь, упрощающая
чтение и написание кода. Рассмотрим приведенный выше список строк. Допустим, требуется построить множество, содержащее длины входящих в коллекцию строк; это легко сделать с помощью множественного включения:
In [156]: unique_lengths = {len(x) for x in strings}
1

Более-менее устоявшийся перевод термина list comprehension – «списковое включение» крайне неудачен и совершенно не отражает сути дела. Я предложил бы термин
«трансфильтрация», объединяющий слова «трансформация» и «фильтрация», но не
уверен в положительной реакции сообщества. – Прим. перев.

88

Встроенные структуры данных, функции и файлы

In [157]: unique_lengths
Out[157]: set([1, 2, 3, 4, 6])

То же самое можно записать в духе функционального программирования,
воспользовавшись функцией map, с которой мы познакомимся ниже:
In [158]: set(map(len, strings))
Out[158]: {1, 2, 3, 4, 6}

В качестве простого примера словарного включения создадим словарь, сопоставляющий каждой строке ее позицию в списке:
In [159]: loc_mapping = {val : index for index, val in enumerate(strings)}
In [160]: loc_mapping
Out[160]: {'a': 0, 'as': 1, 'bat': 2, 'car': 3, 'dove': 4, 'python': 5}

Вложенное списковое включение
Пусть имеется список списков, содержащий английские и испанские имена:
In [161]: all_data = [['John', 'Emily', 'Michael', 'Mary', 'Steven'],
.....:
['Maria', 'Juan', 'Javier', 'Natalia', 'Pilar']]

Возможно, вы взяли эти имена из двух разных файлов и решили рассортировать их по языкам. А теперь допустим, что требуется получить один список,
содержащий все имена, в которых встречается не менее двух букв e. Конечно,
это можно было бы сделать в таком простом цикле for:
names_of_interest = []
for names in all_data:
enough_es = [name for name in names if name.count('e') > 2]
names_of_interest.extend(enough_es)

Но можно обернуть всю операцию одним вложенным списковым включением:
In [162]: result = [name for names in all_data for name in names
.....:
if name.count('e') >= 2]
In [163]: result
Out[163]: ['Steven']

Поначалу вложенное списковое включение с трудом укладывается в мозгу.
Части for соответствуют порядку вложенности, а все фильтры располагаются
в конце, как и раньше. Вот еще один пример, в котором мы линеаризуем
список кортежей целых чисел, создавая один плоский список:
In [164]: some_tuples = [(1, 2, 3), (4, 5, 6), (7, 8, 9)]
In [165]: flattened = [x for tup in some_tuples for x in tup]
In [166]: flattened
Out[166]: [1, 2, 3, 4, 5, 6, 7, 8, 9]

Функции

89

Помните, что порядок выражений for точно такой же, как если бы вы писали вложенные циклы for, а не списковое включение:
flattened = []
for tup in some_tuples:
for x in tup:
flattened.append(x)

Глубина уровня вложенности не ограничена, хотя, если уровней больше
трех, стоит задуматься о правильности выбора структуры данных. Важно отличать показанный выше синтаксис от спискового включения внутри спискового включения – тоже вполне допустимой конструкции:
In [167]: [[x for x in tup] for tup in some_tuples]
Out[167]: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

Эта операция порождает список списков, а не линеаризованный список
всех внутренних элементов.

3.2. Функции
Функции – главный и самый важный способ организации и повторного использования кода в Python. Если вам кажется, что некоторый код может использоваться более одного раза, возможно, с небольшими вариациями, то
имеет смысл оформить его в виде функции. Кроме того, функции могут
сделать код более понятным, поскольку дают имя группе взаимосвязанных
предложений.
Объявление функции начинается ключевым словом def, а результат возвращается в предложении return:
def my_function(x, y, z=1.5):
if z > 1:
return z * (x + y)
else:
return z / (x + y)

Ничто не мешает иметь в функции несколько предложений return. Если при
выполнении достигнут конец функции, а предложение return не встретилось,
то возвращается None.
У функции могут быть позиционные и именованные аргументы. Именованные аргументы обычно используются для задания значений по умолчанию
и необязательных аргументов. В примере выше x и y – позиционные аргументы, а z – именованный. Следующие вызовы функции эквивалентны:
my_function(5, 6, z=0.7)
my_function(3.14, 7, 3.5)
my_function(10, 20)

90

Встроенные структуры данных, функции и файлы

Основное ограничение состоит в том, что именованные аргументы должны находиться после всех позиционных (если таковые имеются). Сами же
именованные аргументы можно задавать в любом порядке, это освобождает
программиста от необходимости помнить, в каком порядке были указаны
аргументы функции в объявлении. Важно лишь, как они называются.
Ключевые слова можно использовать и для передачи позиционных аргументов. Предыдущий пример можно было бы записать и так:
my_function(x=5, y=6, z=7)
my_function(y=6, x=5, z=7)
Иногда код при этом становится понятнее.

Пространства имен, области видимости
и локальные функции
Функции могут обращаться к переменным, объявленным в двух областях
видимости: глобальной и локальной. Область видимости переменной в Python
называют также пространством имен. Любая переменная, которой присвоено
значение внутри функции, по умолчанию попадает в локальное пространство
имен. Локальное пространство имен создается при вызове функции, и в него
сразу же заносятся аргументы функции. По завершении функции локальное
пространство имен уничтожается (хотя бывают и исключения, см. ниже раздел о замыканиях). Рассмотрим следующую функцию:
def func():
a = []
for i in range(5):
a.append(i)

При вызове func() создается пустой список a, в него добавляется пять элементов, а затем, когда функция завершается, список a уничтожается. Но допустим, что мы объявили a следующим образом:
a = []
def func():
for i in range(5):
a.append(i)

Присваивать значение глобальной переменной внутри функции допустимо, но такие переменные должны быть объявлены глобальными с помощью
ключевого слова global:
In [168]: a = None
In [169]: def bind_a_variable():
.....:
global a
.....:
a = []

91

Функции
.....:
.....:

bind_a_variable()

In [170]: print a
[]
Вообще, я не рекомендую злоупотреблять ключевым словом global. Обычно
глобальные переменные служат для хранения состояния системы. Если вы
понимаете, что пользуетесь ими слишком часто, то стоит подумать о переходе
к объектно-ориентированному программированию (использовать классы).

Возврат нескольких значений
Когда я только начинал программировать на Python после многих лет работы на Java и C++, одной из моих любимых возможностей была возможность
возвращать из функции несколько значений. Вот простой пример:
def f():
a = 5
b = 6
c = 7
return a, b, c
a, b, c = f()

В анализе данных и других научных приложениях это встречается сплошь
и рядом, потому что многие функции вычисляют несколько результатов. На
самом деле функция здесь возвращает всего один объект, а именно кортеж,
который затем распаковывается в результирующие переменные. В примере
выше можно было поступить и так:
return_value = f()

В таком случае return_value было бы 3-кортежем, содержащим все три возвращенные переменные. Иногда разумнее возвращать несколько значений
не в виде кортежа, а в виде словаря:
def f():
a = 5
b = 6
c = 7
return {'a' : a, 'b' : b, 'c' : c}

Полезен ли такой способ, зависит от решаемой задачи.

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

92

Встроенные структуры данных, функции и файлы

In [171]: states = [' Alabama ', 'Georgia!', 'Georgia', 'georgia', 'FlOrIda',
.....:
'south carolina##', 'West virginia?']

Всякий, кому доводилось работать с присланными пользователями данными опроса, ожидает такого рода мусор. Чтобы сделать данный список
строк пригодным для анализа, нужно произвести различные операции: удалить лишние пробелы и знаки препинания, оставить заглавные буквы только в нужных местах. Сделать это можно, например, с помощью встроенных
методов строк и стандартного библиотечного модуля re для работы с регулярными выражениями:
import re
def clean_strings(strings):
result = []
for value in strings:
value = value.strip()
value = re.sub('[!#?]', '', value)
value = value.title()
result.append(value)
return result

Вот как выглядит результат:
In [173]: clean_strings(states)
Out[173]:
['Alabama',
'Georgia',
'Georgia',
'Georgia',
'Florida',
'South Carolina',
'West Virginia']

Другой подход, который иногда бывает полезен, – составить список операций, которые необходимо применить к набору строк:
def remove_punctuation(value):
return re.sub('[!#?]', '', value)
clean_ops = [str.strip, remove_punctuation, str.title]
def clean_strings(strings, ops):
result = []
for value in strings:
for function in ops:
value = function(value)
result.append(value)
return result

Далее поступаем следующим образом:
In [175]: clean_strings(states, clean_ops)
Out[175]:

Функции

93

['Alabama',
'Georgia',
'Georgia',
'Georgia',
'Florida',
'South Carolina',
'West Virginia']

Подобный функциональный подход позволяет задать способ модификации
строк на очень высоком уровне. Степень повторной используемости функции
clean_strings определенно возросла!
Функции можно передавать в качестве аргументов другим функциям, например встроенной функции map, которая применяет переданную функцию
к коллекции:
In [176]: for x in map(remove_punctuation, states):
.....:
print(x)
Alabama
Georgia
Georgia
georgia
FlOrIda
south carolina
West virginia

Анонимные (лямбда) функции
Python поддерживает так называемые анонимные функции, или лямбдафункции. По существу, это простые однострочные функции, возвращающие
значение. Определяются они с помощью ключевого слова lambda, которое
означает всего лишь «мы определяем анонимную функцию» и ничего более.
def short_function(x):
return x * 2
equiv_anon = lambda x: x * 2

В этой книге я обычно употребляю термин «лямбда-функция». Они особенно удобны в ходе анализа данных, потому что, как вы увидите, во многих
случаях функции преобразования данных принимают другие функции в качестве аргументов. Часто быстрее (и чище) передать лямбда-функцию, чем
писать полноценное объявление функции или даже присваивать лямбдафункцию локальной переменной. Рассмотрим такой простенький пример:
def apply_to_list(some_list, f):
return [f(x) for x in some_list]
ints = [4, 0, 1, 5, 6]
apply_to_list(ints, lambda x: x * 2)

94

Встроенные структуры данных, функции и файлы

Можно было бы, конечно, написать [x * 2 for x in ints], но в данном случае
нам удалось передать функции apply_to_list пользовательский оператор.
Еще пример: пусть требуется отсортировать коллекцию строк по количеству различных букв в строке.
In [177]: strings = ['foo', 'card', 'bar', 'aaaa', 'abab']

Для этого можно передать лямбда-функцию методу списка sort:
In [178]: strings.sort(key=lambda x: len(set(list(x))))
In [179]: strings
Out[179]: ['aaaa', 'foo', 'abab', 'bar', 'card']
Лямбда-функции называются анонимными, потому что, в отличие от функций,
объявленных с помощью ключевого слова def, у такой функции нет явного
атрибута __name__.

Каррирование: фиксирование части аргументов
Каррированием (в честь математика Хаскелла Карри) в информатике называется порождение новых функций из существующих путем фиксирования
некоторых аргументов. Рассмотрим, к примеру, тривиальную функцию, складывающую два числа:
def add_numbers(x, y):
return x + y

На ее основе можно создать новую функцию одной переменной add_five,
которая прибавляет к своему аргументу 5:
add_five = lambda y: add_numbers(5, y)

Говорят, что второй аргумент функции add_numbers каррирован. В этом нет
ничего особо примечательного – мы всего лишь определили новую функцию,
которая вызывает существующую. Стандартный модуль functools упрощает
эту процедуру за счет функции partial:
from functools import partial
add_five = partial(add_numbers, 5)

Генераторы
Наличие единого способа обхода последовательностей, например объектов
в списке или строк в файле, – важная особенность Python. Реализована она
с помощью протокола итератора, общего механизма, наделяющего объекты
свойством итерируемости. Например, при обходе (итерировании) словаря
получаем хранящиеся в нем ключи:
In [180]: some_dict = {'a': 1, 'b': 2, 'c': 3}
In [181]: for key in some_dict:
.....:
print(key)

Функции

95

a
b
c

Встречая конструкцию for key in some_dict, интерпретатор Python сначала
пытается создать итератор из some_dict:
In [182]: dict_iterator = iter(some_dict)
In [183]: dict_iterator
Out[183]:

Итератор – это любой объект, который отдает интерпретатору Python
объекты при использовании в контексте, аналогичном циклу for. Методы,
ожидающие получить список или похожий на список объект, как правило,
удовлетворяются любым итерируемым объектом. Это относится, в частности,
к встроенным методам, например min, max и sum, и к конструкторам типов,
например list и tuple:
In [184]: list(dict_iterator)
Out[184]: ['a', 'c', 'b']

Генератор – это простой способ конструирования итерируемого объекта.
Если обычная функция выполняется и возвращает единственное значение,
то генератор «лениво» возвращает последовательность значений, приостанавливаясь после возврата каждого в ожидании запроса следующего. Чтобы создать генератор, нужно вместо return использовать ключевое слово
yield:
def squares(n=10):
print('Generating squares from 1 to {0}'.format(n ** 2))
for i in range(1, n + 1):
yield i ** 2

В момент вызова генератора никакой код не выполняется:
In [186]: gen = squares()
In [187]: gen
Out[187]:

И лишь после запроса элементов генератор начинает выполнять свой код:
In [4]: for x in gen:
...:
print x,
...:
Генерируются квадраты чисел от 1 до 100
1 4 9 16 25 36 49 64 81 100

Генераторные выражения
Еще более лаконичный способ создать генератор – воспользоваться генераторным выражением. Такой генератор аналогичен списковому, словарному

96

Встроенные структуры данных, функции и файлы

и множественному включениям. Чтобы его создать, заключите выражение,
которое выглядит как списковое включение, в круглые скобки вместо квадратных:
In [189]: gen = (x ** 2 for x in xrange(100))
In [190]: gen
Out[190]:

Это в точности эквивалентно следующему более многословному определению генератора:
def _make_gen():
for x in xrange(100):
yield x ** 2
gen = _make_gen()

Генераторные выражения можно использовать внутри любой функции Python, принимающей генератор:
In [191]: sum(x ** 2 for x in xrange(100))
Out[191]: 328350
In [192]: dict((i, i **2) for i in xrange(5))
Out[192]: {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

Модуль itertools
Стандартный библиотечный модуль itertools содержит набор генераторов
для многих общеупотребительных алгоритмов. Так, генератор groupby принимает произвольную последовательность и функцию, он группирует соседние элементы последовательности по значению, возвращенному функцией,
например:
In [193]: import itertools
In [194]: first_letter = lambda x: x[0]
In [195]: names = ['Alan', 'Adam', 'Wes', 'Will', 'Albert', 'Steven']
In [196]: for letter, names in itertools.groupby(names, first_letter):
.....:
print letter, list(names) # names – это генератор
A ['Alan', 'Adam']
W ['Wes', 'Will']
A ['Albert']
S ['Steven']

В табл. 3.2 описаны некоторые функции из модуля itertools, которыми
я часто пользуюсь. Дополнительную информацию об этом полезном стандартном модуле можно почерпнуть из официальной документации на сайте
https://docs.python.org/3/library/itertools.html.

97

Функции

Таблица 3.2. Некоторые полезные функции из модуля itertools
Функция

Описание

Генерирует последовательность всех возможных k-кортежей,
составленных из элементов iterable, без учета порядка
permutations(iterable, k)
Генерирует последовательность всех возможных k-кортежей,
составленных из элементов iterable, с учетом порядка
groupby(iterable[, keyfunc]) Генерирует пары (ключ, субитератор) для каждого уникального ключа
product(*iterables, repeat=1) Генерирует декартово произведение входных итерируемых величин
в виде кортежей, как если бы использовался вложенный цикл for
combinations(iterable, k)

Обработка исключений
Обработка ошибок, или исключений, в Python – важная часть создания
надежных программ. В приложениях для анализа данных многие функции
работают только для входных данных определенного вида. Например, функция float может привести строку к типу числа с плавающей точкой, но если
формат строки заведомо некорректен, то завершается с ошибкой ValueError:
In [197]: float('1.2345')
Out[197]: 1.2345
In [198]: float('something')
–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
ValueError
Traceback (most recent call last)
in ()
––––> 1 float('something')
ValueError: could not convert string to float: 'something'

Пусть требуется написать версию float, которая не завершается с ошибкой,
а возвращает поданный на вход аргумент. Это можно сделать, обернув вызов
float блоком try/except:
def attempt_float(x):
try:
return float(x)
except:
return x

Код в части except будет выполняться, только если float(x) возбуждает исключение:
In [200]: attempt_float('1.2345')
Out[200]: 1.2345
In [201]: attempt_float('something')
Out[201]: 'something'

Кстати, float может возбуждать и другие исключения, не только ValueError:
In [202]: float((1, 2))
–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––

98

Встроенные структуры данных, функции и файлы

TypeError
Traceback (most recent call last)
in ()
––––> 1 float((1, 2))
TypeError: float() argument must be a string or a number, not 'tuple'

Возможно, вы хотите перехватить только исключение ValueError, поскольку
TypeError (аргумент был не строкой и не числом) может свидетельствовать
о логической ошибке в программе. Для этого нужно написать после except
тип исключения:
def attempt_float(x):
try:
return float(x)
except ValueError:
return x

В таком случае получим:
In [204]: attempt_float((1, 2))
–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
TypeError
Traceback (most recent call last)
in ()
––––> 1 attempt_float((1, 2))
in attempt_float(x)
1 def attempt_float(x):
2
try:
––––> 3
return float(x)
4
except ValueError:
5
return x
TypeError: float() argument must be a string or a number, not 'tuple'

Можно перехватывать исключения нескольких типов, для чего достаточно
написать кортеж типов (скобки обязательны):
def attempt_float(x):
try:
return float(x)
except (TypeError, ValueError):
return x

Иногда исключение не нужно перехватывать, но какой-то код должен быть
выполнен вне зависимости от того, возникло исключение в блоке try или нет.
Для этого служит предложение finally:
f = open(path, 'w')
try:
write_to_file(f)
finally:
f.close()

Функции

99

Здесь описатель файла f закрывается в любом случае. Можно также написать код, который исполняется, только если в блоке try не было исключения.
Для этого используется ключевое слово else:
f = open(path, 'w')
try:
write_to_file(f)
except:
print 'Ошибка'
else:
print 'Все хорошо'
finally:
f.close()

Исключения в IPython
Если исключение возникает в процессе выполнения скрипта командой %run
или при выполнении любого предложения, то IPython по умолчанию распечатывает весь стек (выполняет трассировку стека) и несколько строк вокруг
каждого предложения в стеке, чтобы можно было понять контекст:
In [10]: %run examples/ipython_bug.py
–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
AssertionError
Traceback (most recent call last)
/home/wesm/code/pydata–book/examples/ipython_bug.py in ()
13
throws_an_exception()
14
–––> 15 calling_things()
/home/wesm/code/pydata–book/examples/ipython_bug.py in calling_things()
11 def calling_things():
12
works_fine()
–––> 13
throws_an_exception()
14
15 calling_things()
/home/wesm/code/pydata–book/examples/ipython_bug.py in throws_an_exception()
7
a = 5
8
b = 6
––––> 9
assert(a + b == 10)
10
11 def calling_things():
AssertionError:

Наличие дополнительного контекста уже является весомым преимуществом по сравнению со стандартным интерпретатором Python (который никакого контекста не выводит). Объемом контекста можно управлять с помощью магической команды %xmode, он может варьироваться от Plain (так же
как в стандартном интерпретаторе Python) до Verbose (печатаются значения

100

Встроенные структуры данных, функции и файлы

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

3.3. Файлы и операционная система
В этой книге для чтения файла с диска и загрузки данных из него в структуры Python, как правило, используются такие высокоуровневые средства, как
функция pandas.read_csv. Однако важно понимать основы работы с файлами
в Python. По счастью, здесь все очень просто, и именно поэтому Python так
часто выбирают, когда нужно работать с текстом или файлами.
Чтобы открыть файл для чтения или записи, пользуйтесь встроенной функцией open, которая принимает относительный или абсолютный путь:
In [207]: path = 'ch13/segismundo.txt'
In [208]: f = open(path)

По умолчанию файл открывается только для чтения – в режиме 'r'. Далее
описатель файла f можно рассматривать как список и перебирать строки:
for line in f:
pass

У строк, прочитанных из файла, сохраняется признак конца строки (EOL),
поэтому часто можно встретить код, который удаляет концы строк:
In [209]: lines = [x.rstrip() for x in open(path)]
In [210]: lines
Out[210]:
['Sueńa el rico en su riqueza,',
'que más cuidados le ofrece;',
'',
'sueńa el pobre que padece',
'su miseria y su pobreza;',
'',
'sueńa el que a medrar empieza,',
'sueńa el que afana y pretende,',
'sueńa el que agravia y ofende,',
'',
'y en el mundo, en conclusión,',
'todos sueńan lo que son,',
'aunque ninguno lo entiende.',
'']

Если для создания объекта файла использовалась функция open, то следует
явно закрывать файл по завершении работы с ним. Закрытие файла возвращает ресурсы операционной системе:

Файлы и операционная система

101

In [211]: f.close()

Упростить эту процедуру позволяет предложение with:
In [212]: with open(path) as f:
.....:
lines = [x.rstrip() for x in f]

В таком случае файл f автоматически закрывается по выходе из блока with.
Если бы мы написали f = open(path, 'w'), то был бы создан новый файл
examples/segismundo.txt (будьте осторожны!), а старый был бы перезаписан.
Существует также режим 'x', в котором создается допускающий запись файл,
но лишь в том случае, если его еще не существует, в противном случае возбуждается исключение. Все допустимые режимы ввода-выводаперечислены
в табл. 3.3.
Таблица 3.3. Режимы открытия файла в Python
Режим

Описание

r
w
a
r+
b
t

Режим чтения
Режим записи. Создается новый файл (старый с тем же именем удаляется)
Дописывание в конец существующего файла (если файла нет, его создают)
Чтение и запись
Уточнение режима для двоичных файлов: 'rb' или 'wb'
Текстовый режим (байты автоматически декодируются в Unicode). Этот режим подразумевается по умолчанию, если не указано противное. Букву t можно комбинировать с другими
режимами (например, 'rt' или 'xt')

При работе с файлами, допускающими чтение, чаще всего употребляются
методы read, seek и tell. Метод read возвращает определенное число символов
из файла. Что такое «символ», определяется кодировкой файла (например,
UTF-8). Если же файл открыт в двоичном режиме, то под символами понимаются просто байты:
In [213]: f = open(path)
In [214]: f.read(10)
Out[214]: 'Sueńa el r'
In [215]: f2 = open(path, 'rb') # Двоичный режим
In [216]: f2.read(10)
Out[216]: b'Sue\xc3\xb1a el '

Метод read продвигает указатель файла вперед на количество прочитанных
байтов. Метод tell сообщает текущую позицию:
In [217]: f.tell()
Out[217]: 11
In [218]: f2.tell()
Out[218]: 10

102

Встроенные структуры данных, функции и файлы

Хотя мы прочитали из файла 10 символов, позиция равна 11, потому что
именно столько байтов пришлось прочитать, чтобы декодировать 19 символов в подразумеваемой по умолчанию кодировке файла. Чтобы узнать кодировку по умолчанию, воспользуемся модулем sys:
In [219]: import sys
In [220]: sys.getdefaultencoding()
Out[220]: 'utf–8'

Метод seek изменяет позицию в файле на указанную:
In [221]: f.seek(3)
Out[221]: 3
In [222]: f.read(1)
Out[222]: 'ń'

Наконец, не забудем закрыть файлы:
In [223]: f.close()
In [224]: f2.close()

Для записи текста в файл служат методы write или writelines. Например,
можно было бы создать вариант файла prof_mod.py без пустых строк:
In [225]: with open('tmp.txt', 'w') as handle:
.....:
handle.writelines(x for x in open(path) if len(x) > 1)
In [226]: with open('tmp.txt') as f:
.....:
lines = f.readlines()
In [227]: lines
Out[227]:
['Sueńa el rico en su riqueza,\n',
'que más cuidados le ofrece;\n',
'sueńa el pobre que padece\n',
'su miseria y su pobreza;\n',
'sueńa el que a medrar empieza,\n',
'sueńa el que afana y pretende,\n',
'sueńa el que agravia y ofende,\n',
'y en el mundo, en conclusión,\n',
'todos sueńan lo que son,\n',
'aunque ninguno lo entiende.\n']

В табл. 3.4 приведены многие из наиболее употребительных методов работы с файлами.

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

Файлы и операционная система

103

ками (которые хранятся в Unicode). Чтобы открыть файл в двоичном режиме,
следует добавить к основному режиму букву b. Рассмотрим файл из предыдущего раздела (содержащий не-ASCII-символы в кодировке UTF-8):
In [230]: with open(path) as f:
.....:
chars = f.read(10)
In [231]: chars
Out[231]: 'Sueńa el r'

Таблица 3.4. Наиболее употребительные методы и атрибуты
для работы с файлами в Python
Метод

Описание

read([size])

Возвращает прочитанные из файла данные в виде строки. Необязательный
аргумент size сообщает, сколько байтов читать
Возвращает список прочитанных из файла строк. Необязательный аргумент size
сообщает, сколько строк читать
Записывает переданную строку в файл
Записывает переданную последовательность строк в файл
Закрывает описатель файла
Сбрасывает внутренний буфер ввода-вывода на диск
Перемещает указатель чтения-записи на байт файла с указанным номером
Возвращает текущую позицию в файле в виде целого числа
True, если файл закрыт

readlines([size])
write(str)
writelines(strings)
close()
flush()
seek(pos)
tell()
closed

UTF-8 – это кодировка Unicode переменной длины, поэтому, когда я запрашиваю чтение нескольких символов из файла, Python читает столько байтов,
чтобы после декодирования получилось указанное количество символов (их
может быть всего 10, а может быть и целых 40). Если вместо этого открыть файл
в режиме 'rb', то read прочитает ровно столько байтов, сколько запрошено:
In [232]: with open(path, 'rb') as f:
.....:
data = f.read(10)
In [233]: data
Out[233]: b'Sue\xc3\xb1a el '

Зная кодировку текста, вы можете декодировать байты в объект str самостоятельно, но только в том случае, если последовательность байтов корректна и полна:
In [234]: data.decode('utf8')
Out[234]: 'Sueńa el '
In [235]: data[:4].decode('utf8')
–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
UnicodeDecodeError Traceback (most recent call last)
in ()
––––> 1 data[:4].decode('utf8')
UnicodeDecodeError: 'utf–8' codec can't decode byte 0xc3 in position 3: unexpected end of data

104

Встроенные структуры данных, функции и файлы

Текстовый режим в сочетании с параметром encoding функции open – удобный способ преобразовать данные из одной кодировки Unicode в другую:
In [236]: sink_path = 'sink.txt'
In [237]: with open(path) as source:
.....:
with open(sink_path, 'xt', encoding='iso–8859–1') as sink:
.....:
sink.write(source.read())
In [238]: with open(sink_path, encoding='iso–8859–1') as f:
.....:
print(f.read(10))
Sueńa el r

Будьте осторожны, вызывая метод seek для файла, открытого не в двоичном режиме. Если указанная позиция окажется в середине последовательности байтов, образующих один символ Unicode, то последующие операции
чтения завершатся ошибкой:
In [240]: f = open(path)
In [241]: f.read(5)
Out[241]: 'Sueńa'
In [242]: f.seek(4)
Out[242]: 4
In [243]: f.read(1)
–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
UnicodeDecodeError
Traceback (most recent call last)
in ()
––––> 1 f.read(1)
/miniconda/envs/book–env/lib/python3.6/codecs.py in decode(self, input, final)
319
# декодировать входные данные (с учетом буфера)
320
data = self.buffer + input
––> 321
(result, consumed) = self._buffer_decode(data, self.errors, final
)
322
# сохранить недекодированные входные данные до следующего вызова
323
self.buffer = data[consumed:]
UnicodeDecodeError: 'utf–8' codec can't decode byte 0xb1 in position 0: invalid start byte
In [244]: f.close()

Если вы регулярно анализируете текстовые данные в кодировке, отличной
от ASCII, то уверенное владение средствами работы с Unicode, имеющимися
в Python, придется весьма кстати. Дополнительные сведения смотрите в онлайновой документации на сайте https://docs.python.org/3/.

3.4. Заключение
Вооружившись базовыми знаниями о языке Python и его среде, мы можем
перейти к изучению NumPy и вычислениям с массивами.

Глава 4. Основы NumPy: массивы
и векторные вычисления
Numerical Python, или, сокращенно, NumPy, – один из важнейших пакетов для
численных расчетов в Python. В большинстве пакетов для научных расчетов
используются объекты массивов NumPy, которые являются универсальным
языком обмена данными.
Вот лишь часть того, что предлагает NumPy:
• ndarray, эффективный многомерный массив, предоставляющий быстрые
арифметические операции с массивами и гибкий механизм укладывания;
• математические функции для выполнения быстрых операций над целыми массивами без явного выписывания циклов;
• средства для чтения массива данных с диска и записи его на диск, а также для работы с проецируемыми на память файлами;
• алгоритмы линейной алгебры, генерация случайных чисел и преобразование Фурье;
• средства для интеграции с кодом, написанным на C, C++ или Fortran.
Благодаря наличию простого C API в NumPy очень легко передавать данные внешним библиотекам, написанным на языке низкого уровня, а также
получать от внешних библиотек данные в виде массивов NumPy. Эта возможность сделала Python излюбленным языком для обертывания имеющегося
кода на C/C++/Fortran с приданием ему динамического и простого в использовании интерфейса.
Хотя сам по себе пакет NumPy почти не содержит средств для моделирования и научных расчетов, понимание массивов NumPy и ориентированных на
эти массивы вычислений поможет гораздо эффективнее использовать инструменты типа pandas. Поскольку NumPy – обширная тема, я вынес описание многих продвинутых средств NumPy, в частности укладывания, в приложение A.

106

Основы NumPy: массивы и векторные вычисления

В большинстве приложений для анализа данных основной интерес представляет следующая функциональность:
• быстрые векторные операции для переформатирования и очистки данных, выборки подмножеств и фильтрации, преобразований и других видов вычислений;
• стандартные алгоритмы работы с массивами, например фильтрация,
удаление дубликатов и теоретико-множественные операции;
• эффективная описательная статистика, агрегирование и обобщение
данных;
• выравнивание данных и реляционные операции объединения и соединения разнородных наборов данных;
• описание условной логики в виде выражений-массивов вместо циклов
с ветвлением if–elif–else;
• групповые операции с данными (агрегирование, преобразование, применение функции).
Хотя в NumPy имеются вычислительные основы для этих операций, по
большей части для анализа данных (особенно структурированных или табличных) лучше использовать библиотеку pandas, потому что она предлагает
развитый высокоуровневый интерфейс для решения большинства типичных
задач обработки данных – простой и лаконичный. Кроме того, в pandas есть
кое-какая предметно-ориентированная функциональность, например операции с временными рядами, отсутствующая в NumPy.
Вычисления с массивами в Python уходят корнями в 1995 год, когда Джим
Хьюганин (Jim Hugunin) создал библиотеку Numeric. В течение следующих десяти лет программирование с массивами распространилось во многих научных
сообществах, но в начале 2000-х годов библиотечная экосистема оказалась
фрагментированной. В 2005 году Трэвис Олифант (Travis Oliphant) сумел собрать проект NumPy из существовавших тогда проектов Numeric и Numarray,
чтобы объединить сообщество вокруг общей вычислительной инфраструктуры.

Одна из причин, по которым NumPy играет такую важную роль в численных
расчетах, – то, что она проектировалась с прицелом на эффективную работу
с большими массивами данных. Отметим, в частности, следующие аспекты:
• NumPy хранит данные в непрерывном блоке памяти независимо от
других встроенных объектов Python. Алгоритмы NumPy, написанные на
языке C, могут работать с этим блоком, не обременяя себя проверкой
типов и другими накладными расходами. Массивы NumPy потребляют
гораздо меньше памяти, чем встроенные в Python последовательности;
• в NumPy сложные операции применяются к массивам целиком, так что
циклы for не нужны.
Чтобы вы могли составить представление о выигрыше в производительности, рассмотрим массив NumPy, содержащий миллион чисел, и эквивалентный список Python:

NumPy ndarray: объект многомерного массива

107

In [7]: import numpy as np
In [8]: my_arr = np.arange(1000000)
In [9]: my_list = list(range(1000000))

Умножим каждую последовательность на 2:
In [10]: %time for _ in range(10): my_arr2 = my_arr * 2
CPU times: user 20 ms, sys: 50 ms, total: 70 ms
Wall time: 72.4 ms
In [11]: %time for _ in range(10): my_list2 = [x * 2 for x in my_list]
CPU times: user 760 ms, sys: 290 ms, total: 1.05 s
Wall time: 1.05 s

Алгоритмы, основанные на NumPy, в общем случае оказываются в 10–
100 раз (а то и больше) быстрее аналогов, написанных на чистом Python,
и потребляют гораздо меньше памяти.

4.1. NumPy ndarray: объект многомерного массива
Одна из ключевых особенностей NumPy – объект ndarray для представления
N-мерного массива. Это быстрый и гибкий контейнер для хранения больших
наборов данных в Python. Массивы позволяют выполнять математические
операции над целыми блоками данных, применяя такой же синтаксис, как
для соответствующих операций над скалярами.
Чтобы показать, как NumPy позволяет производить пакетные вычисления,
применяя такой же синтаксис, как для встроенных в Python скалярных объектов, я начну с импорта NumPy и генерации небольшого массива случайных
данных:
In [12]: import numpy as np
# Сгенерировать случайные данные
In [13]: data = np.random.randn(2, 3)
In [14]: data
Out[14]:
array([[–0.2047, 0.4789, –0.5194],
[–0.5557, 1.9658, 1.3934]])

Затем произведу математические операции над data:
In [15]: data * 10
Out[15]:
array([[ –2.0471, 4.7894, –5.1944],
[ –5.5573, 19.6578, 13.9341]])
In [16]: data + data
Out[16]:
array([[–0.4094, 0.9579, –1.0389],
[–1.1115, 3.9316, 2.7868]])

108

Основы NumPy: массивы и векторные вычисления

В первом примере все элементы умножены на 10. Во втором примере соответственные элементы в каждой «ячейке» складываются.
В этой главе и во всей книге я буду придерживаться стандартного соглашения
NumPy и писать import numpy as np. Конечно, вы можете включить в свой код
предложение from numpy import *, чтобы не писать каждый раз np., но я рекомендую не брать это в привычку. Пространство имен numpy очень велико
и содержит ряд функций, имена которых совпадают с именами встроенных
в Python функций (например, min и max).

ndarray – это обобщенный многомерный контейнер для однородных данных, т. е. в нем могут храниться только элементы одного типа. У любого массива есть атрибут shape – кортеж, описывающий размер по каждому измерению, и атрибут dtype – объект, описывающий тип данных в массиве:
In [17]: data.shape
Out[17]: (2, 3)
In [18]: data.dtype
Out[18]: dtype('float64')

В этой главе вы познакомитесь с основами работы с массивами NumPy
в объеме, достаточном для чтения книги. Для многих аналитических приложений глубокое понимание NumPy необязательно, но овладение стилем
мышления и методами программирования, ориентированными на массивы, – ключевой этап на пути становления эксперта по применению Python
в научных приложениях.
Слова «массив», «массив NumPy» и «ndarray» в этой книге почти всегда означают одно и то же – объект ndarray.

Создание ndarray
Проще всего создать массив с помощью функции array. Она принимает
любой объект, похожий на последовательность (в том числе другой массив),
и порождает новый массив NumPy, содержащий переданные данные. Например, такое преобразование можно проделать со списком:
In [19]: data1 = [6, 7.5, 8, 0, 1]
In [20]: arr1 = np.array(data1)
In [21]: arr1
Out[21]: array([ 6. , 7.5, 8. , 0. , 1. ])

Вложенные последовательности, например список списков одинаковой
длины, можно преобразовать в многомерный массив:
In [22]: data2 = [[1, 2, 3, 4], [5, 6, 7, 8]]

Powered by TCPDF (www.tcpdf.org)

NumPy ndarray: объект многомерного массива

109

In [23]: arr2 = np.array(data2)
In [24]: arr2
Out[24]:
array([[1, 2, 3, 4],
[5, 6, 7, 8]])

Поскольку data2 – список списков, массив NumPy arr2 имеет два измерения,
а его форма выведена из данных. В этом легко убедиться, прочитав атрибуты
ndim и shape:
In [25]: arr2.ndim
Out[25]: 2
In [26]: arr2.shape
Out[26]: (2, 4)

Если явно не задано противное (подробнее об этом ниже), то функция
np.array пытается самостоятельно определить подходящий тип данных для
создаваемого массива. Этот тип данных хранится в специальном объекте
dtype. Например, в примерах выше имеем:
In [27]: arr1.dtype
Out[27]: dtype('float64')
In [28]: arr2.dtype
Out[28]: dtype('int64')

Помимо np.array существует еще ряд функций для создания массивов. Например, zeros и ones создают массивы заданной длины, состоящие из нулей
и единиц соответственно, а shape.empty создает массив, не инициализируя
его элементы. Для создания многомерных массивов нужно передать кортеж,
описывающий форму:
In [29]: np.zeros(10)
Out[29]: array([ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
In [30]:
Out[30]:
array([[
[
[

np.zeros((3, 6))
0., 0., 0., 0., 0., 0.],
0., 0., 0., 0., 0., 0.],
0., 0., 0., 0., 0., 0.]])

In [31]: np.empty((2, 3, 2))
Out[31]:
array([[[ 0., 0.],
[ 0., 0.],
[ 0., 0.]],
[[ 0., 0.],
[ 0., 0.],
[ 0., 0.]]])

110

Основы NumPy: массивы и векторные вычисления
Предполагать, что np.empty возвращает массив из одних нулей, небезопасно.
Часто возвращается массив, содержащий неинициализированный мусор, как
в примере выше.

Функция arange – вариант встроенной в Python функции range, только возвращаемым значением является массив:
In [32]: np.arange(15)
Out[32]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])

В табл. 4.1 приведен краткий список стандартных функций создания массива. Поскольку NumPy ориентирован прежде всего на численные расчеты,
тип данных, если он не указан явно, во многих случаях предполагается float64
(числа с плавающей точкой).
Таблица 4.1. Функции создания массива
Функция

Описание

array

Преобразует входные данные (список, кортеж, массив или любую другую
последовательность) в ndarray. Тип dtype задается явно или выводится неявно.
Входные данные по умолчанию копируются
Преобразует входные данные в ndarray, но не копирует, если на вход уже подан
ndarray
Аналогична встроенной функции range, но возвращает массив, а не список
Порождает массив, состоящий из одних единиц, с заданными атрибутами shape
и dtype. Функция ones_like принимает другой массив и порождает массив из одних
единиц с такими же значениями shape и dtype
Аналогичны ones и ones_like, только порождаемый массив состоит из одних нулей
Создают новые массивы, выделяя под них память, но, в отличие от ones и zeros,
не инициализируют ее
Создают массивы с заданными атрибутами shape и dtype, в которых все элементы
равны заданному символу-заполнителю. full_like принимает массив и порождает
заполненный массив с такими же значениями атрибутов shape и dtype
Создают единичную квадратную матрицу N×N (элементы на главной диагонали
равны 1, все остальные – 0)

asarray
arange
ones, ones_like

zeros, zeros_like
empty, empty_like
full, full_like

eye, identity

Тип данных для ndarray
Тип данных, или dtype, – это специальный объект, который содержит информацию (метаданные), необходимую ndarray для интерпретации содержимого
блока памяти:
In [33]: arr1 = np.array([1, 2, 3], dtype=np.float64)
In [34]: arr2 = np.array([1, 2, 3], dtype=np.int32)
In [35]: arr1.dtype
Out[35]: dtype('float64')
In [36]: arr2.dtype
Out[36]: dtype('int32')

NumPy ndarray: объект многомерного массива

111

Объектам dtype NumPy в значительной мере обязан своей эффективностью
и гибкостью. В большинстве случаев они точно соответствуют внутреннему
машинному представлению, что позволяет без труда читать и записывать
двоичные потоки данных на диск, а также обмениваться данными с кодом,
написанным на языке низкого уровня типа C или Fortran. Числовые dtype
именуются единообразно: имя типа, например float или int, затем число,
указывающее разрядность одного элемента. Стандартное значение с плавающей точкой двойной точности (хранящееся во внутреннем представлении
объекта Python типа float) занимает 8 байтов, или 64 бита. Поэтому соответствующий тип в NumPy называется float64. В табл. 4.2 приведен полный
список поддерживаемых NumPy типов данных.
Не пытайтесь сразу запомнить все типы данных NumPy, особенно если вы
только приступаете к его изучению. Часто нужно заботиться лишь об общем
виде обрабатываемых данных, например о числах с плавающей точкой, о комплексных, целых, булевых значениях, о строках или общих объектах Python.
Если необходим более точный контроль над тем, как данные хранятся в памяти или на диске, особенно когда речь идет о больших наборах данных, то
знать о возможности такого контроля полезно.

Таблица 4.2. Типы данных NumPy
Функция

Код типа Описание

int8, uint8
int16, uint16
int32, uint32
int64, uint64
float16
float32

i1, u1
i2, u2
i4, u4
i8, u8
f2
f4

float64

f8 или d

float128

f16

complex64,
complex128,
complex256
bool
object
string_

c8, c16,
c32
?
O
S

unicode_

U

Знаковое и беззнаковое 8-разрядное (1 байт) целое
Знаковое и беззнаковое 16-разрядное (2 байта) целое
Знаковое и беззнаковое 32-разрядное (4 байта) целое
Знаковое и беззнаковое 64-разрядное (8 байт) целое
С плавающей точкой половинной точности
Стандартный тип с плавающей точкой одинарной точности.
Совместим с типом C float
Стандартный тип с плавающей точкой двойной точности.
Совместим с типом C double и с типом Python float
С плавающей точкой расширенной точности
Комплексные числа, вещественная и мнимая части которых представлены
соответственно типами float32, float64 и float128
Булев тип, способный хранить значения True и False
Тип объекта Python
Тип ASCII-строки фиксированной длины (1 байт на символ).
Например, строка длиной 10 имеет тип 'S10'
Тип Unicode-строки фиксированной длины (количество байтов на символ
зависит от платформы). Семантика такая же, как у типа string_ (например, 'U10')

Можно явно преобразовать, или привести, массив одного типа к другому,
воспользовавшись методом astype:
In [37]: arr = np.array([1, 2, 3, 4, 5])
In [38]: arr.dtype

112

Основы NumPy: массивы и векторные вычисления

Out[38]: dtype('int64')
In [39]: float_arr = arr.astype(np.float64)
In [40]: float_arr.dtype
Out[40]: dtype('float64')

Здесь целые были приведены к типу с плавающей точкой. Если бы я попытался привести числа с плавающей точкой к целому типу, то дробная часть
была бы отброшена:
In [41]: arr = np.array([3.7, –1.2, –2.6, 0.5, 12.9, 10.1])
In [42]: arr
Out[42]: array([ 3.7, –1.2, –2.6, 0.5, 12.9, 10.1])
In [43]: arr.astype(np.int32)
Out[43]: array([ 3, –1, –2, 0, 12, 10], dtype=int32)

Если имеется массив строк, представляющих целые числа, то astype позволит преобразовать их в числовую форму:
In [44]: numeric_strings = np.array(['1.25', '–9.6', '42'], dtype=np.string_)
In [45]: numeric_strings.astype(float)
Out[45]: array([ 1.25, –9.6 , 42. ])
Будьте осторожнее при работе с типом numpy.string_, поскольку в NumPy размер строковых данных фиксирован и входные данные могут быть обрезаны
без предупреждения. Поведение pandas для нечисловых данных лучше согласуется с интуицией.

Если по какой-то причине выполнить приведение не удастся (например,
если строку нельзя преобразовать в тип float64), то будет возбуждено исключение TypeError. Обратите внимание, что в примере выше я поленился
и написал float вместо np.float64, но NumPy оказался достаточно умным – он
умеет подменять типы Python эквивалентными dtype.
Можно также использовать атрибут dtype другого массива:
In [46]: int_array = np.arange(10)
In [47]: calibers = np.array([.22, .270, .357, .380, .44, .50], dtype=np.float64)
In [48]: int_array.astype(calibers.dtype)
Out[48]: array([ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9.])

На dtype можно сослаться с помощью коротких кодов типа:
In [49]: empty_uint32 = np.empty(8, dtype='u4')
In [50]: empty_uint32
Out[50]:
array([
0, 1075314688,
0, 1075707904,
1075838976,
0, 1072693248], dtype=uint32)

0,

NumPy ndarray: объект многомерного массива

113

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

Арифметические операции с массивами NumPy
Массивы важны, потому что позволяют выразить операции над совокупностями данных без выписывания циклов for. Обычно это называется векторизацией. Любая арифметическая операция над массивами одинакового
размера применяется к соответственным элементам:
In [51]: arr = np.array([[1., 2., 3.], [4., 5., 6.]])
In [52]: arr
Out[52]:
array([[ 1., 2., 3.],
[ 4., 5., 6.]])
In [53]: arr * arr
Out[53]:
array([[ 1., 4., 9.],
[ 16., 25., 36.]])
In [54]: arr – arr
Out[54]:
array([[ 0., 0., 0.],
[ 0., 0., 0.]])

Как легко догадаться, арифметические операции, в которых участвует скаляр, применяются к каждому элементу массива:
In [55]: 1 / arr
Out[55]:
array([[ 1. , 0.5 , 0.3333],
[ 0.25 , 0.2 , 0.1667]])
In [56]: arr ** 0.5
Out[56]:
array([[ 1. , 1.4142, 1.7321],
[ 2. , 2.2361, 2.4495]])

Сравнение массивов одинакового размера дает булев массив:
In [57]: arr2 = np.array([[0., 4., 1.], [7., 2., 12.]])
In [58]: arr2
Out[58]:
array([[ 0., 4., 1.],
[ 7., 2., 12.]])
In [59]: arr2 > arr
Out[59]:

114

Основы NumPy: массивы и векторные вычисления

array([[False, True, False],
[ True, False, True]], dtype=bool)

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

Индексирование и вырезание
Индексирование массивов NumPy – обширная тема, поскольку подмножество массива или его отдельные элементы можно выбрать различными
способами. С одномерными массивами все просто. На поверхностный взгляд
они ведут себя как списки Python:
In [60]: arr = np.arange(10)
In [61]: arr
Out[61]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
In [62]: arr[5]
Out[62]: 5
In [63]: arr[5:8]
Out[63]: array([5, 6, 7])
In [64]: arr[5:8] = 12
In [65]: arr
Out[65]: array([ 0, 1, 2, 3, 4, 12, 12, 12, 8, 9])

Как видите, если присвоить скалярное значение срезу, как в arr[5:8] = 12,
то оно распространяется (или укладывается) на весь срез. Важнейшее отличие от списков состоит в том, что срез массива является представлением
исходного массива. Это означает, что данные на самом деле не копируются,
а любые изменения, внесенные в представление, попадают и в исходный
массив.
Для демонстрации я сначала создам срез массива arr:
In [66]: arr_slice = arr[5:8]
In [67]: arr_slice
Out[67]: array([12, 12, 12])

Если теперь изменить значения в arr_slice, то изменения отразятся и на
исходном массиве arr:
In [68]: arr_slice[1] = 12345
In [69]: arr
Out[69]: array([ 0, 1, 2, 3, 4, 12, 12345, 12, 8, 9])

Присваивание неуточненному срезу [:] приводит к записи значения во
все элементы массива:

115

NumPy ndarray: объект многомерного массива
In [70]: arr_slice[:] = 64
In [71]: arr
Out[71]: array([ 0, 1, 2, 3, 4, 64, 64, 64, 8, 9])

При первом знакомстве с NumPy это может стать неожиданностью, особенно если вы привыкли к программированию массивов в других языках, где
копирование данных применяется чаще. Но NumPy проектировался для работы с большими массивами данных, поэтому при безудержном копировании
данных неизбежно возникли бы проблемы с быстродействием и памятью.
Чтобы получить копию, а не представление среза массива, нужно выполнить
операцию копирования явно, например arr[5:8].copy().

Для массивов большей размерности и вариантов больше. В случае двумерного массива результатом индексирования с одним индексом является не
скаляр, а одномерный массив:
In [72]: arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
In [73]: arr2d[2]
Out[73]: array([7, 8, 9])

К отдельным элементам можно обращаться рекурсивно. Но это слишком
громоздко, поэтому для выбора одного элемента можно указать список индексов через запятую. Таким образом, следующие две конструкции эквивалентны:
In [74]: arr2d[0][2]
Out[74]: 3
In [75]: arr2d[0, 2]
Out[75]: 3

Рисунок 4.1 иллюстрирует индексирование двумерного массива. Лично
мне удобно представлять ось 0 как «строки» массива, а ось 1 – как «столбцы».
ось 1

ось 0

Рис. 4.1. Индексирование элементов в массиве NumPy

116

Основы NumPy: массивы и векторные вычисления

Если при работе с многомерным массивом опустить несколько последних
индексов, то будет возвращен объект ndarray меньшей размерности, содержащий данные по указанным при индексировании осям. Так, пусть имеется
массив arr3d размерности 2×2×3:
In [76]: arr3d = np.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]])
In [77]: arr3d
Out[77]:
array([[[ 1, 2, 3],
[ 4, 5, 6]],
[[ 7, 8, 9],
[10, 11, 12]]])

Тогда arr3d[0] – массив размерности 2×3:
In [78]: arr3d[0]
Out[78]:
array([[1, 2, 3],
[4, 5, 6]])

Выражению arr3d[0] можно присвоить как скалярное значение, так и массив:
In [79]: old_values = arr3d[0].copy()
In [80]: arr3d[0] = 42
In [81]: arr3d
Out[81]:
array([[[42, 42, 42],
[42, 42, 42]],
[[ 7, 8, 9],
[10, 11, 12]]])
In [82]: arr3d[0] = old_values
In [83]: arr3d
Out[83]:
array([[[ 1, 2, 3],
[ 4, 5, 6]],
[[ 7, 8, 9],
[10, 11, 12]]])

Аналогично arr3d[1, 0] дает все значения, список индексов которых начинается с (1, 0), т. е. одномерный массив:
In [84]: arr3d[1, 0]
Out[84]: array([7, 8, 9])

Результат такой же, как если бы мы индексировали в два приема:
In [85]: x = arr3d[1]
In [86]: x

NumPy ndarray: объект многомерного массива

117

Out[86]:
array([[ 7, 8, 9],
[10, 11, 12]])
In [87]: x[0]
Out[87]: array([7, 8, 9])

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

Индексирование срезами
Как и для одномерных объектов наподобие списков Python, для объектов
ndarray можно формировать срезы:
In [88]: arr
Out[88]: array([ 0, 1, 2, 3, 4, 64, 64, 64, 8, 9])
In [89]: arr[1:6]
Out[89]: array([ 1, 2, 3, 4, 64])

Рассмотрим приведенный выше двумерный массив arr2d. Применение
к нему вырезания дает несколько иной результат:
In [90]: arr2d
Out[90]:
array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
In [91]: arr2d[:2]
Out[91]:
array([[1, 2, 3],
[4, 5, 6]])

Как видите, вырезание производится вдоль оси 0, первой оси. Поэтому срез
содержит диапазон элементов вдоль этой оси. Выражение arr2d[:2] полезно
читать так: «выбрать первые две строки arr2d».
Можно указать несколько срезов – как несколько индексов:
In [78]: arr2d[:2, 1:]
Out[78]:
array([[2, 3],
[5, 6]])

При таком вырезании мы всегда получаем представления массивов с таким же числом измерений, как у исходного. Сочетая срезы и целочисленные
индексы, можно получить массивы меньшей размерности.
Например, я могу выбрать вторую строку, а в ней только первые два столбца:
In [93]: arr2d[1, :2]
Out[93]: array([4, 5])

118

Основы NumPy: массивы и векторные вычисления

Аналогично я могу выбрать третий столбец, а в нем только первые две
строки:
In [94]: arr2d[:2, 2]
Out[94]: array([3, 6])

Иллюстрация приведена на рис. 4.2. Отметим, что двоеточие без указания
числа означает, что нужно взять всю ось целиком, поэтому для получения
осей только высших размерностей можно поступить следующим образом:
In [95]: arr2d[:, :1]
Out[95]:
array([[1],
[4],
[7]])

Разумеется, присваивание выражению-срезу означает присваивание всем
элементам этого среза:
In [96]: arr2d[:2, 1:] = 0
In [97]: arr2d
Out[97]:
array([[1, 0, 0],
[4, 0, 0],
[7, 8, 9]])

Выражение

Форма

Рис. 4.2. Вырезание из двумерного массива

NumPy ndarray: объект многомерного массива

119

Булево индексирование
Пусть имеется некоторый массив с данными и массив имен, содержащий
дубликаты. Я хочу воспользоваться функцией randn из модуля numpy.random,
чтобы сгенерировать случайные данные с нормальным распределением:
In [98]: names = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])
In [99]: data = np.random.randn(7, 4)
In [100]: names
Out[100]:
array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'],
dtype='[100,1])
/obj1_col

series

(shape–>[100])

/obj2
[index])

frame_table (typ–>appendable,nrows–>100,ncols–>1,indexers–>

/obj3
[index])

frame_table (typ–>appendable,nrows–>100,ncols–>1,indexers–>

Объекты из HDF5-файла можно извлекать как из словаря:
In [97]: store['obj1']
Out[97]:
a
0 –0.204708
1 0.478943
2 –0.519439
3 –0.555730
4 1.965781
.. ...
95 0.795253
96 0.118110
97 –0.748532
98 0.584970
99 0.152677
[100 rows x 1 columns]

HDFStore поддерживает две схемы хранения: 'fixed' и 'table'. Последняя,
вообще говоря, медленнее, но поддерживает запросы в специальном синтаксисе:
In [98]: store.put('obj2', frame, format='table')
In [99]: store.select('obj2', where=['index >= 10 and index 1 val.index(':')
ValueError: substring not found
Метод count возвращает количество вхождений подстроки:
In [219]: val.count(',')
Out[219]: 2

Метод replace заменяет вхождения образца указанной строкой. Он же применяется для удаления подстрок – достаточно в качестве заменяющей передать пустую строку:
In [146]: val.replace(',', '::')
Out[146]: 'a::b:: guido'

234

Очистка и подготовка данных

In [147]: val.replace(',', '')
Out[147]: 'ab guido'

В табл. 7.3 перечислены некоторые методы работы со строками в Python.
Как мы вскоре увидим, во многих таких операциях можно использовать
также регулярные выражения.
Таблица 7.3. Встроенные в Python методы строковых объектов
Метод

Описание

count
endswith, startswith
join

Возвращает количество неперекрывающихся вхождений подстроки в строку
Возвращает True, если строка оканчивается (начинается) указанной подстрокой
Использовать данную строку как разделитель при конкатенации
последовательности других строк
Возвращает позицию первого символа подстроки в строке. Если подстрока
не найдена, возбуждает исключение ValueError
Возвращает позицию первого символа первого вхождения подстроки в строку,
как и index. Но если строка не найдена, то возвращает –1
Возвращает позицию первого символа последнего вхождения подстроки
в строку. Если строка не найдена, то возвращает –1
Заменяет вхождения одной строки другой строкой
Удаляет пробельные символы, в том числе символы новой строки в начале
и (или) конце строки
Разбивает строку на список подстрок по указанному разделителю
Преобразует буквы в нижний регистр
Преобразует буквы в верхний регистр
Выравнивает строку по левой или правой границе соответственно. Противоположный конец строки заполняется пробелами (или каким-либо другим символом), так чтобы получилась строка как минимум заданной длины

index
find
rfind
replace
strip, rstrip,
lstrip
split
lower
upper
ljust, rjust

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

Функции из модуля re можно отнести к трем категориям: сопоставление с образцом, замена и разбиение. Естественно, все они взаимосвязаны;
регулярное выражение описывает образец, который нужно найти в тексте,
а затем его уже можно применять для разных целей. Рассмотрим простой
пример: требуется разбить строку в тех местах, где имеется сколько-то пробельных символов (пробелов, знаков табуляции и знаков новой строки). Для

235

Манипуляции со строками

сопоставления с одним или несколькими пробельными символами служит
регулярное выражение \s+:
In [148]: import re
In [149]: text = "foo

bar\t baz \tqux"

In [150]: re.split('\s+', text)
Out[150]: ['foo', 'bar', 'baz', 'qux']

При обращении re.split('\s+', text) сначала компилируется регулярное выражение, а затем его методу split передается заданный текст. Можно просто откомпилировать регулярное выражение методом re.compile, создав тем
самым объект, допускающий повторное использование:
In [151]: regex = re.compile('\s+')
In [152]: regex.split(text)
Out[152]: ['foo', 'bar', 'baz', 'qux']

Чтобы получить список всех подстрок, отвечающих данному регулярному
выражению, следует воспользоваться методом findall:
In [153]: regex.findall(text)
Out[153]: [' ', '\t ', ' \t']
Чтобы не прибегать к громоздкому экранированию знаков \ в регулярном выражении, пользуйтесь примитивными (raw) строковыми литералами, например
r'C:\x' вместо 'C:\\x'.

Создавать объект регулярного выражения с помощью метода re.compile рекомендуется, если вы планируете применять одно и то же выражение к нескольким строкам, при этом экономится процессорное время.
С findall тесно связаны методы match и search. Если findall возвращает все
найденные в строке соответствия, то search – лишь первое. А метод match находит только соответствие, начинающееся в начале строки. В качестве не
столь тривиального примера рассмотрим блок текста и регулярное выражение, распознающее большинство адресов электронной почты:
text = """Dave dave@google.com
Steve steve@gmail.com
Rob rob@gmail.com
Ryan ryan@yahoo.com
"""
pattern = r'[A–Z0–9._%+–]+@[A–Z0–9.–]+\.[A–Z]{2,4}'
# Флаг re.IGNORECASE делает регулярное выражение нечувствительным к регистру
regex = re.compile(pattern, flags=re.IGNORECASE)

Применение метода findall к этому тексту порождает список почтовых
адресов:
In [155]: regex.findall(text)

236

Очистка и подготовка данных

Out[155]:
['dave@google.com',
'steve@gmail.com',
'rob@gmail.com',
'ryan@yahoo.com']

Метод search возвращает специальный объект соответствия для первого
встретившегося в тексте адреса. В нашем случае этот объект может сказать
только о начальной и конечной позициях найденного в строке образца:
In [156]: m = regex.search(text)
In [157]: m
Out[157]:
In [158]: text[m.start():m.end()]
Out[159]: 'dave@google.com'

Метод regex.match возвращает None, потому что он находит соответствие образцу только в начале строки:
In [159]: print regex.match(text)
None

Метод sub возвращает новую строку, в которой вхождения образца заменены указанной строкой:
In [160]: print regex.sub('REDACTED', text)
Dave REDACTED
Steve REDACTED
Rob REDACTED
Ryan REDACTED

Предположим, что мы хотим найти почтовые адреса и в то же время разбить каждый адрес на три компонента: имя пользователя, имя домена и суффикс домена. Для этого заключим соответствующие части образца в скобки:
In [161]: pattern = r'([A–Z0–9._%+–]+)@([A–Z0–9.–]+)\.([A–Z]{2,4})'
In [162]: regex = re.compile(pattern, flags=re.IGNORECASE)

Метод groups объекта соответствия, порожденного таким модифицированным регулярным выражением, возвращает кортеж компонентов образца:
In [163]: m = regex.match('wesm@bright.net')
In [164]: m.groups()
Out[164]: ('wesm', 'bright', 'net')

Если в образце есть группы, то метод findall возвращает список кортежей:
In [165]: regex.findall(text)
Out[165]:
[('dave', 'google', 'com'),

Манипуляции со строками

237

('steve', 'gmail', 'com'),
('rob', 'gmail', 'com'),
('ryan', 'yahoo', 'com')]

Метод sub тоже имеет доступ к группам в каждом найденном соответствии
с помощью специальных конструкций \1, \2 и т. д.:
In [166]: print regex.sub(r'Username: \1, Domain: \2, Suffix: \3', text)
Dave Username: dave, Domain: google, Suffix: com
Steve Username: steve, Domain: gmail, Suffix: com
Rob Username: rob, Domain: gmail, Suffix: com
Ryan Username: ryan, Domain: yahoo, Suffix: com

О регулярных выражениях в Python можно рассказывать еще долго, но
бóльшая часть этого материала выходит за рамки данной книги. В табл. 7.4
приведена краткая сводка методов.
Таблица 7.4. Методы регулярных выражений
Метод

Описание

findall
finditer
match

Возвращает список всех непересекающихся образцов, найденных в строке
Аналогичен findall, но возвращает итератор
Ищет соответствие образцу в начале строки и факультативно выделяет в образце группы.
Если образец найден, возвращает объект соответствия, иначе None
Ищет в строке образец; если найден, возвращает объект соответствия. В отличие от match,
образец может находиться в любом месте строки, а не только в начале
Разбивает строку на части в местах вхождения образца
Заменяет все (sub) или только первые n (subn) вхождений образца указанной строкой.
Чтобы в указанной строке сослаться на группы, выделенные в образце, используйте
конструкции \1, \2, ...

search
split
sub, subn

Векторные строковые функции в pandas
Очистка замусоренного набора данных для последующего анализа подразумевает значительный объем манипуляций со строками и использование
регулярных выражений. А чтобы жизнь не казалась медом, в столбцах, содержащих строки, иногда встречаются отсутствующие значения:
In [167]: data = {'Dave': 'dave@google.com', 'Steve': 'steve@gmail.com',
.....:
'Rob': 'rob@gmail.com', 'Wes': np.nan}
In [168]: data = pd.Series(data)
In [169]: data
Out[169]:
Dave
dave@google.com
Rob
rob@gmail.com
Steve
steve@gmail.com
Wes
NaN
dtype: object

238

Очистка и подготовка данных

In [170]: data.isnull()
Out[170]:
Dave
False
Rob
False
Steve
False
Wes
True
dtype: bool

Методы строк и регулярных выражений можно применить к каждому значению с помощью метода data.map (которому передается лямбда или другая
функция), но для отсутствующих значений они «грохнутся». Чтобы справиться с этой проблемой, в классе Series есть методы для операций со строками,
которые пропускают отсутствующие значения. Доступ к ним производится
через атрибут str; например, вот как можно было бы с помощью метода str.
contains проверить, содержит ли каждый почтовый адрес подстроку 'gmail':
In [171]: data.str.contains('gmail')
Out[171]:
Dave
False
Rob
True
Steve
True
Wes
NaN
dtype: object

Регулярные выражения тоже можно так использовать, равно как и их флаги
типа IGNORECASE:
In [172]: pattern
Out[172]: '([A–Z0–9._%+–]+)@([A–Z0–9.–]+)\\.([A–Z]{2,4})'
In [173]: data.str.findall(pattern, flags=re.IGNORECASE)
Out[173]:
Dave
[('dave', 'google', 'com')]
Rob
[('rob', 'gmail', 'com')]
Steve
[('steve', 'gmail', 'com')]
Wes
NaN
dtype: object

Существует два способа векторной выборки элементов: str.get или доступ
к атрибуту str по индексу:
In [174]: matches = data.str.match(pattern, flags=re.IGNORECASE)
In [175]: matches
Out[175]:
Dave
True
Rob
True
Steve True
Wes
NaN
dtype: object

Манипуляции со строками

239

Для доступа к элементам списков мы можем передать индекс любой из
этих функций:
In [176]: matches.str.get(1)
Out[176]:
Dave
NaN
Rob
NaN
Steve NaN
Wes
NaN
dtype: float64
In [177]: matches.str[0]
Out[177]:
Dave
NaN
Rob
NaN
Steve NaN
Wes
NaN
dtype: float64

Аналогичный синтаксис позволяет вырезать строки:
In [178]:
Out[178]:
Dave
Rob
Steve
Wes

data.str[:5]
dave@
rob@g
steve
NaN

В табл. 7.5 перечислены дополнительные методы строк в pandas.
Таблица 7.5. Неполный перечень векторных методов строковых объектов
Метод

Описание

cat
contains
count
extract

Поэлементно конкатенирует строки с необязательным разделителем
Возвращает булев массив, показывающий, содержит ли каждая строка указанный образец
Подсчитывает количество вхождений образца
Использует регулярное выражение с группами, чтобы выделить одну или несколько
строк из объекта Series, содержащего строки; результатом является DataFrame,
содержащий по одному столбцу на каждую группу
Эквивалентно x.endswith(pattern) для каждого элемента
Эквивалентно x.startswith(pattern) для каждого элемента
Возвращает список всех вхождений образца для каждой строки
Доступ по индексу ко всем элементам (выбрать i-й элемент)
Эквивалентно встроенному методу str.isalnum
Эквивалентно встроенному методу str.isalpha
Эквивалентно встроенному методу str.isdecimal
Эквивалентно встроенному методу str.isdigit
Эквивалентно встроенному методу str.islower
Эквивалентно встроенному методу str.isnumeric

endswith
startswith
findall
get
isalnum
isalpha
isdecimal
isdigit
islower
isnumeric

240

Очистка и подготовка данных

Таблица 7.5 (окончание)
Метод

Описание

isupper
join
len

Эквивалентно встроенному методу str.isupper
Объединяет строки в каждом элементе Series, вставляя между ними указанный разделитель
Вычисляет длину каждой строки
Преобразование регистра; эквивалентно x.lower() или x.upper() для каждого элемента
Вызывает re.match с указанным регулярным выражением для каждого элемента,
возвращает список выделенных групп
Дополняет строки пробелами слева, справа или с обеих сторон
Эквивалентно pad(side='both')
Дублирует значения; например, s.str.repeat(3) эквивалентно x * 3 для каждой строки
Заменяет вхождения образца указанной строкой
Вырезает каждую строку в объекте Series
Разбивает строки по разделителю или по регулярному выражению
Убирает пробельные символы, в том числе знак новой строки, с обеих сторон строки
Убирает пробельные символы справа
Убирает пробельные символы слева

lower, upper
match
pad
center
repeat
replace
slice
split
strip
rstrip
lstrip

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

Глава 8. Переформатирование
данных: соединение,
комбинирование и изменение
формы
Во многих приложениях бывает, что данные разбросаны по многим файлам
или базам данных либо организованы так, что их трудно проанализировать.
Эта глава посвящена средствам комбинирования, соединения и реорганизации данных.
Сначала познакомимся с концепцией иерархического индексирования в pandas, которая широко применяется в некоторых из описываемых далее операций, а затем перейдем к деталям конкретных манипуляций данными. В главе 14 будут продемонстрированы различные применения этих средств.

8.1. Иерархическое индексирование
Иерархическое индексирование – важная особенность pandas, позволяющая организовать несколько (два и более) уровней индексирования по одной оси. Говоря абстрактно, это способ работать с многомерными данными, представив
их в форме с меньшей размерностью. Начнем с простого примера – создадим
объект Series с индексом в виде списка списков или массивов:
In [9]: data = pd.Series(np.random.randn(9),
...: index=[['a', 'a', 'a', 'b', 'b', 'c', 'c', 'd', 'd'],
...: [1, 2, 3, 1, 3, 1, 2, 2, 3]])
In [10]: data

242

Переформатирование данных: соединение, комбинирование и изменение формы

Out[10]:
a 1 –0.204708
2 0.478943
3 –0.519439
b 1 –0.555730
3 1.965781
c 1 1.393406
2 0.092908
d 2 0.281746
3 0.769023
dtype: float64

Здесь мы видим отформатированное представление Series с мультииндексом (MultiIndex). Разрывы в представлении индекса означают «взять значение
вышестоящей метки».
In [11]: data.index
Out[11]:
MultiIndex(levels=[['a', 'b', 'c', 'd'], [1, 2, 3]],
labels=[[0, 0, 0, 1, 1, 2, 2, 3, 3], [0, 1, 2, 0, 2, 0, 1, 1, 2]])

Для иерархически индексированного объекта возможен доступ по так называемому частичному индексу, что позволяет лаконично записывать выборку подмножества данных:
In [12]: data['b']
Out[12]:
1 –0.555730
3 1.965781
dtype: float64
In [13]: data['b':'c']
Out[13]:
b 1 –0.555730
3 1.965781
c 1 1.393406
2 0.092908
dtype: float64
In [14]: data.loc[['b', 'd']]
Out[14]:
b 1 –0.555730
3 1.965781
d 2 0.281746
3 0.769023
dtype: float64

В некоторых случаях возможна даже выборка с «внутреннего» уровня:
In [15]: data.loc[:, 2]

Иерархическое индексирование

243

Out[15]:
a 0.478943
c 0.092908
d 0.281746
dtype: float64

Иерархическое индексирование играет важнейшую роль в изменении
формы данных и групповых операциях, в том числе в построении сводных
таблиц. Например, эти данные можнобыло бы преобразовать в DataFrame
с помощью метода unstack:
In [16]: data.unstack()
Out[16]:
1
2
3
a –0.204708 0.478943 –0.519439
b –0.555730
NaN 1.965781
c 1.393406 0.092908
NaN
d
NaN 0.281746 0.769023

Обратной к unstack операцией является stack:
In [17]: data.unstack().stack()
Out[17]:
a 1 –0.204708
2 0.478943
3 –0.519439
b 1 –0.555730
3 1.965781
c 1 1.393406
2 0.092908
d 2 0.281746
3 0.769023
dtype: float64

Методы stack и unstack будут подробно рассмотрены в главе 7.
В случае DataFrame иерархический индекс может существовать для любой
оси:
In [18]: frame = pd.DataFrame(np.arange(12).reshape((4, 3)),
.....:
index=[['a', 'a', 'b', 'b'], [1, 2, 1, 2]],
.....:
columns=[['Ohio', 'Ohio', 'Colorado'],
.....:
['Green', 'Red', 'Green']])
In [19]: frame
Out[19]:
Ohio
Colorado
Green Red
Green
a 1
0
1
2
2
3
4
5
b 1
6
7
8
2
9 10
11

244

Переформатирование данных: соединение, комбинирование и изменение формы

Уровни иерархии могут иметь имена (как строки или любые объекты Python). В таком случае они будут показаны при выводе на консоль (не путайте
имена индексов с метками на осях!):
In [20]: frame.index.names = ['key1', 'key2']
In [21]: frame.columns.names = ['state', 'color']
In [22]: frame
Out[22]:
state
Ohio
Colorado
color Green Red
Green
key1 key2
a 1
0
1
2
2
3
4
5
b 1
6
7
8
2
9
10
11
Не путайте имена индексов 'state' и 'color' с метками строк.

Доступ по частичному индексу, как и раньше, позволяет выбирать группы
столбцов:
In [23]: frame['Ohio']
Out[23]:
color Green Red
key1 key2
a 1
0
1
2
3
4
b 1
6
7
2
9 10

Мультииндекс можно создать отдельно, а затем использовать повторно;
в показанном выше объекте DataFrame столбцы с именами уровней можно
было бы создать так:
pd.MultiIndex.from_arrays([['Ohio', 'Ohio', 'Colorado'], ['Green', 'Red', 'Green']],
names=['state', 'color'])

Переупорядочение и уровни сортировки
Иногда требуется изменить порядок уровней на оси или отсортировать
данные по значениям на одном уровне. Метод swaplevel принимает номера
или имена двух уровней и возвращает новый объект, в котором эти уровни
переставлены (но во всех остальных отношениях данные не изменяются):
In [24]: frame.swaplevel('key1', 'key2')
Out[24]:
state
Ohio
Colorado
color Green Red
Green

Иерархическое индексирование
key2
1
2
1
2

key1
a
a
b
b

0
3
6
9

1
4
7
10

245

2
5
8
11

С другой стороны, метод sort_index выполняет сортировку данных, используя
только значения на одном уровне. После перестановки уровней обычно вызывают также sort_index, чтобы лексикографически отсортировать результат:
In [25]: frame.sort_index(level=1)
Out[25]:
state
Ohio
Colorado
color
Green Red
Green
key1 key2
a
1
0 1
2
b
1
6 7
8
a
2
3 4
5
b
2
9 10
11
In [26]: frame.swaplevel(0, 1).sort_index(level=0)
Out[26]:
state
Ohio
Colorado
color
Green Red
Green
key2 key1
1
a
0 1
2
b
6 7
8
2
a
3 4
5
b
9 10
11
Производительность выборки данных из иерархически индексированных объектов будет гораздо выше, если индекс отсортирован лексикографически начиная с самого внешнего уровня, т. е. в результате вызова sort_index(level=0)
или sort_index().

Сводная статистика по уровню
У многих методов объектов DataFrame и Series, вычисляющих сводные
и описательные статистики, имеется параметр level для задания уровня, на
котором требуется производить агрегирование по конкретной оси. Рассмотрим тот же объект DataFrame, что и выше; мы можем суммировать по уровню
для строк или для столбцов:
In [27]: frame.sum(level='key2')
Out[27]:
state
Ohio
Colorado
color Green Red
Green
key2
1
6
8
10
2
12
14
16

246

Переформатирование данных: соединение, комбинирование и изменение формы

In [28]: frame.sum(level='color', axis=1)
Out[28]:
color
Green
Red
key1 key2
a
1
2
1
2
8
4
b
1
14
7
2
20
10

Реализовано это с помощью имеющегося в pandas механизма groupby, который мы подробно рассмотрим позже.

Индексирование с помощью столбцов DataFrame
Не так уж редко возникает необходимость использовать один или несколько столбцов DataFrame в качестве индекса строк; альтернативно можно переместить индекс строк в столбцы DataFrame. Рассмотрим пример:
In [29]: frame = DataFrame({'a': range(7), 'b': range(7, 0, –1),
.....:
'c': ['one', 'one', 'one', 'two', 'two', 'two', 'two'],
.....:
'd': [0, 1, 2, 0, 1, 2, 3]})
In [30]:
Out[30]:
a b
0 0 7
1 1 6
2 2 5
3 3 4
4 4 3
5 5 2
6 6 1

frame
c
one
one
one
two
two
two
two

d
0
1
2
0
1
2
3

Метод set_index объекта DataFrame создает новый DataFrame, используя
в качестве индекса один или несколько столбцов:
In [31]: frame2 = frame.set_index(['c', 'd'])
In [32]: frame2
Out[32]:
a b
c d
one 0 0 7
1 1 6
2 2 5
two 0 3 4
1 4 3
2 5 2
3 6 1

Комбинирование и слияние наборов данных

247

По умолчанию столбцы удаляются из DataFrame, хотя их можно и оставить:
In [33]: frame.set_index(['c', 'd'], drop=False)
Out[33]:
a b c d
c d
one 0 0 7 one 0
1 1 6 one 1
2 2 5 one 2
two 0 3 4 two 0
1 4 3 two 1
2 5 2 two 2
3 6 1 two 3

Есть также метод reset_index, который делает прямо противоположное set_
index; уровни иерархического индекса перемещаются в столбцы:
In [34]: frame2.reset_index()
Out[34]:
c d a b
0 one 0 0 7
1 one 1 1 6
2 one 2 2 5
3 two 0 3 4
4 two 1 4 3
5 two 2 5 2
6 two 3 6 1

8.2. Комбинирование и слияние наборов данных
Данные, хранящиеся в объектах pandas, можно комбинировать различными
способами:
• метод pandas.merge соединяет строки объектов DataFrame по одному или
нескольким ключам. Эта операция хорошо знакома пользователям реляционных баз данных;
• метод pandas.concat склеивает объекты, располагая их в стопке вдоль оси;
• метод экземпляра combine_first позволяет сращивать перекрывающиеся
данные, чтобы заполнить отсутствующие в одном объекте данные значениями из другого объекта.
Я рассмотрю эти способы на многочисленных примерах. Мы будем неоднократно пользоваться ими в последующих главах.

Слияние объектов DataFrame как в базах данных
Операция слияния или соединения комбинирует наборы данных, соединяя
строки по одному или нескольким ключам. Эта операция является одной из

248

Переформатирование данных: соединение, комбинирование и изменение формы

основных в базах данных. Функция merge в pandas – портал ко всем алгоритмам такого рода.
Начнем с простого примера:
In [35]: df1 = pd.DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'a', 'b'],
....:
'data1': range(7)})
In [36]: df2 = pd.DataFrame({'key': ['a', 'b', 'd'],
....:
'data2': range(3)})
In [37]: df1
Out[37]:
data1 key
0
0 b
1
1 b
2
2 a
3
3 c
4
4 a
5
5 a
6
6 b
In [38]: df2
Out[38]:
data2 key
0
0 a
1
1 b
2
2 d

Это пример соединения типа многие-к-одному; в объекте df1 есть несколько
строк с метками a и b, а в df2 – только одна строка для каждого значения
в столбце key. Вызов merge для таких объектов дает:
In [39]: pd.merge(df1, df2)
Out[39]:
data1 key data2
0
0
b
1
1
1
b
1
2
6
b
1
3
2
a
0
4
4
a
0
5
5
a
0

Обратите внимание, что я не указал, по какому столбцу производить соединение. В таком случае merge использует в качестве ключей столбцы с одинаковыми именами. Однако рекомендуется все же указывать столбцы явно:
In [40]: pd.merge(df1, df2, on='key')
Out[40]:
data1 key data2
0
0
b
1
1
1
b
1
2
6
b
1

Комбинирование и слияние наборов данных
3
4
5

2
4
5

a
a
a

249

0
0
0

Если имена столбцов в объектах различаются, то можно задать их порознь:
In [41]: df3 = pd.DataFrame({'lkey': ['b', 'b', 'a', 'c', 'a', 'a', 'b'],
....:
'data1': range(7)})
In [42]: df4 = pd.DataFrame({'rkey': ['a', 'b', 'd'],
....:
'data2': range(3)})
In [43]: pd.merge(df3, df4, left_on='lkey', right_on='rkey')
Out[43]:
data1 lkey data2 rkey
0
0
b
1
b
1
1
b
1
b
2
6
b
1
b
3
2
a
0
a
4
4
a
0
a
5
5
a
0
a

Вероятно, вы обратили внимание, что значения 'c' и 'd' и ассоциированные с ними данные отсутствуют в результирующем объекте. По умолчанию
функция merge производит внутреннее соединение ('inner'); в результирующий объект попадают только ключи, присутствующие в обоих объектах-аргументах. Альтернативы – 'left', 'right' и 'outer'. В случае внешнего соединения ('outer') берется объединение ключей, т. е. получается то же самое, что
при совместном применении левого и правого соединений:
In [44]: pd.merge(df1, df2, how='outer')
Out[44]:
data1 key data2
0 0.0 b
1.0
1 1.0 b
1.0
2 6.0 b
1.0
3 2.0 a
0.0
4 4.0 a
0.0
5 5.0 a
0.0
6 3.0 c
NaN
7 NaN d
2.0

В табл. 8.1 перечислены возможные значения аргумента how.
Таблица 8.1. Различные типы соединения, задаваемые аргументом how
Значение

Поведение

'inner'
'left'
'right'
'outer'

Брать только комбинации ключей, встречающиеся в обеих таблицах
Брать все ключи, встречающиеся в левой таблице
Брать все ключи, встречающиеся в правой таблице
Брать все комбинации ключей

250

Переформатирование данных: соединение, комбинирование и изменение формы

Для слияния типа многие-ко-многим поведение корректно определено, хотя
на первый взгляд неочевидно. Вот пример:
In [45]: df1 = pd.DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'b'],
....:
'data1': range(6)})
In [46]: df2 = pd.DataFrame({'key': ['a', 'b', 'a', 'b', 'd'],
....:
'data2': range(5)})
In [47]: df1
Out[47]:
data1 key
0
0 b
1
1 b
2
2 a
3
3 c
4
4 a
5
5 b
In [48]: df2
Out[48]:
data2 key
0
0 a
1
1 b
2
2 a
3
3 b
4
4 d
In [49]: pd.merge(df1, df2, on='key', how='left')
Out[49]:
data1 key data2
0
0 b 1.0
1
0 b 3.0
2
1 b 1.0
3
1 b 3.0
4
2 a 0.0
5
2 a 2.0
6
3 c NaN
7
4 a 0.0
8
4 a 2.0
9
5 b 1.0
10
5 b 3.0

Соединение многие-ко-многим порождает декартово произведение строк.
Поскольку в левом объекте DataFrame было три строки с ключом 'b', а в правом – две, то в результирующем объекте таких строк получилось шесть. Метод соединения оказывает влияние только на множество различных ключей
в результате:
In [50]: pd.merge(df1, df2, how='inner')

Комбинирование и слияние наборов данных

251

Out[50]:
data1 key data2
0
0
b
1
1
0
b
3
2
1
b
1
3
1
b
3
4
5
b
1
5
5
b
3
6
2
a
0
7
2
a
2
8
4
a
0
9
4
a
2

Для соединения по нескольким ключам следует передать список имен
столбцов:
In [51]: left = pd.DataFrame({'key1': ['foo', 'foo', 'bar'],
....:
'key2': ['one', 'two', 'one'],
....:
'lval': [1, 2, 3]})
In [52]: right = pd.DataFrame({'key1': ['foo', 'foo', 'bar', 'bar'],
....:
'key2': ['one', 'one', 'one', 'two'],
....:
'rval': [4, 5, 6, 7]})
In [53]: pd.merge(left, right, on=['key1', 'key2'], how='outer')
Out[53]:
key1 key2 lval rval
0 foo one 1.0 4.0
1 foo one 1.0 5.0
2 foo two 2.0 NaN
3 bar one 3.0 6.0
4 bar two NaN 7.0

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

Последний момент, касающийся операций слияния, – обработка одинаковых имен столбцов. Хотя эту проблему можно решить вручную (см. раздел
о переименовании меток на осях ниже), у функции merge имеется параметр
suffixes, позволяющий задать строки, которые должны дописываться в конец
одинаковых имен в левом и правом объектах DataFrame:
In [54]: pd.merge(left, right, on='key1')
Out[54]:
key1 key2_x lval key2_y rval
0 foo
one
1
one
4

252
1
2
3
4
5

foo
foo
foo
bar
bar

Переформатирование данных: соединение, комбинирование и изменение формы
one
two
two
one
one

1
2
2
3
3

one
one
one
one
two

5
4
5
6
7

In [55]: pd.merge(left, right, on='key1', suffixes=('_left', '_right'))
Out[55]:
key1 key2_left lval key2_right rval
0 foo
one
1
one
4
1 foo
one
1
one
5
2 foo
two
2
one
4
3 foo
two
2
one
5
4 bar
one
3
one
6
5 bar
one
3
two
7

В табл. 8.2 приведена справка по аргументам функции merge. Соединение
с использованием индекса строк DataFrame – тема следующего раздела.
Таблица 8.2. Аргументы функции merge
Аргумент

Описание

left
right
how
on

Объект DataFrame в левой части операции слияния
Объект DataFrame в правой части операции слияния
Допустимые значения: 'inner', 'outer', 'left', 'right'
Имена столбцов, по которым производится соединение. Должны присутствовать в обоих
объектах DataFrame. Если не заданы и не указаны никакие другие ключи соединения,
то используются имена столбцов, общих для обоих объектов
Столбцы левого DataFrame, используемые как ключи соединения
Столбцы правого DataFrame, используемые как ключи соединения
Использовать индекс строк левого DataFrame в качестве его ключа соединения
(или нескольких ключей в случае мультииндекса)
То же, что left_index, но для правого DataFrame
Сортировать слитые данные лексикографически по ключам соединения; по умолчанию
True. Иногда при работе с большими наборами данных лучше отключить
Кортеж строк, которые дописываются в конец совпадающих имен столбцов;
по умолчанию ('_x', '_y'). Например, если в обоих объектах DataFrame встречается
столбец 'data', то в результирующем объекте появятся столбцы 'data_x' и 'data_y'
Если равен False, то в некоторых особых случаях разрешается не копировать данные
в результирующую структуру. По умолчанию данные копируются всегда
Добавляет специальный столбец _merge, который сообщает об источнике каждой строки;
он может принимать значения 'left_only', 'right_only' или 'both' в зависимости от
того, как строка попала в результат соединения

left_on
right_on
left_index
right_index
sort
suffixes

copy
indicator

Соединение по индексу
Иногда ключ (или ключи) соединения находится в индексе объекта DataFrame. В таком случае можно задать параметр left_index=True или right_
index=True (или то и другое), чтобы указать, что в качестве ключа соединения
следует использовать индекс:

Комбинирование и слияние наборов данных

253

In [56]: left1 = pd.DataFrame({'key': ['a', 'b', 'a', 'a', 'b', 'c'],
....: 'value': range(6)})
In [57]: right1 = pd.DataFrame({'group_val': [3.5, 7]}, index=['a', 'b'])
In [58]: left1
Out[58]:
key value
0 a
0
1 b
1
2 a
2
3 a
3
4 b
4
5 c
5
In [59]: right1
Out[59]:
group_val
a
3.5
b
7.0
In [60]: pd.merge(left1, right1, left_on='key', right_index=True)
Out[60]:
key value group_val
0 a
0
3.5
2 a
2
3.5
3 a
3
3.5
1 b
1
7.0
4 b
4
7.0

По умолчанию соединение производится по пересекающимся ключам, но
можно вместо пересечения выполнить объединение, указав внешнее соединение:
In [61]: pd.merge(left1, right1, left_on='key', right_index=True, how='outer')
Out[61]:
key value group_val
0
a
0
3.5
2
a
2
3.5
3
a
3
3.5
1
b
1
7.0
4
b
4
7.0
5
c
5
NaN

В случае иерархически индексированных данных ситуация немного усложняется:
In [62]: lefth = pd.DataFrame({'key1': ['Ohio', 'Ohio', 'Ohio',
....:
'Nevada', 'Nevada'],
....:
'key2': [2000, 2001, 2002, 2001, 2002],
....:
'data': np.arange(5.)})

254

Переформатирование данных: соединение, комбинирование и изменение формы

In [63]: righth = pd.DataFrame(np.arange(12).reshape((6, 2)),
....:
index=[['Nevada', 'Nevada', 'Ohio', 'Ohio',
....:
'Ohio', 'Ohio'],
....:
[2001, 2000, 2000, 2000, 2001, 2002]],
....:
columns=['event1', 'event2'])
In [64]: lefth
Out[64]:
data
key1
0 0.0
Ohio
1 1.0
Ohio
2 2.0
Ohio
3 3.0 Nevada
4 4.0 Nevada

key2
2000
2001
2002
2001
2002

In [65]: righth
Out[65]:
event1 event2
Nevada 2001
0
1
2000
2
3
Ohio 2000
4
5
2000
6
7
2001
8
9
2002
10
11

В этом случае необходимо перечислить столбцы, по которым производится
соединение, в виде списка (обратите внимание на обработку повторяющихся
значений в индексе, когда how='outer'):
In [66]: pd.merge(lefth, righth, left_on=['key1', 'key2'], right_index=True)
Out[66]:
data
key1 key2 event1 event2
0 0.0
Ohio 2000
4
5
0 0.0
Ohio 2000
6
7
1 1.0
Ohio 2001
8
9
2 2.0
Ohio 2002
10
11
3 3.0 Nevada 2001
0
1
In [67]:
....:
Out[67]:
data
0 0.0
0 0.0
1 1.0
2 2.0
3 3.0
4 4.0
4 NaN

pd.merge(lefth, righth, left_on=['key1', 'key2'],
right_index=True, how='outer')
key1
Ohio
Ohio
Ohio
Ohio
Nevada
Nevada
Nevada

key2 event1 event2
2000
4.0
5.0
2000
6.0
7.0
2001
8.0
9.0
2002 10.0 11.0
2001
0.0
1.0
2002
NaN
NaN
2000
2.0
3.0

Комбинирование и слияние наборов данных

255

Употребление индексов в обеих частях соединения тоже не проблема:
In [68]: left2 = pd.DataFrame([[1., 2.], [3., 4.], [5., 6.]],
....:
index=['a', 'c', 'e'],
....:
columns=['Ohio', 'Nevada'])
In [69]: right2 = pd.DataFrame([[7., 8.], [9., 10.], [11., 12.], [13, 14]],
....:
index=['b', 'c', 'd', 'e'],
....:
columns=['Missouri', 'Alabama'])
In [70]: left2
Out[70]:
Ohio Nevada
a 1.0
2.0
c 3.0
4.0
e 5.0
6.0
In [71]: right2
Out[71]:
Missouri Alabama
b
7.0
8.0
c
9.0
10.0
d
11.0
12.0
e
13.0
14.0
In [72]: pd.merge(left2, right2, how='outer', left_index=True, right_index=True)
Out[72]:
Ohio Nevada Missouri Alabama
a 1.0
2.0
NaN
NaN
b NaN
NaN
7.0
8.0
c 3.0
4.0
9.0
10.0
d NaN
NaN
11.0
12.0
e 5.0
6.0
13.0
14.0

В классе DataFrame есть и более удобный метод экземпляра join для слияния по индексу. Его также можно использовать для комбинирования нескольких объектов DataFrame, обладающих одинаковыми или похожими индексами, но непересекающимися столбцами. В предыдущем примере можно было
бы написать:
In [73]: left2.join(right2, how='outer')
Out[73]:
Ohio Nevada Missouri Alabama
a 1.0
2.0
NaN
NaN
b NaN
NaN
7.0
8.0
c 3.0
4.0
9.0
10.0
d NaN
NaN
11.0
12.0
e 5.0
6.0
13.0
14.0

Отчасти из-за необходимости поддерживать совместимость (с очень старыми версиями pandas) метод join объекта DataFrame выполняет левое внешнее соединение, в точности сохраняя индекс строк левого фрейма. Он также

256

Переформатирование данных: соединение, комбинирование и изменение формы

поддерживает соединение с индексом переданного DataFrame по одному из
столбцов вызывающего:
In [74]: left1.join(right1, on='key')
Out[74]:
key value group_val
0
a
0
3.5
1
b
1
7.0
2
a
2
3.5
3
a
3
3.5
4
b
4
7.0
5
c
5
NaN

Наконец, в случае простых операций слияния индекса с индексом можно
передать список объектов DataFrame методу join в качестве альтернативы
использованию более общей функции concat, которая описана ниже:
In [75]: another = pd.DataFrame([[7., 8.], [9., 10.], [11., 12.], [16., 17.]],
....: index=['a', 'c', 'e', 'f'],
....: columns=['New York', 'Oregon'])
In [76]: another
Out[76]:
New York Oregon
a
7.0
8.0
c
9.0 10.0
e
11.0 12.0
f
16.0 17.0
In [77]: left2.join([right2, another])
Out[77]:
Ohio Nevada Missouri Alabama New York Oregon
a 1.0 2.0
NaN
NaN
7.0
8.0
c 3.0 4.0
9.0
10.0
9.0 10.0
e 5.0 6.0
13.0
14.0
11.0 12.0
In [78]: left2.join([right2, another], how='outer')
Out[78]:
Ohio Nevada Missouri Alabama New York Oregon
a 1.0
2.0
NaN
NaN
7.0
8.0
b NaN
NaN
7.0
8.0
NaN
NaN
c 3.0
4.0
9.0
10.0
9.0 10.0
d NaN
NaN
11.0
12.0
NaN
NaN
e 5.0
6.0
13.0
14.0
11.0 12.0
f NaN
NaN
NaN
NaN
16.0 17.0

Конкатенация вдоль оси
Еще одну операцию комбинирования данных разные авторы называют
по-разному: конкатенация, связывание или укладка. В библиотеке NumPy
имеется функция concatenate для выполнения этой операции над массивами:

Комбинирование и слияние наборов данных

257

In [79]: arr = np.arange(12).reshape((3, 4))
In [80]:
Out[80]:
array([[
[
[

arr

In [81]:
Out[81]:
array([[
[
[

np.concatenate([arr, arr], axis=1)

0, 1, 2, 3],
4, 5, 6, 7],
8, 9, 10, 11]])

0, 1, 2, 3, 0, 1, 2, 3],
4, 5, 6, 7, 4, 5, 6, 7],
8, 9, 10, 11, 8, 9, 10, 11]])

В контексте объектов pandas, Series и DataFrame наличие помеченных осей
позволяет обобщить конкатенацию массивов. В частности, нужно решить следующие вопросы:
• если объекты по-разному проиндексированы по другим осям, следует
ли объединять различные элементы на этих осях, или нужно использовать только общие значения (пересечение)?
• нужно ли иметь возможность идентифицировать группы в результирующем объекте?
• содержит ли «ось конкатенации» данные, которые необходимо сохранить? Во многих случаях подразумеваемые по умолчанию целочисленные метки в объекте DataFrame в процессе конкатенации лучше
отбросить.
Функция concat в pandas дает согласованные ответы на эти вопросы. Я покажу, как она работает, на примерах. Допустим, имеются три объекта Series
с непересекающимися индексами:
In [82]: s1 = pd.Series([0, 1], index=['a', 'b'])
In [83]: s2 = pd.Series([2, 3, 4], index=['c', 'd', 'e'])
In [84]: s3 = pd.Series([5, 6], index=['f', 'g'])

Если передать их функции concat списком, то она склеит данные и индексы:
In [85]: pd.concat([s1, s2, s3])
Out[85]:
a 0
b 1
c 2
d 3
e 4
f 5
g 6
dtype: int64

258

Переформатирование данных: соединение, комбинирование и изменение формы

По умолчанию concat работает вдоль оси axis=0, порождая новый объект
Series. Но если передать параметр axis=1, то результатом будет DataFrame
(в нем axis=1 – ось столбцов):
In [86]: pd.concat([s1, s2, s3], axis=1)
Out[86]:
0
1
2
a
0 NaN NaN
b
1 NaN NaN
c NaN
2 NaN
d NaN
3 NaN
e NaN
4 NaN
f NaN NaN
5
g NaN NaN
6

В данном случае на другой оси нет перекрытия, и она, как видно, является
отсортированным объединением (внешним соединением) индексов. Но можно образовать и пересечение индексов, если передать параметр join='inner':
In [87]: s4 = pd.concat([s1, s3])
In [88]: s4
Out[88]:
a 0
b 1
f 5
g 6
dtype: int64
In [89]: pd.concat([s1, s4], axis=1)
Out[89]:
0 1
a 0.0 0
b 1.0 1
f NaN 5
g NaN 6
In [90]: pd.concat([s1, s4], axis=1, join='inner')
Out[90]:
0 1
a 0 0
b 1 1

В последнем примере метки 'f' и 'g' пропали, поскольку был задан аргумент join='inner'.
Можно даже задать, какие метки будут использоваться на других осях –
с помощью параметра join_axes:
In [91]: pd.concat([s1, s4], axis=1, join_axes=[['a', 'c', 'b', 'e']])
Out[91]:
0
1
a 0.0 0.0

Комбинирование и слияние наборов данных

259

c NaN NaN
b 1.0 1.0
e NaN NaN

Проблема может возникнуть из-за того, что в результирующем объекте не
видно, конкатенацией каких объектов он получен. Допустим, что вы на самом
деле хотите построить иерархический индекс на оси конкатенации. Для этого
используется аргумент keys:
In [92]: result = pd.concat([s1, s1, s3], keys=['one', 'two', 'three'])
In [93]:
Out[93]:
one
a
b
two
a
b
three f
g

result
0
1
0
1
5
6

In [94]: result.unstack()
Out[94]:
a
b
f
g
one
0.0 1.0 NaN NaN
two
0.0 1.0 NaN NaN
three NaN NaN 5.0 6.0

При комбинировании Series вдоль оси axis=1 элементы списка keys становятся заголовками столбцов объекта DataFrame:
In [95]: pd.concat([s1, s2, s3], axis=1, keys=['one', 'two', 'three'])
Out[95]:
one two three
a 0.0 NaN
NaN
b 1.0 NaN
NaN
c NaN 2.0
NaN
d NaN 3.0
NaN
e NaN 4.0
NaN
f NaN NaN
5.0
g NaN NaN
6.0

Эта логика обобщается и на объекты DataFrame:
In [96]: df1 = pd.DataFrame(np.arange(6).reshape(3, 2), index=['a', 'b', 'c'],
....:
columns=['one', 'two'])
In [97]: df2 = pd.DataFrame(5 + np.arange(4).reshape(2, 2), index=['a', 'c'],
....:
columns=['three', 'four'])
In [98]: df1
Out[98]:
one two
a
0 1

260
b
c

2
4

Переформатирование данных: соединение, комбинирование и изменение формы
3
5

In [99]: df2
Out[99]:
three four
a
5
6
c
7
8
In [100]: pd.concat([df1, df2], axis=1, keys=['level1', 'level2'])
Out[100]:
level1
level2
one two three four
a
0 1
5.0 6.0
b
2 3
NaN NaN
c
4 5
7.0 8.0

Если передать не список, а словарь объектов, то роль аргумента keys будут
играть ключи словаря:
In [101]: pd.concat({'level1': df1, 'level2': df2}, axis=1)
Out[101]:
level1
level2
one two three four
a
0
1
5.0 6.0
b
2
3
NaN NaN
c
4
5
7.0 8.0

Дополнительные аргументы управляют созданием иерархического индекса
(см. табл. 8.3). Например, можно поименовать созданные уровни на оси с помощью аргумента names:
In [78]: pd.concat([df1, df2], axis=1, keys=['level1', 'level2'],
....:
names=['upper', 'lower'])
Out[78]:
upper level1
level2
lower
one two three four
a
0
1
5.0 6.0
b
2
3
NaN NaN
c
4
5
7.0 8.0

Последнее замечание касается объектов DataFrame, в которых индекс строк
не имеет смысла в контексте анализа:
In [103]: df1 = pd.DataFrame(np.random.randn(3, 4), columns=['a', 'b', 'c', 'd'])
In [104]: df2 = pd.DataFrame(np.random.randn(2, 3), columns=['b', 'd', 'a'])
In [105]: df1
Out[105]:
a
b
c
d
0 1.246435 1.007189 –1.296221 0.274992

Комбинирование и слияние наборов данных

261

1 0.228913 1.352917 0.886429 –2.001637
2 –0.371843 1.669025 –0.438570 –0.539741
In [106]: df2
Out[106]:
b
d
a
0 0.476985 3.248944 –1.021228
1 –0.577087 0.124121 0.302614

В таком случае можно передать параметр ignore_index=True:
In [107]: pd.concat([df1, df2],
Out[107]:
a
b
c
0 1.246435 1.007189 –1.296221
1 0.228913 1.352917 0.886429
2 –0.371843 1.669025 –0.438570
3 –1.021228 0.476985
NaN
4 0.302614 –0.577087
NaN

ignore_index=True)
d
0.274992
–2.001637
–0.539741
3.248944
0.124121

Таблица 8.3. Аргументы функции concat
Аргумент

Описание

objs

Список или словарь конкатенируемых объектов pandas. Единственный обязательный
аргумент
Ось, вдоль которой производится конкатенация, по умолчанию 0
Допустимые значения: 'inner', 'outer', по умолчанию 'outer'; следует ли
пересекать (inner) или объединять (outer) индексы вдоль других осей
Какие конкретно индексы использовать для других n – 1 осей вместо выполнения
пересечения или объединения
Значения, которые ассоциируются с конкатенируемыми объектами и образуют
иерархический индекс вдоль оси конкатенации. Может быть список или массив
произвольных значений, а также массив кортежей или список массивов
(если в параметре levels передаются массивы для нескольких уровней)
Конкретные индексы, которые используются на одном или нескольких уровнях
иерархического индекса, если задан параметр keys
Имена создаваемых уровней иерархического индекса, если заданы параметры keys
и (или) levels
Проверить новую ось в конкатенированном объекте на наличие дубликатов
и, если они имеются, возбудить исключение. По умолчанию False – дубликаты
разрешены
Не сохранять индексы вдоль оси конкатенации, а вместо этого создать новый
индекс range(total_length)

axis
join
join_axes
keys

levels
names
verify_integrity

ignore_index

Комбинирование перекрывающихся данных
Есть еще одна ситуация, которую нельзя выразить как слияние или конкатенацию. Речь идет о двух наборах данных, индексы которых полностью
или частично пересекаются. В качестве пояснительного примера рассмотрим
функцию NumPy where, которая выражает векторный аналог if-else:

262

Переформатирование данных: соединение, комбинирование и изменение формы

In [108]: a = pd.Series([np.nan, 2.5, np.nan, 3.5, 4.5, np.nan],
.....:
index=['f', 'e', 'd', 'c', 'b', 'a'])
In [109]: b = pd.Series(np.arange(len(a), dtype=np.float64),
.....:
index=['f', 'e', 'd', 'c', 'b', 'a'])
In [110]: b[–1] = np.nan
In [111]: a
Out[111]:
f NaN
e 2.5
d NaN
c 3.5
b 4.5
a NaN
dtype: float64
In [112]: b
Out[112]:
f 0.0
e 1.0
d 2.0
c 3.0
b 4.0
a NaN
dtype: float64
In [113]: np.where(pd.isnull(a), b, a)
Out[113]: array([ 0. , 2.5, 2. , 3.5, 4.5, nan])

У объекта Series имеется метод combine_first, который выполняет эквивалент этой операции плюс обычное для pandas выравнивание данных:
In [114]: b[:–2].combine_first(a[2:])
Out[114]:
a
NaN
b
4.5
c
3.0
d
2.0
e
1.0
f
0.0
dtype: float64

В случае DataFrame метод combine_first делает то же самое для каждого
столбца, так что можно считать, что он подставляет вместо данных, отсутствующих в вызывающем объекте, данные из объекта, переданного в аргументе:
In [115]: df1 = pd.DataFrame({'a': [1., np.nan, 5., np.nan],
....:
'b': [np.nan, 2., np.nan, 6.],
....:
'c': range(2, 18, 4)})

Изменение формы и поворот

263

In [116]: df2 = pd.DataFrame({'a': [5., 4., np.nan, 3., 7.],
....: 'b': [np.nan, 3., 4., 6., 8.]})
In [117]: df1.combine_first(df2)
Out[117]:
a
b c
0 1.0 NaN 2
1 NaN 2.0 6
2 5.0 NaN 10
3 NaN 6.0 14
In [118]: df2
Out[118]:
a
b
0 5.0 NaN
1 4.0 3.0
2 NaN 4.0
3 3.0 6.0
4 7.0 8.0
In [119]: df1.combine_first(df2)
Out[119]:
a
b
c
0 1.0 NaN 2.0
1 4.0 2.0 6.0
2 5.0 4.0 10.0
3 3.0 6.0 14.0
4 7.0 8.0 NaN

8.3. Изменение формы и поворот
Существует ряд фундаментальных операций реорганизации табличных данных. Иногда их называют изменением формы (reshape), а иногда – поворотом
(pivot).

Изменение формы с помощью иерархического индексирования
Иерархическое индексирование дает естественный способ реорганизовать
данные в DataFrame. Есть два основных действия:
• stack – это «поворот», который переносит данные из столбцов в строки;
• Unstack – обратный поворот, который переносит данные из строк
в столбцы.
Проиллюстрирую эти операции примерами. Рассмотрим небольшой
DataFrame, в котором индексы строк и столбцов – массивы строк.
In [120]: data = pd.DataFrame(np.arange(6).reshape((2, 3)),
....:
index=pd.Index(['Ohio', 'Colorado'], name='state'),
....:
columns=pd.Index(['one', 'two', 'three'], name='number'))

264

Переформатирование данных: соединение, комбинирование и изменение формы

In [121]: data
Out[121]:
number
one two three
state
Ohio
0
1
2
Colorado
3
4
5

Метод stack поворачивает таблицу, так что столбцы оказываются строками,
и в результате получается объект Series:
In [122]: result = data.stack()
In [123]: result
Out[123]:
state
number
Ohio
one
0
two
1
three 2
Colorado one
3
two
4
three 5
dtype: int64

Имея иерархически проиндексированный объект Series, мы можем восстановить DataFrame методом unstack:
In [124]: result.unstack()
Out[124]:
number
one two three
state
Ohio
0
1
2
Colorado
3
4
5

По умолчанию поворачивается самый внутренний уровень (как и в случае
stack). Но можно повернуть и любой другой, если указать номер или имя
уровня:
In [125]: result.unstack(0)
Out[125]:
state Ohio Colorado
number
one
0
3
two
1
4
three
2
5
In [126]: result.unstack('state')
Out[126]:
state Ohio Colorado
number
one
0
3
two
1
4
three
2
5

Изменение формы и поворот

265

При обратном повороте могут появиться отсутствующие данные, если не
каждое значение на указанном уровне присутствует в каждой подгруппе:
In [127]: s1 = pd.Series([0, 1, 2, 3], index=['a', 'b', 'c', 'd'])
In [128]: s2 = pd.Series([4, 5, 6], index=['c', 'd', 'e'])
In [129]: data2 = pd.concat([s1, s2], keys=['one', 'two'])
In [130]: data2
Out[130]:
one a 0
b 1
c 2
d 3
two c 4
d 5
e 6
dtype: int64
In [131]: data2.unstack()
Out[131]:
a
b
c
d
e
one 0.0 1.0 2.0 3.0 NaN
two NaN NaN 4.0 5.0 6.0

При выполнении поворота отсутствующие данные по умолчанию отфильтровываются, поэтому операция обратима:
In [132]: data2.unstack()
Out[132]:
a
b
c
d
e
one 0.0 1.0 2.0 3.0 NaN
two NaN NaN 4.0 5.0 6.0
In [133]: data2.unstack().stack()
Out[133]:
one a 0.0
b 1.0
c 2.0
d 3.0
two c 4.0
d 5.0
e 6.0
dtype: float64
In [134]: data2.unstack().stack(dropna=False)
Out[134]:
one a 0.0
b 1.0
c 2.0
d 3.0
e NaN

266
two a
b
c
d
e
dtype:

Переформатирование данных: соединение, комбинирование и изменение формы
NaN
NaN
4.0
5.0
6.0
float64

В случае обратного поворота DataFrame поворачиваемый уровень становится самым нижним уровнем результирующего объекта:
In [135]: df = pd.DataFrame({'left': result, 'right': result + 5},
.....:
columns=pd.Index(['left', 'right'], name='side'))
In [136]: df
Out[136]:
Side
left right
state
number
Ohio
one
0
5
Two
1
6
three 2
7
Colorado one
3
8
two
4
9
three 5
10
In [137]: df.unstack('state')
Out[137]:
side
left
right
state Ohio Colorado Ohio Colorado
number
one
0
3
5
8
two
1
4
6
9
three
2
5
7
10

При вызове stack можно указать имя поворачиваемой оси:
In [138]: df.unstack('state').stack('side')
Out[138]:
state
Colorado Ohio
number side
one
left
3
0
right
8
5
two
left
4
1
right
9
6
three left
5
2
right
10
7

Поворот из «длинного» в «широкий» формат
Стандартный способ хранения нескольких временных рядов в базах данных и в CSV-файлах – так называемый длинный формат (в столбик). Загрузим

267

Изменение формы и поворот

демонстрационные данные и займемся переформатированием временных
рядов и другими операциями очистки данных:
In [139]: data = pd.read_csv('examples/macrodata.csv')
In [140]: data.head()
Out[140]:
year quarter realgdp realcons realinv realgovt
0 1959.0
1.0 2710.349 1707.4 286.898 470.045
1 1959.0
2.0 2778.801 1733.7 310.859 481.301
2 1959.0
3.0 2775.488 1751.8 289.226 491.260
3 1959.0
4.0 2785.204 1753.7 299.356 484.052
4 1960.0
1.0 2847.699 1770.5 331.722 462.199
m1 tbilrate unemp
pop infl realint
0 139.7
2.82 5.8 177.146 0.00
0.00
1 141.7
3.08 5.1 177.830 2.34
0.74
2 140.5
3.82 5.3 178.657 2.74
1.09
3 140.0
4.33 5.6 179.386 0.27
4.06
4 139.6
3.50 5.2 180.007 2.31
1.19

realdpi
1886.9
1919.7
1916.4
1931.3
1955.5

cpi \
28.98
29.15
29.35
29.37
29.54

In [141]: periods = pd.PeriodIndex(year=data.year, quarter=data.quarter,
.....:
name='date')
In [142]: columns = pd.Index(['realgdp', 'infl', 'unemp'], name='item')
In [143]: data = data.reindex(columns=columns)
In [144]: data.index = periods.to_timestamp('D', 'end')
In [145]: ldata = data.stack().reset_index().rename(columns={0: 'value'})

С функцией PeriodIndex мы поближе познакомимся в главе 11. В двух словах, она объединяет столбцы year и quarter, создавая специальный тип временного интервала. Теперь ldata выглядит так:
In [146]: ldata[:10]
Out[146]:
date
item
0 1959–03–31 realgdp
1 1959–03–31
infl
2 1959–03–31 unemp
3 1959–06–30 realgdp
4 1959–06–30
infl
5 1959–06–30 unemp
6 1959–09–30 realgdp
7 1959–09–30
infl
8 1959–09–30 unemp
9 1959–12–31 realgdp

value
2710.349
0.000
5.800
2778.801
2.340
5.100
2775.488
2.740
5.300
2785.204

Это и называется длинным форматом для нескольких временных рядов или
других данных наблюдений с двумя и более ключами (в данном случае клю-

268

Переформатирование данных: соединение, комбинирование и изменение формы

чами являются дата и показатель item). Каждая строка таблицы соответствует
одному наблюдению.
Так данные часто хранятся в реляционных базах данных типа MySQL, поскольку при наличии фиксированной схемы (совокупность имен и типов данных столбцов) количество различных значений в столбце item может увеличиваться или уменьшаться при добавлении или удалении данных. В примере
выше пара столбцов date и item обычно выступает в роли первичного ключа
(в терминологии реляционных баз данных), благодаря которому обеспечивается целостность данных и упрощаются многие операции соединения. Иногда с данными в таком формате трудно работать; предпочтительнее иметь
объект DataFrame, содержащий по одному столбцу на каждое уникальное
значение item и проиндексированный временными метками в столбце date.
Метод pivot объекта DataFrame именно такое преобразование и выполняет:
In [147]: pivoted = ldata.pivot('date', 'item', 'value')
In [148]: pivoted
Out[148]:
item
infl
realgdp unemp
date
1959–03–31 0.00 2710.349 5.8
1959–06–30 2.34 2778.801 5.1
1959–09–30 2.74 2775.488 5.3
1959–12–31 0.27 2785.204 5.6
1960–03–31 2.31 2847.699 5.2
1960–06–30 0.14 2834.390 5.2
1960–09–30 2.70 2839.022 5.6
1960–12–31 1.21 2802.616 6.3
1961–03–31 –0.40 2819.264 6.8
1961–06–30 1.47 2872.005 7.0
...
...
... ...
2007–06–30 2.75 13203.977 4.5
2007–09–30 3.45 13321.109 4.7
2007–12–31 6.38 13391.249 4.8
2008–03–31 2.82 13366.865 4.9
2008–06–30 8.53 13415.266 5.4
2008–09–30 –3.16 13324.600 6.0
2008–12–31 –8.79 13141.920 6.9
2009–03–31 0.94 12925.410 8.1
2009–06–30 3.37 12901.504 9.2
2009–09–30 3.56 12990.341 9.6
[203 rows x 3 columns]

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

269

Изменение формы и поворот
In [149]: ldata['value2'] = np.random.randn(len(ldata))
In [150]: ldata[:10]
Out[150]:
date
0 1959–03–31 00:00:00
1 1959–03–31 00:00:00
2 1959–03–31 00:00:00
3 1959–06–30 00:00:00
4 1959–06–30 00:00:00
5 1959–06–30 00:00:00
6 1959–09–30 00:00:00
7 1959–09–30 00:00:00
8 1959–09–30 00:00:00
9 1959–12–31 00:00:00

item
realgdp
infl
unemp
realgdp
infl
unemp
realgdp
infl
unemp
realgdp

value
2710.349
0.000
5.800
2778.801
2.340
5.100
2775.488
2.740
5.300
2785.204

value2
1.669025
–0.438570
–0.539741
0.476985
3.248944
–1.021228
–0.577087
0.124121
0.302614
0.523772

Опустив последний аргумент, получим DataFrame с иерархическими столбцами:
In [151]: pivoted = ldata.pivot('date', 'item')
In [152]: pivoted[:5]
Out[152]:
value
value2
item
infl realgdp unemp
infl
realgdp
unemp
date
1959–03–31 0.00 2710.349
5.8 –0.438570 1.669025 –0.539741
1959–06–30 2.34 2778.801
5.1 3.248944 0.476985 –1.021228
1959–09–30 2.74 2775.488
5.3 0.124121 –0.577087 0.302614
1959–12–31 0.27 2785.204
5.6 0.000940 0.523772 1.343810
1960–03–31 2.31 2847.699
5.2 –0.831154 –0.713544 –2.370232
In [153]: pivoted['value'][:5]
Out[153]:
item
infl realgdp unemp
date
1959–03–31 0.00 2710.349
5.8
1959–06–30 2.34 2778.801
5.1
1959–09–30 2.74 2775.488
5.3
1959–12–31 0.27 2785.204
5.6
1960–03–31 2.31 2847.699
5.2

Отметим, что метод pivot – это не более чем сокращенный способ создания
иерархического индекса с помощью set_index и последующего вызова unstack:
In [154]: unstacked = ldata.set_index(['date', 'item']).unstack('item')
In [155]: unstacked[:7]
Out[155]:
value value2
item infl realgdp unemp infl realgdp unemp
date

270
item
date
1959–03–31
1959–06–30
1959–09–30
1959–12–31
1960–03–31
1960–06–30
1960–09–30

Переформатирование данных: соединение, комбинирование и изменение формы
value
infl
0.00
2.34
2.74
0.27
2.31
0.14
2.70

realgdp unemp
2710.349
2778.801
2775.488
2785.204
2847.699
2834.390
2839.022

value2
infl

realgdp

unemp

5.8 –0.438570 1.669025 –0.539741
5.1 3.248944 0.476985 –1.021228
5.3 0.124121 –0.577087 0.302614
5.6 0.000940 0.523772 1.343810
5.2 –0.831154 –0.713544 –2.370232
5.2 –0.970736 –1.541996 –1.307030
5.6 0.377984 0.286350 –0.753887

Поворот из «широкого» в «длинный» формат
Обратной к pivot операцией является pandas.melt. Вместо того чтобы преобразовывать один столбец в несколько в новом объекте DataFrame, она объединяет несколько столбцов в один, порождая DataFrame длиннее входного.
Рассмотрим пример:
In [157]: df = pd.DataFrame({'key': ['foo', 'bar', 'baz'],
.....:
'A': [1, 2, 3],
.....:
'B': [4, 5, 6],
.....:
'C': [7, 8, 9]})
In [158]: df
Out[158]:
A B C key
0 1 4 7 foo
1 2 5 8 bar
2 3 6 9 baz

Столбец 'key' может быть индикатором группы, а остальные столбцы –
значениями данных. При использовании функции pandas.melt необходимо
указать, какие столбцы являются индикаторами группы (если таковые имеются). Будем считать, что в данном случае 'key' – единственный индикатор
группы:
In [159]: melted = pd.melt(df, ['key'])
In [160]: melted
Out[160]:
key variable value
0 foo
A
1
1 bar
A
2
2 baz
A
3
3 foo
B
4
4 bar
B
5
5 baz
B
6
6 foo
C
7
7 bar
C
8
8 baz
C
9

Powered by TCPDF (www.tcpdf.org)

Изменение формы и поворот

271

Применив pivot, можем вернуться к исходной форме:
In [161]: reshaped = melted.pivot('key', 'variable', 'value')
In [162]: reshaped
Out[162]:
variable A B C
key
bar
2 5 8
baz
3 6 9
foo
1 4 7

Поскольку в результате работы pivot из столбца создается индекс, используемый как метки строк, возможно, понадобится вызвать reset_index, чтобы
восстановить из индекса столбец:
In [163]: reshaped.reset_index()
Out[163]:
variable key A B C
0
bar 2 5 8
1
baz 3 6 9
2
foo 1 4 7

Можно также указать, какое подмножество столбцов следует использовать
как значения:
In [164]: pd.melt(df, id_vars=['key'], value_vars=['A', 'B'])
Out[164]:
key variable value
0 foo
A
1
1 bar
A
2
2 baz
A
3
3 foo
B
4
4 bar
B
5
5 baz
B
6

Функцию pandas.melt можно использовать и без идентификаторов групп:
In [165]: pd.melt(df, value_vars=['A', 'B', 'C'])
Out[165]:
variable value
0
A
1
1
A
2
2
A
3
3
B
4
4
B
5
5
B
6
6
C
7
7
C
8
8
C
9
In [166]: pd.melt(df, value_vars=['key', 'A', 'B'])

272

Переформатирование данных: соединение, комбинирование и изменение формы

Out[166]:
variable value
0
key foo
1
key bar
2
key baz
3
A
1
4
A
2
5
A
3
6
B
4
7
B
5
8
B
6

8.4. Заключение
Теперь, вооружившись базовыми знаниями о применении pandas для импорта, очистки и реорганизации данных, мы готовы перейти к визуализации
с помощью matplotlib. Но мы еще вернемся к pandas, когда начнем обсуждать
дополнительные средства аналитики.

Глава 9. Построение графиков
и визуализация
Информативная визуализация (называемая также построением графиков) –
одна из важнейших задач анализа данных. Она может быть частью процесса
исследования, например применяться для выявления выбросов, определения
необходимых преобразований данных или поиска идей для построения моделей. В других случаях построение интерактивной визуализации для вебсайта может быть конечной целью. Для Python имеется много дополнительных библиотек статической и динамической визуализации, но я буду использовать в основном matplotlib (http://matplotlib.sourceforge.net) и надстроенные
над ней библиотеки.
Matplotlib – это пакет для построения графиков (главным образом двумерных) полиграфического качества. Проект был основан Джоном Хантером
в 2002 году с целью реализовать на Python интерфейс, аналогичный MATLAB.
Впоследствии сообщества matplotlib и IPython совместно работали над тем,
чтобы упростить интерактивное построение графиков из оболочки IPython
(а теперь и Jupyter-блокнотов). Matplotlib поддерживает разнообразные графические интерфейсы пользователя во всех операционных системах, а также
умеет экспортировать графические данные во всех векторных и растровых
форматах: PDF, SVG, JPG, PNG, BMP, GIF и т. д. С его помощью я построил
почти все рисунки для этой книги, за исключением нескольких диаграмм.
Со временем над matplotlib было надстроено много дополнительных библиотек визуализации. Одну из них, seaborn (http://seaborn.pydata.org/), мы будем изучать в этой главе.
Для проработки приведенных в данной главе примеров кода проще всего
воспользоваться интерактивным построением графиков в Jupyter-блокноте.
Чтобы настроить этот режим, выполните в Jupyter-блокноте такую команду:
%matplotlib notebook

274

Построение графиков и визуализация

9.1. Краткое введение в API библиотеки
matplotlib
При работе с matplotlib мы будем использовать следующее соглашение об
импорте:
In [11]: import matplotlib.pyplot as plt

После выполнения команды %matplotlib notebook в Jupyter (или просто %mat­
plotlib в IPython) уже можно создать простой график. Если все настроено
правильно, то должна появиться прямая линия, показанная на рис. 9.1:
In [12]: import numpy as np
In [13]: data = np.arange(10)
In [14]: data
Out[14]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
In [15]: plt.plot(data)

Рис. 9.1. Простой линейный график

Библиотеки типа seaborn и встроенные в pandas функции построения
графиков берут на себя многие рутинные детали, но если предусмотренных
в них параметров вам недостаточно, то придется разбираться с API библиотеки matplotlib.

Краткое введение в API библиотеки matplotlib

275

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

Рисунки и подграфики
Графики в matplotlib «живут» внутри объекта рисунка Figure. Создать новый рисунок можно методом plt.figure:
In [16]: fig = plt.figure()

В IPython появится пустое окно графика, но в Jupyter не появится ничего,
пока мы не выполним еще несколько команд. У команды plt.figure() имеется
ряд параметров, в частности figsize гарантирует, что при сохранении рисунка
на диске у него будут определенные размер и отношение сторон.
Нельзя создать график, имея пустой рисунок. Сначала нужно создать один
или несколько подграфиков с помощью метода add_subplot:
In [17]: ax1 = fig.add_subplot(2, 2, 1)

Это означает, что рисунок будет расчерчен сеткой 2×2, и мы выбираем
первый из четырех подграфиков (нумерация начинается с 1). Если создать
следующие два подграфика, то получится как на рис. 9.2.
In [18]: ax2 = fig.add_subplot(2, 2, 2)
In [19]: ax3 = fig.add_subplot(2, 2, 3)

Рис. 9.2. Пустой рисунок matplotlib с тремя подграфиками

276

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

В примере ниже все команды находятся в одной ячейке:
fig
ax1
ax2
ax3

=
=
=
=

plt.figure()
fig.add_subplot(2, 2, 1)
fig.add_subplot(2, 2, 2)
fig.add_subplot(2, 2, 3)

При выполнении команды построения графика, например plt.plot([1.5,
3.5, –2, 1.6]), matplotlib рисует на последнем использованном рисунке и подграфике (при необходимости создав то и другое) и тем самым маскирует
создание рисунка и подграфика. Следовательно, выполнив показанную ниже
команду, мы получим картину, изображенную на рис. 9.3:
In [20]: plt.plot(np.random.randn(50).cumsum(), 'k––')

Рис. 9.3. Визуализация данных после построения одного графика

Параметр стиля 'k––' говорит matplotlib, что график нужно рисовать черной штриховой линией. Метод fig.add_subplot возвращает объект AxesSubplot,
который позволяет рисовать в другом пустом подграфике, вызывая его методы экземпляра (см. рис. 9.4):
In [21]: _ = ax1.hist(np.random.randn(100), bins=20, color='k', alpha=0.3)
In [22]: ax2.scatter(np.arange(30), np.arange(30) + 3 * np.random.randn(30))

277

Краткое введение в API библиотеки matplotlib

Рис. 9.4. Визуализация данных
после построения дополнительных графиков

Полный перечень типов графиков имеется в документации по matplotlib.
Поскольку создание рисунка с несколькими подграфиками, расположенными определенным образом, – типичная задача, существует вспомогательный метод plt.subplots, который создает новый рисунок и возвращает массив
NumPy, содержащий созданные в нем объекты подграфиков:
In [24]: fig, axes = plt.subplots(2, 3)
In [25]: axes
Out[25]:
array([[]], dtype

Это очень полезно, потому что к массиву axes вполне можно обращаться
как к двумерному массиву, например axes[0, 1]. Можно также указать, что
подграфики должны иметь общую ось x или y, задав параметры sharex и sharey
соответственно. Особенно это удобно, когда надо сравнить данные в одном
масштабе; иначе matplotlib автоматически и независимо выбирает масштаб
графика. Подробнее об этом методе см. табл. 9.1.

278

Построение графиков и визуализация

Таблица 9.1. Параметры метода pyplot.subplots
Аргумент

Описание

Число строк в сетке подграфиков
Число столбцов в сетке подграфиков
Все подграфики должны иметь одинаковые риски на оси X (настройка xlim отражается
на всех подграфиках)
sharey
Все подграфики должны иметь одинаковые риски на оси Y (настройка ylim отражается
на всех подграфиках)
subplot_kw Словарь ключевых слов для создания подграфиков
**fig_kw
Дополнительные ключевые слова используются при создании рисунка, например
plt.subplots(2, 2,figsize=(8, 6))

nrows
ncols
sharex

Задание свободного места вокруг подграфиков
По умолчанию matplotlib оставляет пустое место вокруг каждого подграфика и между подграфиками. Размер этого места определяется высотой
и шириной графика, так что если изменить размер графика программно или
вручную (изменив размер окна), то график автоматически перестроится. Величину промежутка легко изменить с помощью метода subplots_adjust объекта Figure, который также доступен в виде функции верхнего уровня:
subplots_adjust(left=None, bottom=None, right=None, top=None,
wspace=None, hspace=None)

Параметры wspace и hspace определяют, какой процент от ширины (соответственно высоты) рисунка должен составлять промежуток между подграфиками. В примере ниже я задал нулевой промежуток (рис. 9.5):
fig, axes = plt.subplots(2, 2, sharex=True, sharey=True)
for i in range(2):
for j in range(2):
axes[i,j].hist(randn(500), bins=50, color='k', alpha=0.5)
plt.subplots_adjust(wspace=0, hspace=0)

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

Цвета, маркеры и стили линий
Главная функция matplotlib – plot – принимает массивы координат x и y,
а также необязательную строку, в которой закодированы цвет и стиль линии.
Например, чтобы нарисовать график зависимости y от x зеленой штриховой
линией, нужно выполнить следующий вызов:
ax.plot(x, y, 'g––')

Краткое введение в API библиотеки matplotlib

279

Рис. 9.5. Визуализация данных,
в которой подграфики не разделены промежутками

Такой способ задания цвета и стиля линий в виде строки не более чем
удобство; на практике, когда графики строятся из программы, лучше не запутывать код строковыми обозначениями стиля. Этот график можно было
бы описать и более понятно:
ax.plot(x, y, linestyle='––', color='g')

Существует ряд сокращений для наиболее употребительных цветов, но вообще любой цвет можно представить своим RGB-значением (например, '#CE­
CECE'). Полный перечень стилей линий имеется в строке документации для
функции plot (в IPython или Jupyter введите plot?).
Линейные графики могут быть также снабжены маркерами, обозначающими точки, по которым построен график. Поскольку matplotlib создает непрерывный линейный график, производя интерполяцию между точками, иногда
не ясно, где же находятся исходные точки. Маркер можно задать в строке
стиля: сначала цвет, потом тип маркера и в конце стиль линии (рис. 9.6):
То же самое можно записать явно:
plot(randn(30).cumsum(), color='k', linestyle='dashed', marker='o')

280

Построение графиков и визуализация

Рис. 9.6. Линейный график с примером маркеров

По умолчанию на линейных графиках соседние точки соединяются отрезками прямой, т. е. производится линейная интерполяция. Параметр drawstyle
позволяет изменить этот режим:
In [33]: data = randn(30).cumsum()
In [34]: plt.plot(data, 'k––', label='Default')
Out[34]: []
In [35]: plt.plot(data, 'k–', drawstyle='steps–post', label='steps–post')
Out[35]: []
In [36]: plt.legend(loc='best')

Вы, наверное, обратили внимание на строчки вида . Matplotlib возвращает объекты, ссылающиеся на только что добавленную часть графика. Обычно эту информацию можно смело игнорировать.
В данном случае, поскольку мы передавали функции plot аргумент label, мы
можем с помощью метода plt.legend нанести на график надпись, описывающую каждую линию.
Для создания надписи необходимо вызвать метод plt.legend (или ax.legend,
если вы сохранили ссылки на оси) вне зависимости от того, передавали вы
аргумент label при построении графика или нет.

Краткое введение в API библиотеки matplotlib

281

Рис. 9.7. Линейный график с различными значениями параметра drawstyle

Риски, метки и надписи
Для оформления большинства графиков существует два основных способа:
процедурный интерфейс pyplot (который будет понятен пользователям MATLAB) и собственный объектно-ориентированный matplotlib API.
Интерфейс pyplot, предназначенный для интерактивного использования,
состоит из методов xlim, xticks и xticklabels. Они управляют размером области, занятой графиком, положением и метками рисок соответственно. Использовать их можно двумя способами:
• при вызове без аргументов возвращается текущее значение параметра.
Например, метод plt.xlim() возвращает текущий диапазон значений по
оси X;
• при вызове с аргументами устанавливается новое значение параметра.
Например, в результате вызова plt.xlim([0, 10]) диапазон значений по
оси X устанавливается от 0 до 10.
Все подобные методы действуют на активный или созданный последним
объект AxesSubplot. Каждому из них соответствуют два метода самого объекта
подграфика; в случае xlim это методы ax.get_xlim и ax.set_xlim. Я предпочитаю пользоваться методами экземпляра подграфика, чтобы код получался
понятнее (в особенности когда работаю с несколькими подграфиками), но
вы, конечно, вольны выбирать то, что вам больше нравится.

282

Построение графиков и визуализация

Задание названия графика, названий осей, рисок и их меток
Чтобы проиллюстрировать оформление осей, я создам простой рисунок
и в нем график случайного блуждания (рис. 9.8):
In [37]: fig = plt.figure()
In [38]: ax = fig.add_subplot(1, 1, 1)
In [39]: ax.plot(np.random.randn(1000).cumsum())

Рис. 9.8. Простой график для иллюстрации рисок (с метками)

Для изменения рисок на оси X проще всего воспользоваться методами
set_xticks и set_xticklabels. Первый говорит matplotlib, где в пределах диапазона значений данных ставить риски; по умолчанию их числовые значения
изображаются также и в виде меток. Но можно задать и другие метки с помощью метода set_xticklabels:
In [40]: ticks = ax.set_xticks([0, 250, 500, 750, 1000])
In [41]: labels = ax.set_xticklabels(['one', 'two', 'three', 'four', 'five'],
....:
rotation=30, fontsize='small')

Аргумент rotation устанавливает угол наклона меток рисок к оси x равным
30°. Наконец, метод set_xlabel именует ось x, а метод set_title задает название подграфика (см. окончательный результат на рис. 9.9):
In [42]: ax.set_title('My first matplotlib plot')
Out[42]:
In [43]: ax.set_xlabel('Stages')

Краткое введение в API библиотеки matplotlib

283

Рис. 9.9. Простой график для иллюстрации рисок

Модификация оси y производится точно так же с заменой x на y. В классе
оси имеется метод set, позволяющий задавать сразу несколько свойств графика. Так, предыдущий пример можно записать в следующем виде:
props = {
'title': 'My first matplotlib plot',
'xlabel': 'Stages'
}
ax.set(**props)

Добавление пояснительных надписей
Пояснительная надпись – еще один важный элемент оформления графика.
Добавить ее можно двумя способами. Проще всего передать аргумент label
при добавлении каждого нового графика:
In [44]: from numpy.random import randn
In [45]: fig = plt.figure(); ax = fig.add_subplot(1, 1, 1)
In [46]: ax.plot(randn(1000).cumsum(), 'k', label='one')
Out[46]: []
In [47]: ax.plot(randn(1000).cumsum(), 'k––', label='two')
Out[47]: []
In [48]: ax.plot(randn(1000).cumsum(), 'k.', label='three')
Out[48]: []

284

Построение графиков и визуализация

После этого можно вызвать метод ax.legend() или plt.legend(), и он автоматически создаст пояснительную надпись. Получившийся график показан
на рис. 9.10:
In [49]: ax.legend(loc='best')

Рис. 9.10. Простой график с тремя линиями и пояснительной надписью

Аргумент loc метода legend может принимать и другие значения. Дополнительные сведения см. в строке документации (введите ax.legend?).
Аргумент loc говорит, где поместить надпись. Если вам все равно, задавайте значение 'best', потому что тогда место будет выбрано так, чтобы по
возможности не загораживать сам график. Чтобы исключить из надписи один
или несколько элементов, не задавайте параметр label вовсе или задайте
label='_nolegend_'.

Аннотации и рисование в подграфике
Помимо стандартных типов графиков, разрешается наносить на график
свои аннотации, которые могут содержать текст, стрелки и другие фигуры.
Для добавления аннотаций и текста предназначены функции text, arrow и an­
notate. Функция text наносит на график текст начиная с точки с заданными
координатами (x, y), с факультативной стилизацией:

Краткое введение в API библиотеки matplotlib

285

ax.text(x, y, 'Hello world!',
family='monospace', fontsize=10)

В аннотациях могут встречаться текст и стрелки. В качестве примера построим график цен закрытия по индексу S&P 500 начиная с 2007 года (данные получены с сайта Yahoo! Finance) и аннотируем его некоторыми важными датами, относящимися к финансовому кризису 2008–2009 годов. Проще
всего воспроизвести этот код, введя его в одну ячейку Jupyter-блокнота. Результат изображен на рис. 9.11.
from datetime import datetime
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
data = pd.read_csv('examples/spx.csv', index_col=0, parse_dates=True)
spx = data['SPX']
spx.plot(ax=ax, style='k–')
crisis_data = [
(datetime(2007, 10, 11), 'Peak of bull market'),
(datetime(2008, 3, 12), 'Bear Stearns Fails'),
(datetime(2008, 9, 15), 'Lehman Bankruptcy')
]
for date, label in crisis_data:
ax.annotate(label, xy=(date, spx.asof(date) + 50),
xytext=(date, spx.asof(date) + 200),
arrowprops=dict(facecolor='black'),
horizontalalignment='left', verticalalignment='top')
# Оставить только диапазон 2007–2010
ax.set_xlim(['1/1/2007', '1/1/2011'])
ax.set_ylim([600, 1800])
ax.set_title('Important dates in 2008–2009 financial crisis')

Отметим несколько важных моментов. Метод ax.annotate умеет рисовать
метку в позиции, заданной координатами x и y. Мы воспользовались методами set_xlim и set_ylim, чтобы вручную задать нижнюю и верхнюю границы
графика, а не полагаться на умолчания matplotlib. Наконец, метод ax.set_title
задает название графика в целом.
В галерее matplotlib в Сети есть много других поучительных примеров аннотаций.
Для рисования фигур требуется больше усилий. В matplotlib имеются объекты, соответствующие многим стандартным фигурам, они называются патчами (patches). Часть из них, например Rectangle и Circle, находится в модуле
matplotlib.pyplot, а весь набор – в модуле matplotlib.patches.

286

Построение графиков и визуализация

Рис. 9.11. Важные даты, относящиеся к финансовому кризису 2008–2009 годов

Чтобы поместить на график фигуру, мы создаем объект патча shp и добавляем его в подграфик, вызывая метод ax.add_patch(shp) (см. рис. 9.12):
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
rect = plt.Rectangle((0.2, 0.75), 0.4, 0.15, color='k', alpha=0.3)
circ = plt.Circle((0.7, 0.2), 0.15, color='b', alpha=0.3)
pgon = plt.Polygon([[0.15, 0.15], [0.35, 0.4], [0.2, 0.6]],
color='g', alpha=0.5)
ax.add_patch(rect)
ax.add_patch(circ)
ax.add_patch(pgon)

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

Сохранение графиков в файле
Активный рисунок можно сохранить в файле методом plt.savefig. Этот
метод эквивалентен методу экземпляра рисунка savefig. Например, чтобы
сохранить рисунок в формате SVG, достаточно указать только имя файла:
plt.savefig('figpath.svg')

Формат выводится из расширения имени файла. Если бы мы задали файл
с расширением .pdf, то рисунок был бы сохранен в формате PDF. При публикации графики я часто использую два параметра: dpi (разрешение в точках на

Краткое введение в API библиотеки matplotlib

287

дюйм) и bbox_inches (размер пустого места вокруг рисунка). Чтобы получить
тот же самый график в формате PNG с минимальным обрамлением и разрешением 400 DPI, нужно было бы написать:
plt.savefig('figpath.png', dpi=400, bbox_inches='tight')

Рис. 9.12. Визуализация данных, составленная из трех разных патчей

Метод savefig может писать не только на диск, а в любой похожий на файл
объект, например StringIO:
from io import StringIO
buffer = StringIO()
plt.savefig(buffer)
plot_data = buffer.getvalue()

В табл. 9.2 перечислены некоторые другие аргументы метода savefig.
Таблица 9.2. Аргументы метода Figure.savefig
Аргумент

Описание

fname

Строка, содержащая путь к файлу или похожий на файл объект Python. Формат
рисунка определяется по расширению имени файла, например: PDF для .pdf
и PNG для .png
Разрешение рисунка в точках на дюйм; по умолчанию 100, но может
настраиваться
Цвет фона рисунка вне области, занятой подграфиками. По умолчанию 'w'
(белый)
Явно заданный формат файла ('png', 'pdf', 'svg', 'ps', 'eps' и т. д.)
Какую часть рисунка сохранять. Если задано значение 'tight', то метод
пытается обрезать все пустое место вокруг рисунка

dpi
facecolor, edgecolor
format
bbox_inches

288

Построение графиков и визуализация

Конфигурирование matplotlib
В начальной конфигурации matplotlib заданы цветовые схемы и умолчания,
ориентированные главным образом на подготовку рисунков к публикации. По
счастью, почти все аспекты поведения по умолчанию можно сконфигурировать с помощью обширного набора глобальных параметров, определяющих
размер рисунка, промежутки между подграфиками, цвета, размеры шрифтов,
стили сетки и т. д. Есть два основных способа работы с системой конфигурирования matplotlib. Первый – программный, с помощью метода rc. Например,
чтобы глобально задать размер рисунка равным 10×10, нужно написать:
plt.rc('figure', figsize=(10, 10))

Первый аргумент rc – настраиваемый компонент, например: 'figure', 'ax­
es', 'xtick', 'ytick', 'grid', 'legend' и т. д. Вслед за ним идут позиционные
аргументы, задающие параметры этого компонента. В программе описывать
параметры проще всего в виде словаря:
font_options = {'family' : 'monospace',
'weight' : 'bold',
'size' : 'small'}
plt.rc('font', **font_options)

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

9.2. Построение графиков с помощью pandas
и seaborn
Библиотека matplotlib – средство довольно низкого уровня. График собирается из базовых компонентов: способ отображения данных (тип графика:
линейный график, столбчатая диаграмма, коробчатая диаграмма, диаграмма
рассеяния, контурный график и т. д.), пояснительная надпись, название, метки рисок и прочие аннотации.
В библиотеке pandas может быть несколько столбцов данных, а с ними
метки строк и метки столбцов. В саму pandas встроены методы построения,
упрощающие создание визуализаций объектов DataFrame и Series. Существует другая библиотека, seaborn, разработанная Майклом Уэскомом (Michael
Waskom) для создания статистических графиков. Seaborn упрощает создание
многих типов визуализаций.

Построение графиков с помощью pandas и seaborn

289

В результате импорта seaborn изменяются принятые в matplotlib по умолчанию схемы цветов и стили графиков – чтобы повысить удобочитаемость
и улучшить эстетическое восприятие. Даже если вы не собираетесь использовать API seaborn, все равно стоит импортировать ее, чтобы улучшить внешний
вид графиков matplotlib.

Линейные графики
У объектов Series и DataFrame имеется метод plot, который умеет строить графики разных типов. По умолчанию он строит линейные графики (см.
рис. 8.13):
In [60]: s = pd.Series(np.random.randn(10).cumsum(), index=np.arange(0, 100, 10))
In [61]: s.plot()

Рис. 9.13. Простой пример графика для объекта Series

Индекс объекта Series передается matplotlib для нанесения рисок на ось x,
но это можно отключить, задав параметр use_index=False. Риски и диапазон
значений на оси x можно настраивать с помощью параметров xticks и xlim,
а на оси y – с помощью параметров yticks и ylim. Полный перечень параметров метода plot приведен в табл. 9.3. О некоторых я расскажу в этом разделе,
а остальные оставлю вам для самостоятельного изучения.
Большинство методов построения графиков в pandas принимает необязательный параметр ax – объект подграфика matplotlib. Это позволяет гибко
расположить подграфики в сетке.

290

Построение графиков и визуализация

Таблица 9.3. Параметры метода Series.plot
Аргумент

Описание

label
ax

Метка для пояснительной надписи на графике
Объект подграфика matplotlib, внутри которого строится график. Если параметр не задан,
то используется активный подграфик
Строка стиля, например 'ko––', которая передается matplotlib
Уровень непрозрачности графика (число от 0 до 1)
Может принимать значения 'line', 'bar', 'barh', 'kde'
Использовать логарифмический масштаб по оси y
Брать метки рисок из индекса объекта
Угол поворота меток рисок (от 0 до 360)
Значения рисок на оси x
Значения рисок на оси y
Границы по оси x (например, [0, 10])
Границы по оси y
Отображать координатную сетку (по умолчанию включено)

style
alpha
kind
logy
use_index
rot
xticks
yticks
xlim
ylim
grid

Метод plot объекта DataFrame строит отдельные графики каждого столбца
внутри одного подграфика и автоматически создает пояснительную надпись
(см. рис. 9.14).
In [62]: df = pd.DataFrame(np.random.randn(10, 4).cumsum(0),
....: columns=['A', 'B', 'C', 'D'],
....: index=np.arange(0, 100, 10))
In [63]: df.plot()

Рис. 9.14. Простой пример графика для объекта DataFrame

Построение графиков с помощью pandas и seaborn

291

Атрибут plot содержит «семейство» методов для различных типов графиков. Например, df.plot() эквивалентно df.plot.line(). Некоторые из этих методов мы рассмотрим ниже.
Дополнительные именованные аргументы метода plot без изменения передаются соответствующей функции matplotlib, поэтому, внимательно изучив API
matplotlib, вы сможете настраивать графики более точно.

У объекта DataFrame есть ряд параметров, которые гибко описывают обработку столбцов. Например, поясняют, где нужно строить их графики – внутри одного и того же подграфика или внутри разных подграфиков. Все они
перечислены в табл. 9.4.
Таблица 9.4. Параметры метода DataFrame.plot
Аргумент

Описание

subplots
sharex
sharey
figsize
title
legend
sort_columns

Рисовать график каждого столбца DataFrame в отдельном подграфике
Если subplots=True, то совместно использовать ось x, объединяя риски и границы
Если subplots=True, то совместно использовать ось y
Размеры создаваемого рисунка в виде кортежа
Название графика в виде строки
Помещать в подграфик пояснительную надпись (по умолчанию True)
Строить графики столбцов в алфавитном порядке; по умолчанию используется
существующий порядок столбцов

О построении графиков временных рядов см. главу 11.

Столбчатые диаграммы
Методы plot.bar() и plot.barh() строят соответственно вертикальную
и горизонтальную столбчатые диаграммы. В этом случае индекс Series или
DataFrame будет использоваться для нанесения рисок на ось x (bar) или y
(barh) (см. рис. 9.15):
In [64]: fig, axes = plt.subplots(2, 1)
In [65]: data = pd.Series(np.random.rand(16), index=list('abcdefghijklmnop'))
In [66]: data.plot.bar(ax=axes[0], color='k', alpha=0.7)
Out[66]:
In [67]: data.plot.barh(ax=axes[1], color='k', alpha=0.7)

Аргументы color='k' и alpha=0.7 задают цвет диаграмм (черный) и частичную прозрачность столбиков.
В случае DataFrame значения каждой строки объединяются в группы столбиков, расположенные поодаль друг от друга (рис. 9.16).

292

Построение графиков и визуализация

Рис. 9.15. Примеры горизонтальной и вертикальной столбчатых диаграмм

Рис. 9.16. Столбчатая диаграмма для DataFrame
In [69]: df = DataFrame(np.random.rand(6, 4),
....:
index=['one', 'two', 'three', 'four', 'five', 'six'],
....:
columns=pd.Index(['A', 'B', 'C', 'D'], name='Genus'))

Построение графиков с помощью pandas и seaborn
In [70]: df
Out[70]:
Genus
A
one
0.370670
two
0.420082
three 0.814568
four 0.374020
five 0.433270
six
0.601648

B
0.602792
0.571653
0.277160
0.899420
0.125107
0.478576

C
0.229159
0.049024
0.880316
0.460304
0.494675
0.205690

293

D
0.486744
0.880592
0.431326
0.100843
0.961825
0.560547

In [71]: df.plot.bar()

Обратите внимание, что название столбцов DataFrame – «Genus» – используется в заголовке пояснительной надписи.
Для построения составной столбчатой диаграммы по объекту DataFrame
нужно задать параметр stacked=True, тогда столбики, соответствующие значению в каждой строке, будут приставлены друг к другу (рис. 9.17):
In [73]: df.plot(kind='barh', stacked=True, alpha=0.5)

Рис. 9.17. Составная столбчатая диаграмма для DataFrame
Столбчатые диаграммы полезны для визуализации частоты значений в объекте
Series с применением метода value_counts: s.value_counts().plot.bar().

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

294

Построение графиков и визуализация

In [75]: tips = pd.read_csv('examples/tips.csv')
In [76]: party_counts = pd.crosstab(tips.day, tips.size)
In [77]:
Out[77]:
size 1
day
Fri 1
Sat 2
Sun 0
Thur 1

party_counts
2

3

4 5 6

16 1 1 0
53 18 13 1
39 15 18 3
48 4 5 1

0
0
1
3

# Группы, насчитывающие 1 и 6 гостей, редки
In [71]: party_counts = party_counts.ix[:, 2:5]

Затем нормирую значения, так чтобы сумма в каждой строке была равна
1, и строю график (рис. 9.18):
# Нормировка на сумму 1
In [79]: party_pcts = party_counts.div(party_counts.sum(1), axis=0)
In [80]: party_pcts
Out[80]:
size
2
3
day
Fri 0.888889 0.055556
Sat 0.623529 0.211765
Sun 0.520000 0.200000
Thur 0.827586 0.068966

4

5

0.055556
0.152941
0.240000
0.086207

0.000000
0.011765
0.040000
0.017241

In [81]: party_pcts.plot.bar()

Как видим, в выходные количество гостей в одной группе увеличивается.
Если перед построением графика данные необходимо как-то агрегировать,
то пакет seaborn может существенно упростить жизнь. Посмотрим, как посчитать процент чаевых в зависимости от дня (результат показан на рис. 9.19):
In [83]: import seaborn as sns
In [84]: tips['tip_pct'] = tips['tip'] / (tips['total_bill'] – tips['tip'])
In [85]: tips.head()
Out[85]:
total_bill tip smoker
0
16.99 1.01
No
1
10.34 1.66
No
2
21.01 3.50
No
3
23.68 3.31
No
4
24.59 3.61
No

day
Sun
Sun
Sun
Sun
Sun

time size tip_pct
Dinner
2 0.063204
Dinner
3 0.191244
Dinner
3 0.199886
Dinner
2 0.162494
Dinner
4 0.172069

In [86]: sns.barplot(x='tip_pct', y='day', data=tips, orient='h')

Построение графиков с помощью pandas и seaborn

Рис. 9.18. Распределение по количеству гостей в группе
в каждый день недели

Рис. 9.19. Процент чаевых
в зависимости от дня с доверительными интервалами

295

296

Построение графиков и визуализация

Функции построения графиков из библиотеки seaborn принимают аргумент
data, в роли которого может выступать объект pandas DataFrame. Остальные
аргументы относятся к именам столбцов. Поскольку для каждого значения
в day имеется несколько наблюдений, столбики отражают среднее значение
tip_pct. Черные линии поверх столбиков представляют 95%-ные доверительные интервалы (эту величину можно настроить, задав дополнительные аргументы).
Функция seaborn.barplot принимает аргумент hue, позволяющий произвести
разбиение по дополнительному дискретному значению (рис. 9.20):
In [88]: sns.barplot(x='tip_pct', y='day', hue='time', data=tips, orient='h')

Рис. 9.20. Процент чаевых в зависимости от дня и времени суток

Обратите внимание, что seaborn автоматически изменила внешний вид
диаграмм: цветовую палитру по умолчанию, цвет фона и цвета линий сетки.
Менять внешний вид графиков позволяет функция seaborn.set:
In [90]: sns.set(style="whitegrid")

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

Построение графиков с помощью pandas и seaborn

297

рана мы можем с помощью метода hist объекта Series построить гистограмму
распределения процента чаевых от общей суммы счета (рис. 9.21):
In [92]: tips['tip_pct'].plot.hist(bins=50)

Рис. 9.21. Гистограмма процента чаевых

С гистограммой тесно связан график плотности, который строится на
основе оценки непрерывного распределения вероятности по результатам
измерений. Обычно стремятся аппроксимировать это распределение комбинацией ядер, т. е. более простых распределений, например нормального
(гауссова). Поэтому графики плотности еще называют графиками ядерной
оценки плотности (KDE – kernel density estimate). Функция plot с параметром
kind='kde' строит график плотности, применяя стандартный метод комбинирования нормальных распределений (рис. 9.22):
In [94]: tips['tip_pct'].plot. density()

Seaborn еще упрощает построение гистограмм и графиков плотности благодаря методу distplot, который может строить одновременно гистограмму
и непрерывную оценку плотности. В качестве примера рассмотрим бимодальное распределение, содержащее выборки из двух разных стандартных
нормальных распределений (рис. 9.23):
In [96]: comp1 = np.random.normal(0, 1, size=200)
In [97]: comp2 = np.random.normal(10, 2, size=200)
In [98]: values = pd.Series(np.concatenate([comp1, comp2]))
In [99]: sns.distplot(values, bins=100, color='k')

298

Построение графиков и визуализация

Рис. 9.22. График плотности процента чаевых

Рис. 9.23. Нормированная гистограмма
и оценка плотности смеси нормальных распределений

Построение графиков с помощью pandas и seaborn

299

Диаграммы рассеяния
Диаграмма рассеяния, или точечная диаграмма, – полезный способ исследования соотношения между двумя одномерными рядами данных. Для демонстрации я загрузил набор данных macrodata из проекта statsmodels, выбрал
несколько переменных и вычислил логарифмические разности:
In [100]: macro = pd.read_csv('examples/macrodata.csv')
In [101]: data = macro[['cpi', 'm1', 'tbilrate', 'unemp']]
In [102]: trans_data = np.log(data).diff().dropna()
In [103]: trans_data[–5:]
Out[103]:
cpi
m1 tbilrate
198 –0.007904 0.045361 –0.396881
199 –0.021979 0.066753 –2.277267
200 0.002340 0.010286 0.606136
201 0.008419 0.037461 –0.200671
202 0.008894 0.012202 –0.405465

unemp
0.105361
0.139762
0.160343
0.127339
0.042560

Затем мы можем использовать метод regplot из библиотеки seaborn, чтобы
построить диаграмму рассеяния и аппроксимирующую ее прямую линейной
регрессии (рис. 9.24):
In [105]: sns.regplot('m1', 'unemp', data=trans_data)
Out[105]:
In [106]: plt.title('Changes in log %s versus log %s' % ('m1', 'unemp'))

Рис. 9.24. Диаграмма рассеяния с прямой регрессии, построенная средствами seaborn

300

Построение графиков и визуализация

В разведочном анализе данных полезно видеть все диаграммы рассеяния
для группы переменных; это называется диаграммой пар, или матрицей диаграмм рассеяния. Построение такого графика с нуля – довольно утомительное
занятие, поэтому в seaborn имеется функция pairplot, которая поддерживает
размещение гистограмм или графиков оценки плотности каждой переменной вдоль диагонали (см. результирующий график на рис. 9.25):
In [107]: sns.pairplot(trans_data, diag_kind='kde', plot_kws={'alpha': 0.2})

Рис. 9.25. Матрица диаграмм рассеяния
для набора данных macrodata из проекта statsmodels

Построение графиков с помощью pandas и seaborn

301

Обратите внимание на аргумент plot_kws. Он позволяет передавать конфигурационные параметры отдельным вызовам построения во внедиагональных элементах. Дополнительные сведения о конфигурационных параметрах
см. в строке документации seaborn.pairplot.

Фасетные сетки и категориальные данные
Как быть с наборами данных, в которых имеются дополнительные группировочные измерения? Один из способов визуализировать данные с большим
числом категориальных переменных – воспользоваться фасетной сеткой.
В seaborn имеется полезная функция factorplot, которая упрощает построение разнообразных фасетных графиков (рис. 9.26):
In [108]: sns.factorplot(x='day', y='tip_pct', hue='time', col='smoker',
.....:
kind='bar', data=tips[tips.tip_pct < 1])

Рис. 9.26. Процент чаевых в зависимости от дня, времени суток и курения

Вместо того чтобы использовать для группировки по 'time' столбики разных цветов внутри фасеты, мы можем расширить фасетную сетку, добавив
по одной строке на каждое значение time (рис. 9.27):
In [109]: sns.factorplot(x='day', y='tip_pct', row='time',
.....:
col='smoker',
.....:
kind='bar', data=tips[tips.tip_pct < 1])

302

Построение графиков и визуализация

Рис. 9.27. Процент чаевых в зависимости от дня;
фасеты по времени суток и курению

Функция factorplot поддерживает и другие типы графиков, которые могут
оказаться полезны в зависимости от того, что мы пытаемся показать. Например, диаграммы размаха (на которых изображаются медиана, квартили и выбросы) могут оказаться эффективным средством визуализации (рис. 9.28):
In [110]: sns.factorplot(x='tip_pct', y='day', kind='box',
.....:
data=tips[tips.tip_pct < 0.5])

С помощью более общего класса seaborn.FacetGrid можно создавать собственные фасетные сетки. Дополнительные сведения см. в документации по
seaborn: https://seaborn.pydata.org/.

Заключение

303

9.3. Другие средства визуализации для Python
Как обычно бывает в проектах с открытым исходным кодом, в средствах создания графики для Python нехватки не ощущается (их слишком много, чтобы
все перечислить). Начиная с 2010 года усилия разработчиков были сосредоточены на создании интерактивной графики для публикации в вебе. Благодаря таким инструментам, как Bokeh (http://bokeh.pydata.org/) и Plotly (https://
github.com/plotly/plotly.py), стало возможно описывать на Python динамичную
интерактивную графику, ориентированную на отображение в браузере.
Если вы создаете статическую графику для печати или для веба, то рекомендую начать с matplotlib и таких дополнительных библиотек, как pandas
и seaborn. Если же требования к визуализации иные, то полезно изучить какой-нибудь из других имеющихся в Сети инструментов. Настоятельно советую изучать экосистему, потому что она продолжает развиваться и устремлена в будущее.

9.4. Заключение
В этой главе нашей целью было познакомиться с основными средствами визуализации на основе pandas, matplotlib и seaborn. Если наглядное представление результатов анализа данных важно для вашей работы, то рекомендую
почитать еще что-нибудь об эффективной визуализации данных. Это активная область исследований, поэтому вы найдете немало отличных учебных
ресурсов как в Сети, так и в реальности.
В следующей главе займемся агрегированием данных и операциями группировки в pandas.

Глава 10. Агрегирование данных
и групповые операции
Разбиение набора данных на группы и применение некоторой функции
к каждой группе, будь то в целях агрегирования или преобразования, зачастую является одной из важнейших операций анализа данных. После загрузки, слияния и подготовки набора данных обычно вычисляют статистику по
группам или, возможно, сводные таблицы для построения отчета или визуализации. В библиотеке pandas имеется гибкий и быстрый механизм groupby,
который позволяет формировать продольные и поперечные срезы, а также
агрегировать наборы данных естественным образом.
Одна из причин популярности реляционных баз данных и языка SQL
(структурированного языка запросов) – простота соединения, фильтрации,
преобразования и агрегирования данных. Однако в том, что касается групповых операций, языки запросов типа SQL несколько ограничены. Как мы увидим, выразительность и мощь языка Python и библиотеки pandas позволяют
выполнять гораздо более сложные групповые операции с помощью функций,
принимающих произвольный объект pandas или массив NumPy. В этой главе
мы изучим следующие возможности:
• разделение объекта pandas на части по одному или нескольким ключам
(в виде функций, массивов или имен столбцов объекта DataFrame);
• вычисление групповых статистик, например общего количества, среднего, стандартного отклонения или определенной пользователем функции;
• применение переменного множества функций к каждому столбцу
DataFrame;
• применение внутригрупповых преобразований или иных манипуляций,
например нормировки, линейной регрессии, ранжирования или выборки подмножества;

305

Механизм GroupBy

• вычисление сводных таблиц и перекрестное табулирование;
• квантильный анализ и другие виды статистического анализа групп.
Частный случай механизма groupby, агрегирование временных рядов, в этой
книге называется передискретизацией и рассматривается отдельно в главе 11.

10.1. Механизм GroupBy
Хэдли Уикхэм (Hadley Wickham), автор многих популярных пакетов на языке
программирования R, предложил для групповых операций термин разделение-применение-объединение, который, как мне кажется, удачно описывает
процесс. На первом этапе данные, хранящиеся в объекте pandas, будь то
Series, DataFrame или что-то еще, разделяются на группы по одному или
нескольким указанным вами ключам. Разделение производится вдоль одной
оси объекта. Например, DataFrame можно группировать по строкам (axis=0)
или по столбцам (axis=1). Затем к каждой группе применяется некоторая
функция, которая порождает новое значение. Наконец, результаты применения всех функций объединяются в результирующий объект. Форма результирующего объекта обычно зависит от того, что именно проделывается
с данными. На рис. 10.1 показан схематический пример простого группового
агрегирования.
ключ

данные

Разделение Применение

Объединение

Рис. 10.1. Пример группового агрегирования

Ключи группировки могут задаваться по-разному и необязательно должны
быть одного типа:

306

Агрегирование данных и групповые операции

• список или массив значений той же длины, что ось, по которой производится группировка;
• значение, определяющее имя столбца объекта DataFrame;
• словарь или объект Series, определяющий соответствие между значениями на оси группировки и именами групп;
• функция, которой передается индекс оси или отдельные метки из этого
индекса.
Отметим, что последние три метода – просто различные способы порождения массива значений, используемого далее для разделения объекта на
группы. Не пугайтесь, если это кажется слишком абстрактным. В данной главе
будут приведены многочисленные примеры каждого метода. Для начала рассмотрим очень простой табличный набор данных, представленный в виде
объекта DataFrame:
In [10]: df = pd.DataFrame({'key1' : ['a', 'a', 'b', 'b', 'a'],
....:
'key2' : ['one', 'two', 'one', 'two', 'one'],
....:
'data1' : np.random.randn(5),
....:
'data2' : np.random.randn(5)})
In [11]: df
Out[11]:
data1
0 –0.204708
1 0.478943
2 –0.519439
3 –0.555730
4 1.965781

data2 key1 key2
1.393406
a one
0.092908
a two
0.281746
b one
0.769023
b two
1.246435
a one

Пусть требуется вычислить среднее по столбцу data1, используя метки групп
в столбце key1. Сделать это можно несколькими способами. Первый – взять
столбец data1 и вызвать метод groupby, передав ему столбец (объект Series) key1:
In [12]: grouped = df['data1'].groupby(df['key1'])
In [13]: grouped
Out[13]:

Переменная grouped – это объект GroupBy. Пока что он не вычислил ничего,
кроме промежуточных данных о групповом ключе df['key1']. Идея в том, что
этот объект хранит всю информацию, необходимую для последующего применения некоторой операции к каждой группе. Например, чтобы вычислить
средние по группам, мы можем вызвать метод mean объекта GroupBy:
In [14]: grouped.mean()
Out[14]:
key1
a
0.746672
b –0.537585
Name: data1, dtype: float64

Механизм GroupBy

307

Позже я подробнее объясню, что происходит при вызове .mean(). Важно,
что данные (объект Series) агрегированы по групповому ключу и в результате создан новый объект Series, индексированный уникальными значениями
в столбце key1. Получившийся индекс назван 'key1', потому что так именовался столбец df['key1'] объекта DataFrame.
Если бы мы передали несколько массивов в виде списка, то получили бы
другой результат:
In [15]: means = df['data1'].groupby([df['key1'], df['key2']]).mean()
In [16]: means
Out[16]:
key1
key2
a one 0.880536
two 0.478943
b one –0.519439
two –0.555730
Name: data1, dtype: float64

В этом случае данные сгруппированы по двум ключам, а у результирующего объекта Series имеется иерархический индекс, который состоит из уникальных пар значений ключей, встретившихся в исходных данных:
In [20]: means.unstack()
Out[20]:
key2
one
two
key1
a 0.880536 0.478943
b –0.519439 –0.555730

В этом примере групповыми ключами были объекты Series, но можно было
бы использовать и произвольные массивы правильной длины:
In [18]: states = np.array(['Ohio', 'California', 'California', 'Ohio', 'Ohio'])
In [19]: years = np.array([2005, 2005, 2006, 2005, 2006])
In [20]: df['data1'].groupby([states, years]).mean()
Out[20]:
California 2005 0.478943
2006 –0.519439
Ohio
2005 –0.380219
2006 1.965781
Name: data1, dtype: float64

Часто информация о группировке находится в том же объекте DataFrame,
что и группируемые данные. В таком случае в качестве групповых ключей
можно передать имена столбцов (не важно, что они содержат – строки, числа
или другие объекты Python):
In [21]: df.groupby('key1').mean()

308

Агрегирование данных и групповые операции

Out[21]:
data1
data2
key1
a 0.746672 0.910916
b –0.537585 0.525384
In [22]: df.groupby(['key1', 'key2']).mean()
Out[22]:
data1
data2
key1 key2
a one 0.880536 1.319920
two 0.478943 0.092908
b one –0.519439 0.281746
two –0.555730 0.769023

Вероятно, вы обратили внимание, что в первом случае – df.groupby('key1').
mean() – результат не содержал столбца key2. Поскольку df['key2'] содержит
нечисловые данные, говорят, что это посторонний столбец, и в результат не
включают. По умолчанию агрегируются все числовые столбцы, хотя можно
выбрать и некоторое их подмножество, как мы вскоре увидим.
Вне зависимости от цели использования groupby у объекта GroupBy есть
полезный метод size, который возвращает объект Series, содержащий размеры групп:
In [23]: df.groupby(['key1', 'key2']).size()
Out[23]:
key1 key2
a one
2
two
1
b one
1
two
1
dtype: int64

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

Обход групп
Объект GroupBy поддерживает итерирование, в результате которого генерируется последовательность 2-кортежей, содержащих имя группы и блок
данных. Рассмотрим следующий небольшой набор данных:
In [24]: for
....:
....:
....:
a
data1
0 –0.204708

name, group in df.groupby('key1'):
print name
print group

data2 key1 key2
1.393406
a one

309

Механизм GroupBy
1
4
b

0.478943 0.092908
1.965781 1.246435

a
a

two
one

data1
data2 key1 key2
2 –0.519439 0.281746
b one
3 –0.555730 0.769023
b two

В случае нескольких ключей первым элементом кортежа будет кортеж, содержащий значения ключей:
In [25]: for
....:
....:
....:
('a', 'one')
data1
0 –0.204708
4 1.965781
('a', two')
data1
1 0.478943
('b', 'one')
data1
2 –0.519439
('b', two')
data1
3 –0.555730

(k1, k2), group in df.groupby(['key1', 'key2']):
print k1, k2
print group

data2 key1 key2
1.393406
a one
1.246435
a one
data2 key1 key2
0.092908
a two
data2 key1 key2
0.281746
b one
data2 key1 key2
0.769023
b two

Разумеется, только вам решать, что делать с блоками данных. Возможно,
пригодится следующий однострочный код, который строит словарь блоков:
In [26]: pieces = dict(list(df.groupby('key1')))
In [27]: pieces['b']
Out[27]:
data1
data2 key1 key2
2 –0.519439 0.281746
b one
3 –0.555730 0.769023
b two

По умолчанию метод groupby группирует по оси axis=0, но можно задать
любую другую ось. Например, в нашем примере столбцы объекта df можно
было бы сгруппировать по dtype:
In [28]: df.dtypes
Out[28]:
data1 float64
data2 float64
key1
object
key2
object
dtype: object
In [32]: grouped = df.groupby(df.dtypes, axis=1)

310

Агрегирование данных и групповые операции

Группы можно распечатать следующим образом:
In [30]: for
....:
....:
....:
float64
data1
0 –0.204708
1 0.478943
2 –0.519439
3 –0.555730
4 1.965781
object
key1 key2
0
a one
1
a two
2
b one
3
b two
4
a one

dtype, group in grouped:
print(dtype)
print(group)

data2
1.393406
0.092908
0.281746
0.769023
1.246435

Доступ по индексу к объекту GroupBy, полученному группировкой объекта
DataFrame путем задания имени столбца или массива имен столбцов, имеет
тот же эффект, что выборка этих столбцов для агрегирования. Это означает,
что
df.groupby('key1')['data1']
df.groupby('key1')[['data2']]

– синтаксический сахар для:
df['data1'].groupby(df['key1'])
df[['data2']].groupby(df['key1'])

Большие наборы данных обычно желательно агрегировать лишь по немногим столбцам. Например, чтобы в приведенном выше примере вычислить
среднее только по столбцу data2 и получить результат в виде DataFrame, можно было бы написать:
In [31]: df.groupby(['key1', 'key2'])[['data2']].mean()
Out[31]:
data2
key1 key2
a
one 1.319920
two 0.092908
b
one 0.281746
two 0.769023

В результате этой операции доступа по индексу возвращается сгруппированный DataFrame, если передан список или массив, или сгруппированный
Series, если передано только одно имя столбца:

Механизм GroupBy

311

In [32]: s_grouped = df.groupby(['key1', 'key2'])['data2']
In [33]: s_grouped
Out[33]:
In [34]: s_grouped.mean()
Out[34]:
key1 key2
a
one 1.319920
two 0.092908
b
one 0.281746
two 0.769023
Name: data2, dtype: float64

Группировка с помощью словарей и объектов Series
Информацию о группировке можно передавать не только в виде массива.
Рассмотрим еще один объект DataFrame:
In [35]: people = DataFrame(np.random.randn(5, 5),
....:
columns=['a', 'b', 'c', 'd', 'e'],
....:
index=['Joe', 'Steve', 'Wes', 'Jim', 'Travis'])
In [36]: people.ix[2:3, ['b', 'c']] = np.nan # Добавить несколько значений NA
In [37]: people
Out[37]:
Joe
Steve
Wes
Jim
Travis

a
b
c
d
e
1.007189 –1.296221 0.274992 0.228913 1.352917
0.886429 –2.001637 –0.371843 1.669025 –0.438570
–0.539741
NaN
NaN –1.021228 –0.577087
0.124121 0.302614 0.523772 0.000940 1.343810
–0.713544 –0.831154 –2.370232 –1.860761 –0.860757

Теперь предположим, что имеется соответствие между столбцами и группами и нужно просуммировать столбцы для каждой группы:
In [38]: mapping = {'a': 'red', 'b': 'red', 'c': 'blue',
....:
'd': 'blue', 'e': 'red', 'f' : 'orange'}

Из этого словаря нетрудно построить массив и передать его groupby, но
можно передать и сам словарь (я включил ключ 'f', чтобы показать, что неиспользуемые ключи группировки не составляют проблемы):
In [39]: by_column = people.groupby(mapping, axis=1)
In [40]: by_column.sum()
Out[40]:
blue
red
Joe
0.503905 1.063885
Steve
1.297183 –1.553778
Wes
–1.021228 –1.116829

312

Агрегирование данных и групповые операции

Jim
0.524712 1.770545
Travis –4.230992 –2.405455

То же самое относится к объекту Series, который можно рассматривать как
отображение фиксированного размера.
In [41]: map_series = Series(mapping)
In [42]: map_series
Out[42]:
a
red
b
red
c
blue
d
blue
e
red
f orange
dtype: object
In [43]: people.groupby(map_series, axis=1).count()
Out[43]:
blue red
Joe
2
3
Steve
2
3
Wes
1
2
Jim
2
3
Travis
2
3

Группировка с помощью функций
Использование функции Python – более абстрактный способ определения
соответствия групп, по сравнению со словарями или объектами Series. Функция, переданная в качестве группового ключа, будет вызвана по одному разу
для каждого значения в индексе, а возвращенные ей значения станут именами групп. Конкретно рассмотрим объект DataFrame из предыдущего раздела,
где значениями индекса являются имена людей. Пусть требуется сгруппировать по длине имени. Можно было бы вычислить массив длин строк, но
лучше просто передать функцию len:
In [44]: people.groupby(len).sum()
Out[44]:
a
b
c
d
e
3 0.591569 –0.993608 0.798764 –0.791374 2.119639
5 0.886429 –2.001637 –0.371843 1.669025 –0.438570
6 –0.713544 –0.831154 –2.370232 –1.860761 –0.860757

Использование вперемежку функций, массивов, словарей и объектов Series
вполне допустимо, потому что внутри все преобразуется в массивы:
In [45]: key_list = ['one', 'one', 'one', 'two', 'two']
In [46]: people.groupby([len, key_list]).min()

313

Агрегирование данных
Out[46]:
3 one
two
5 one
6 two

a
–0.539741
0.124121
0.886429
–0.713544

b
–1.296221
0.302614
–2.001637
–0.831154

c
0.274992
0.523772
–0.371843
–2.370232

d
–1.021228
0.000940
1.669025
–1.860761

e
–0.577087
1.343810
–0.438570
–0.860757

Группировка по уровням индекса
Наконец, иерархически индексированные наборы данных можно агрегировать по одному из уровней индекса оси. Рассмотрим пример:
In [47]: columns = pd.MultiIndex.from_arrays([['US', 'US', 'US', 'JP', 'JP'],
....:
[1, 3, 5, 1, 3]], names=['cty', 'tenor'])
In [48]: hier_df = DataFrame(np.random.randn(4, 5), columns=columns)
In [49]: hier_df
Out[49]:
cty
US
tenor
1
0
0.560145
1
–2.359419
2
0.286350
3
0.069877

JP
3
5
1
3
–1.265934 0.119827 –1.063512 0.332883
–0.199543 –1.541996 –0.970736 –1.307030
0.377984 –0.753887 0.331286 1.349742
0.246674 –0.011862 1.004812 1.327195

Для группировки по уровню нужно передать номер или имя уровня в именованном параметре level:
In [50]:
Out[50]:
cty JP
0
2
1
2
2
2
3
2

hier_df.groupby(level='cty', axis=1).count()
US
3
3
3
3

10.2. Агрегирование данных
Под агрегированием я обычно понимаю любое преобразование данных, которое порождает скалярные значения из массивов. В примерах выше мы
встречали несколько таких преобразований: mean, count, min и sum. Вероятно,
вам интересно, что происходит при вызове mean() для объекта GroupBy. Реализации многих стандартных операций агрегирования, в частности перечисленных в табл. 10.1, оптимизированы. Однако необязательно ограничиваться
только этими методами.
Вы можете придумать собственные способы агрегирования и, кроме того,
вызвать любой метод, определенный для сгруппированного объекта. Напри-

314

Агрегирование данных и групповые операции

мер, метод quantile вычисляет выборочные квантили для объекта Series или
столбцов объекта DataFrame:
Таблица 10.1. Оптимизированные функции агрегирования
Имя функции

Описание

count
sum
mean
median

Количество отличных от NA значений в группе
Сумма отличных от NA значений
Среднее отличных от NA значений
Медиана отличных от NA значений
Несмещенное (со знаменателем n – 1) стандартное отклонение и дисперсия
Минимальное и максимальное отличные от NA значения
Произведение отличных от NA значений
Первое и последнее отличные от NA значения

std, var
min, max
prod
first, last

Несмотря на то что в классе GroupBy метод quantile не реализован, он
есть в классе Series и потому может быть использован. На самом деле объект
GroupBy разбивает Series на части, вызывает piece.quantile(0.9) для каждой
части, а затем собирает результаты в итоговый объект.
In [51]: df
Out[51]:
data1
0 –0.204708
1 0.478943
2 –0.519439
3 –0.555730
4 1.965781

data2 key1 key2
1.393406
a one
0.092908
a two
0.281746
b one
0.769023
b two
1.246435
a one

In [52]: grouped = df.groupby('key1')
In [53]: grouped['data1'].quantile(0.9)
Out[53]:
key1
a
1.668413
b
–0.523068
Name: data1, dtype: float64

Для использования собственных функций агрегирования передайте функцию, агрегирующую массив, методу aggregate или agg:
In [54]:def peak_to_peak(arr):
....:
return arr.max() – arr.min()
In [55]: grouped.agg(peak_to_peak)
Out[55]:
data1
data2
key1
a 2.170488 1.300498
b 0.036292 0.487276

315

Агрегирование данных

Отметим, что некоторые методы, например describe, тоже работают, хотя,
строго говоря, и не являются операциями агрегирования:
In [56]: grouped.describe()
Out[56]:
data1
count
mean
std
key1
a
3.0 0.746672 1.109736
b
2.0 –0.537585 0.025662
data2
max count
mean
key1
a
1.965781
3.0 0.910916
b –0.519439
2.0 0.525384

\
min

25%

50%

75%

–0.204708 0.137118 0.478943 1.222362
–0.555730 –0.546657 –0.537585 –0.528512
\
std

min

25%

50%

0.712217 0.092908 0.669671 1.246435
0.344556 0.281746 0.403565 0.525384

75%
max
key1
a
1.319920 1.393406
b
0.647203 0.769023

Что здесь произошло, я объясню подробнее в разделе 10.3.
В общем случае пользовательские функции агрегирования работают гораздо
медленнее оптимизированных функций из табл. 10.1. Это объясняется большими накладными расходами (на вызовы функций и реорганизацию данных)
при построении промежуточных блоков данных, относящихся к каждой группе.

Применение функций, зависящих от столбца
и нескольких функций
Вернемся к набору данных о чаевых, который уже встречался нам ранее.
После загрузки функцией read_csv добавим в него столбец процента чаевых
tip_pct:
In [57]: tips = pd.read_csv('examples/tips.csv')
# Добавить величину чаевых в виде процента от суммы счета
In [58]: tips['tip_pct'] = tips['tip'] / tips['total_bill']
In [59]: tips[:6]
Out[59]:
total_bill tip
sex smoker day
0
16.99 1.01 Female
No Sun
1
10.34 1.66
Male
No Sun
2
21.01 3.50
Male
No Sun
3
23.68 3.31
Male
No Sun
4
24.59 3.61 Female
No Sun
5
25.29 4.71 Male
No Sun

time size tip_pct
Dinner
2 0.059447
Dinner
3 0.160542
Dinner
3 0.166587
Dinner
2 0.139780
Dinner
4 0.146808
Dinner
4 0.186240

316

Агрегирование данных и групповые операции

Как мы уже видели, для агрегирования объекта Series или всех столбцов
объекта DataFrame достаточно воспользоваться методом aggregate, передав
ему требуемую функцию, или вызвать метод mean, std и им подобный. Однако
иногда нужно использовать разные функции в зависимости от столбца или
сразу несколько функций. К счастью, сделать это совсем нетрудно, что я и
продемонстрирую в следующих примерах. Для начала сгруппирую столбец
tips по значениям sex и smoker:
In [60]: grouped = tips.groupby(['day', 'smoker'])

Отмечу, что в случае описательных статистик типа тех, что приведены
в табл. 10.1, можно передать имя функции в виде строки:
In [61]: grouped_pct = grouped['tip_pct']
In [62]: grouped_pct.agg('mean')
Out[62]:
day smoker
Fri No
0.151650
Yes
0.174783
Sat No
0.158048
Yes
0.147906
Sun No
0.160113
Yes
0.187250
Thur No
0.160298
Yes
0.163863
Name: tip_pct, dtype: float64

Если вместо этого передать список функций или имен функций, то будет
возвращен объект DataFrame, в котором имена столбцов совпадают с именами функций:
In [63]: grouped_pct.agg(['mean', 'std', peak_to_peak])
Out[63]:
mean
std peak_to_peak
day smoker
Fri No
0.151650 0.028123
0.067349
Yes
0.174783 0.051293
0.159925
Sat No
0.158048 0.039767
0.235193
Yes
0.147906 0.061375
0.290095
Sun No
0.160113 0.042347
0.193226
Yes
0.187250 0.154134
0.644685
Thur No
0.160298 0.038774
0.193350
Yes
0.163863 0.039389
0.151240

Здесь мы передали список функций агрегирования методу agg, который
независимо вычисляет агрегаты для групп данных.
Совершенно необязательно соглашаться с именами столбцов, предложенными объектом GroupBy; в частности, все лямбда-функции называются
'', поэтому различить их затруднительно (можете убедиться сами,
распечатав атрибут функции __name__). Поэтому если передать список корте-

317

Агрегирование данных

жей вида (name, function), то в качестве имени столбца DataFrame будет взят
первый элемент кортежа (можно считать, что список 2-кортежей – упорядоченное отображение):
In [64]: grouped_pct.agg([('foo', 'mean'), ('bar', np.std)])
Out[64]:
foo
bar
day smoker
Fri No
0.151650 0.028123
Yes
0.174783 0.051293
Sat No
0.158048 0.039767
Yes
0.147906 0.061375
Sun No
0.160113 0.042347
Yes
0.187250 0.154134
Thur No
0.160298 0.038774
Yes
0.163863 0.039389

В случае DataFrame диапазон возможностей шире, поскольку можно задавать список функций, применяемых ко всем столбцам, или разные функции
для разных столбцов. Допустим, нам нужно вычислить три одинаковые статистики для столбцов tip_pct и total_bill:
In [65]: functions = ['count', 'mean', 'max']
In [66]: result = grouped['tip_pct', 'total_bill'].agg(functions)
In [67]: result
Out[67]:
tip_pct
count
day smoker
Fri No
4
Yes
15
Sat No
45
Yes
42
Sun No
57
Yes
19
Thur No
45
Yes
17

total_bill
max count

mean
0.151650
0.174783
0.158048
0.147906
0.160113
0.187250
0.160298
0.163863

0.187735
0.263480
0.291990
0.325733
0.252672
0.710345
0.266312
0.241255

4
15
45
42
57
19
45
17

mean

max

18.420000
16.813333
19.661778
21.276667
20.506667
24.120000
17.113111
19.190588

22.75
40.17
48.33
50.81
48.17
45.35
41.19
43.11

Как видите, в результирующем DataFrame имеются иерархические столбцы – точно так же, как если бы мы агрегировали каждый столбец по отдельности, а потом склеили результаты с помощью метода concat, передав ему
имена столбцов в качестве аргумента keys:
In [68]: result['tip_pct']
Out[68]:
count
mean
max
day smoker
Fri No
4 0.151650 0.187735
Yes
15 0.174783 0.263480

318
Sat No
Yes
Sun No
Yes
Thur No
Yes

Агрегирование данных и групповые операции
45
42
57
19
45
17

0.158048
0.147906
0.160113
0.187250
0.160298
0.163863

0.291990
0.325733
0.252672
0.710345
0.266312
0.241255

Как и раньше, можно передавать список кортежей, содержащий желаемые
имена:
In [69]: ftuples = [('Durchschnitt', 'mean'), ('Abweichung', np.var)]
In [70]: grouped['tip_pct', 'total_bill'].agg(ftuples)
Out[70]:
tip_pct
total_bill
Durchschnitt Abweichung Durchschnitt Abweichung
day smoker
Fri No
0.151650
0.000791
18.420000 25.596333
Yes
0.174783
0.002631
16.813333 82.562438
Sat No
0.158048
0.001581
19.661778 79.908965
Yes
0.147906
0.003767
21.276667 101.387535
Sun No
0.160113
0.001793
20.506667 66.099980
Yes
0.187250
0.023757
24.120000 109.046044
Thur No
0.160298
0.001503
17.113111 59.625081
Yes
0.163863
0.001551
19.190588 69.808518

Предположим далее, что требуется применить потенциально различные
функции к одному или нескольким столбцам. Делается это путем передачи
методу agg словаря, который содержит отображение имен столбцов на любой
из рассмотренных выше объектов, задающих функции:
In [71]: grouped.agg({'tip' : np.max, 'size' : 'sum'})
Out[71]:
tip size
day smoker
Fri No
3.50
9
Yes
4.73
31
Sat No
9.00 115
Yes
10.00 104
Sun No
6.00 167
Yes
6.50
49
Thur No
6.70 112
Yes
5.00
40
In [72]: grouped.agg({'tip_pct' : ['min', 'max', 'mean', 'std'],
....:
'size' : 'sum'})
Out[72]:
tip_pct
size
min
max
mean
std sum
day smoker
Fri No
0.120385 0.187735 0.151650 0.028123
9
Yes
0.103555 0.263480 0.174783 0.051293 31

Метод apply: часть общего принципа разделения-применения-объединения
Sat No
Yes
Sun No
Yes
Thur No
Yes

0.056797
0.035638
0.059447
0.065660
0.072961
0.090014

0.291990
0.325733
0.252672
0.710345
0.266312
0.241255

0.158048
0.147906
0.160113
0.187250
0.160298
0.163863

0.039767
0.061375
0.042347
0.154134
0.038774
0.039389

319

115
104
167
49
112
40

Объект DataFrame будет содержать иерархические столбцы, только если
хотя бы к одному столбцу было применено несколько функций.

Возврат агрегированных данных без индексов строк
Во всех рассмотренных выше примерах агрегированные данные сопровождались индексом, иногда иерархическим, составленным из уникальных
встретившихся комбинаций групповых ключей. Такое поведение не всегда
желательно, поэтому его можно подавить, передав методу groupby аргумент
as_index=False:
In [73]: tips.groupby(['day', 'smoker'], as_index=False).mean()
Out[73]:
day smoker total_bill
tip
size tip_pct
0 Fri
No 18.420000 2.812500 2.250000 0.151650
1 Fri
Yes 16.813333 2.714000 2.066667 0.174783
2 Sat
No 19.661778 3.102889 2.555556 0.158048
3 Sat
Yes 21.276667 2.875476 2.476190 0.147906
4 Sun
No 20.506667 3.167895 2.929825 0.160113
5 Sun
Yes 24.120000 3.516842 2.578947 0.187250
6 Thur
No 17.113111 2.673778 2.488889 0.160298
7 Thur Yes 19.190588 3.030000 2.352941 0.163863

Разумеется, для получения данных в таком формате всегда можно вызвать
метод reset_index результата. Аргумент as_index=False просто позволяет избежать некоторых лишних вычислений.

10.3. Метод apply: часть общего принципа
разделения-применения-объединения
Самым общим из методов класса GroupBy является apply, ему мы и посвятим
остаток этого раздела. На рис. 10.2 показано, что apply разделяет обрабатываемый объект на части, вызывает для каждой части переданную функцию,
а затем пытается конкатенировать все части вместе.
Возвращаясь к набору данных о чаевых, предположим, что требуется выбрать первые пять значений tip_pct в каждой группе. Прежде всего нетрудно написать функцию, которая отбирает строки с наибольшими значениями
в указанном столбце:
In [74]: def top(df, n=5, column='tip_pct'):
....:
return df.sort_index(by=column)[–n:]

320

Агрегирование данных и групповые операции

In [75]: top(tips, n=6)
Out[75]:
total_bill tip smoker
109
14.31 4.00
Yes
183
23.17 6.50
Yes
232
11.61 3.39
No
67
3.07 1.00
Yes
178
9.60 4.00
Yes
172
7.25 5.15
Yes

ключ

данные

day
Sat
Sun
Sat
Sat
Sun
Sun

time size tip_pct
Dinner
2 0.279525
Dinner
4 0.280535
Dinner
2 0.291990
Dinner
1 0.325733
Dinner
2 0.416667
Dinner
2 0.710345

Разделение Применение

Объединение

Рис. 10.2. Иллюстрация группового агрегирования

Если теперь сгруппировать, например, по столбцу smoker и вызвать метод
apply, передав ему эту функцию, то получим следующее:
In [76]: tips.groupby('smoker').apply(top)
Out[76]:
total_bill tip smoker day
smoker
No
88
24.71 5.85
No Thur
185
20.69 5.00
No Sun
51
10.29 2.60
No Sun
149
7.51 2.00
No Thur
232
11.61 3.39
No Sat
Yes
109
14.31 4.00
Yes Sat
183
23.17 6.50
Yes Sun
67
3.07 1.00
Yes Sat
178
9.60 4.00
Yes Sun
172
7.25 5.15
Yes Sun

time size
Lunch
Dinner
Dinner
Lunch
Dinner
Dinner
Dinner
Dinner
Dinner
Dinner

2
5
2
2
2
2
4
1
2
2

tip_pct
0.236746
0.241663
0.252672
0.266312
0.291990
0.279525
0.280535
0.325733
0.416667
0.710345

Метод apply: часть общего принципа разделения-применения-объединения

321

Что здесь произошло? Функция top вызывается для каждой части DataFrame,
после чего результаты склеиваются методом pandas.concat, а частям сопоставляются метки, совпадающие с именами групп. Поэтому результат имеет
иерархический индекс, внутренний уровень которого содержит индексные
значения из исходного объекта DataFrame.
Если передать методу apply функцию, которая принимает еще какие-то
позиционные или именованные аргументы, то их можно передать вслед за
самой функцией:
In [77]: tips.groupby(['smoker', 'day']).apply(top, n=1, column='total_bill')
Out[77]:
total_bill
tip smoker day
time size tip_pct
smoker day
No
Fri 94
22.75 3.25
No Fri Dinner
2 0.142857
Sat 212
48.33 9.00
No Sat Dinner
4 0.186220
Sun 156
48.17 5.00
No Sun Dinner
6 0.103799
Thur 142
41.19 5.00
No Thur Lunch
5 0.121389
Yes
Fri 95
40.17 4.73
Yes Fri Dinner
4 0.117750
Sat 170
50.81 10.00
Yes Sat Dinner
3 0.196812
Sun 182
45.35 3.50
Yes Sun Dinner
3 0.077178
Thur 197
43.11 5.00
Yes Thur Lunch
4 0.115982
Это лишь простейшие приемы, а вообще, возможности apply ограничены только вашей изобретательностью. Что именно делает переданная функция, решать вам, требуется лишь, чтобы она возвращала объект pandas или скалярное
значение. Далее в этой главе будут в основном примеры, показывающие, как
решать различные задачи с помощью groupby.

Вы, наверное, помните, что выше я вызывал метод describe от имени объекта GroupBy:
In [78]: result = tips.groupby('smoker')['tip_pct'].describe()
In [79]: result
Out[79]:
count
mean
std
min
25%
50%
75% \
smoker
No
151.0 0.159328 0.039910 0.056797 0.136906 0.155625 0.185014
Yes
93.0 0.163196 0.085119 0.035638 0.106771 0.153846 0.195059
max
smoker
No
0.291990
Yes
0.710345
In [80]: result.unstack('smoker')
Out[80]:
smoker
count No
151.000000
Yes
93.000000

322
mean

No
Yes
std
No
Yes
min
No
Yes
25%
No
Yes
50%
No
Yes
75%
No
Yes
max
No
Yes
dtype: float64

Агрегирование данных и групповые операции
0.159328
0.163196
0.039910
0.085119
0.056797
0.035638
0.136906
0.106771
0.155625
0.153846
0.185014
0.195059
0.291990
0.710345

Когда от имени GroupBy вызывается метод типа describe, на самом деле
выполняются такие предложения:
f = lambda x: x.describe()
grouped.apply(f)

Подавление групповых ключей
В примерах выше мы видели, что у результирующего объекта имеется
иерархический индекс, образованный групповыми ключами и индексами
каждой части исходного объекта. Создание этого индекса можно подавить,
передав методу groupby параметр group_keys=False:
In [81]: tips.groupby('smoker', group_keys=False).apply(top)
Out[81]:
total_bill tip smoker day
time size tip_pct
88
24.71 5.85
No Thur Lunch
2 0.236746
185
20.69 5.00
No Sun Dinner
5 0.241663
51
10.29 2.60
No Sun Dinner
2 0.252672
149
7.51 2.00
No Thur Lunch
2 0.266312
232
11.61 3.39
No Sat Dinner
2 0.291990
109
14.31 4.00
Yes Sat Dinner
2 0.279525
183
23.17 6.50
Yes Sun Dinner
4 0.280535
67
3.07 1.00
Yes Sat Dinner
1 0.325733
178
9.60 4.00
Yes Sun Dinner
2 0.416667
172
7.25 5.15
Yes Sun Dinner
2 0.710345

Квантильный и интервальный анализы
Напомним, что в главе 8 шла речь о некоторых средствах библиотеки pandas, в том числе о методах cut и qcut, которые позволяют разложить данные по
ящикам, размер которых задан вами или определяется выборочными квантилями. В сочетании с методом groupby эти методы позволяют очень просто

Метод apply: часть общего принципа разделения-применения-объединения

323

подвергнуть набор данных интервальному или квантильному анализу. Рассмотрим простой набор случайных данных и раскладывание по интервалам
(ящикам) равной длины с помощью cut:
In [82]: frame = pd.DataFrame({'data1': np.random.randn(1000),
....:
'data2': np.random.randn(1000)})
In [83]: quartiles = pd.cut(frame.data1, 4)
In [84]: quartiles[:10]
Out[84]:
0
(–1.23, 0.489]
1
(–2.956, –1.23]
2
(–1.23, 0.489]
3
(0.489, 2.208]
4
(–1.23, 0.489]
5
(0.489, 2.208]
6
(–1.23, 0.489]
7
(–1.23, 0.489]
8
(0.489, 2.208]
9
(0.489, 2.208]
Name: data1, dtype: category
Categories (4, interval[float64]): [(–2.956, –1.23] < (–1.23, 0.489] < (0.489, 2.
208] < (2.208, 3.928]]

Объект Categorical, возвращаемый функцией cut, можно передать непосредственно groupby. Следовательно, набор статистик для столбца data2 можно
вычислить следующим образом:
In [85]: def get_stats(group):
....:
return {'min': group.min(), 'max': group.max(),
....:
'count': group.count(), 'mean': group.mean()}
In [86]: grouped = frame.data2.groupby(factor)
In [87]: grouped.apply(get_stats).unstack()
Out[87]:
count
max
mean
min
data1
(–2.956, –1.23]
95 1.670835 –0.039521 –3.399312
(–1.23, 0.489]
598 3.260383 –0.002051 –2.989741
(0.489, 2.208]
297 2.954439 0.081822 –3.745356
(2.208, 3.928]
10 1.765640 0.024750 –1.929776

Это были интервалы одинаковой длины, а чтобы вычислить интервалы
равного размера на основе выборочных квантилей, нужно использовать
функцию qcut. Я задам параметр labels=False, чтобы получать только номера
квантилей:
# Вернуть номера квантилей
In [88]: grouping = pd.qcut(frame.data1, 10, labels=False)

324

Агрегирование данных и групповые операции

In [89]: grouped = frame.data2.groupby(grouping)
In [90]:
Out[90]:
count
0
100
1
100
2
100
3
100
4
100
5
100
6
100
7
100
8
100
9
100

grouped.apply(get_stats).unstack()
max
1.670835
2.628441
2.527939
3.260383
2.074345
2.184810
2.458842
2.954439
2.735527
2.377020

mean
–0.049902
0.030989
–0.067179
0.065713
–0.111653
0.052130
–0.021489
–0.026459
0.103406
0.220122

min
–3.399312
–1.950098
–2.925113
–2.315555
–2.047939
–2.989741
–2.223506
–3.056990
–3.745356
–2.064111

В главе 12 мы поближе познакомимся с типом pandas Categorical.

Пример: подстановка зависящих от группы значений
вместо отсутствующих
Иногда отсутствующие данные требуется отфильтровать методом dropna,
а иногда восполнить их, подставив либо фиксированное значение, либо значение, зависящее от данных. Для этой цели предназначен метод fillna. Вот,
например, как можно заменить отсутствующие значения средним:
In [91]: s = Series(np.random.randn(6))
In [92]: s[::2] = np.nan
In [93]: s
Out[93]:
0
NaN
1 –0.125921
2
NaN
3 –0.884475
4
NaN
5 0.227290
dtype: float64
In [94]: s.fillna(s.mean())
Out[94]:
0 –0.261035
1 –0.125921
2 –0.261035
3 –0.884475
4 –0.261035
5 0.227290
dtype: float64

Powered by TCPDF (www.tcpdf.org)

Метод apply: часть общего принципа разделения-применения-объединения

325

А что делать, если подставляемое значение зависит от группы? Один из
способов решить задачу – сгруппировать данные и вызвать метод apply, передав ему функцию, которая вызывает fillna для каждого блока данных. Ниже
приведены данные о некоторых штатах США с разделением на восточные
и западные:
In [95]: states = ['Ohio', 'New York', 'Vermont', 'Florida',
.....:
'Oregon', 'Nevada', 'California', 'Idaho']
In [96]: group_key = ['East'] * 4 + ['West'] * 4
In [97]: data = pd.Series(np.random.randn(8), index=states)
In [98]: data
Out[98]:
Ohio
0.922264
New York
–2.153545
Vermont
–0.365757
Florida
–0.375842
Oregon
0.329939
Nevada
0.981994
California 1.105913
Idaho
–1.613716
dtype: float64

Заметим, что выражение ['East'] * 4 порождает список, содержащий четыре
копии элементов в ['East']. Складывание списков означает их конкатенацию.
Сделаем так, чтобы некоторые значения отсутствовали:
In [99]: data[['Vermont', 'Nevada', 'Idaho']] = np.nan
In [100]: data
Out[100]:
Ohio
0.922264
New York
–2.153545
Vermont
NaN
Florida
–0.375842
Oregon
0.329939
Nevada
NaN
California 1.105913
Idaho
NaN
dtype: float64
In [101]: data.groupby(group_key).mean()
Out[101]:
East –0.535707
West 0.717926
dtype: float64

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

326

Агрегирование данных и групповые операции

In [102]: fill_mean = lambda g: g.fillna(g.mean())
In [103]: data.groupby(group_key).apply(fill_mean)
Out[103]:
Ohio
0.922264
New York –2.153545
Vermont
–0.535707
Florida
–0.375842
Oregon
0.329939
Nevada
0.717926
California 1.105913
Idaho
0.717926
dtype: float64

Или, возможно, требуется подставлять вместо отсутствующих значений
фиксированные, но зависящие от группы:
In [104]: fill_values = {'East': 0.5, 'West': –1}
In [105]: fill_func = lambda g: g.fillna(fill_values[g.name])
In [106]: data.groupby(group_key).apply(fill_func)
Out[106]:
Ohio
0.922264
New York –2.153545
Vermont
0.500000
Florida
–0.375842
Oregon
0.329939
Nevada
–1.000000
California 1.105913
Idaho
–1.000000
dtype: float64

Пример: случайная выборка и перестановка
Предположим, что требуется произвести случайную выборку (с возвращением или без оного) из большого набора данных для моделирования методом Монте-Карло или какой-то другой задачи. Существуют разные способы
выборки, одни более эффективны, другие – менее; здесь мы воспользуемся
методом sample для объекта Series.
Для демонстрации сконструируем колоду игральных карт:
# Hearts (черви), Spades (пики), Clubs (трефы), Diamonds (бубны)
suits = ['H', 'S', 'C', 'D']
card_val = (range(1, 11) + [10] * 3) * 4
base_names = ['A'] + range(2, 11) + ['J', 'K', 'Q']
cards = []
for suit in ['H', 'S', 'C', 'D']:
cards.extend(str(num) + suit for num in base_names)
deck = pd.Series(card_val, index=cards)

Метод apply: часть общего принципа разделения-применения-объединения

327

Теперь у нас есть объект Series длины 52, индекс которого содержит названия карт, а значения – ценность карт в блэкджеке и других играх (для
простоты я присвоил тузу значение 1).
In [108]: deck[:13]
Out[108]:
AH 1
2H 2
3H 3
4H 4
5H 5
6H 6
7H 7
8H 8
9H 9
10H 10
JH 10
KH 10
QH 10
dtype: int64

Исходя из сказанного выше, сдать пять карт из колоды можно следующим
образом:
In [109]: def draw(deck, n=5):
.....:
return deck.take(np.random.permutation(len(deck))[:n])
In [110]: draw(deck)
Out[110]:
AD 1
8C 8
5H 5
KC 10
2C 2
dtype: int64

Пусть требуется выбрать по две случайные карты каждой масти. Поскольку
масть обозначается последним символом названия карты, то можно произвести по ней группировку и воспользоваться методом apply:
In [111]: get_suit = lambda card: card[–1] # последняя буква обозначает масть
In [112]: deck.groupby(get_suit).apply(draw, n=2)
Out[112]:
C 2C 2
3C 3
D KD 10
8D 8
H KH 10
3H 3

328

Агрегирование данных и групповые операции

S 2S 2
4S 4
dtype: int64

Можно поступить и по-другому:
In [113]: deck.groupby(get_suit, group_keys=False).apply(draw, n=2)
Out[113]:
KC 10
JC 10
AD 1
5D 5
5H 5
6H 6
7S 7
KS 10
dtype: int64

Пример: групповое взвешенное среднее и корреляция
Принцип разделения-применения-объединения, лежащий в основе groupby,
позволяет легко выразить такие операции между столбцами DataFrame или
двумя объектами Series, как вычисление группового взвешенного среднего.
В качестве примера возьмем следующий набор данных, содержащий групповые ключи, значения и веса:
In [114]: df = DataFrame({'category': ['a', 'a', 'a', 'a', 'b', 'b', 'b', 'b'],
.....:
'data': np.random.randn(8),
.....:
'weights': np.random.rand(8)})
In [115]: df
Out[115]:
category
0
a
1
a
2
a
3
a
4
b
5
b
6
b
7
b

data
1.561587
1.219984
–0.482239
0.315667
–0.047852
–0.454145
–0.556774
0.253321

weights
0.957515
0.347267
0.581362
0.217091
0.894406
0.918564
0.277825
0.955905

Групповое взвешенное среднее по столбцу category равно:
In [116]: grouped = df.groupby('category')
In [117]: get_wavg = lambda g: np.average(g['data'], weights=g['weights'])
In [118]: grouped.apply(get_wavg)
Out[118]:
category
a
0.811643

Метод apply: часть общего принципа разделения-применения-объединения

329

b
–0.122262
dtype: float64

В качестве другого примера рассмотрим набор данных с сайта Yahoo! Finance, содержащий цены дня на некоторые акции и индекс S&P 500 (торговый код SPX):
In [119]: close_px = pd.read_csv('examples/stock_px.csv', parse_dates=True,
....
index_col=0)
In [120]: close_px
Out[120]:

DatetimeIndex: 2214 entries, 2003–01–02 to 2011–10–14
Data columns (total 4 columns):
AAPL 2214 non–null float64
MSFT 2214 non–null float64
XOM
2214 non–null float64
SPX
2214 non–null float64
dtypes: float64(4)
memory usage: 86.5 KB
In [121]: close_px[–4:]
Out[121]:
AAPL MSFT
2011–10–11 400.29 27.00
2011–10–12 402.19 26.96
2011–10–13 408.43 27.18
2011–10–14 422.00 27.27

XOM
76.27
77.16
76.37
78.11

SPX
1195.54
1207.25
1203.66
1224.58

Было бы интересно вычислить объект DataFrame, содержащий годовые
корреляции между суточной доходностью (вычисленной по процентному изменению) и SPX. Один из способов решения этой задачи состоит в том, чтобы
сначала создать функцию, которая вычисляет попарную корреляцию между
каждым столбцом и столбцом 'SPX':
In [122]: spx_corr = lambda x: x.corrwith(x['SPX'])

Затем вычислим процентное изменение в close_px с помощью метода pct_
change:
In [123]: rets = close_px.pct_change().dropna()

И наконец, сгруппируем процентные изменения по годам, которые можно
извлечь из метки строки однострочной функцией, возвращающей атрибут
year метки datetime:
In [124]: get_year = lambda x: x.year
In [125]: by_year = rets.groupby(get_year)
In [126]: by_year.apply(spx_corr)

330

Агрегирование данных и групповые операции

Out[126]:
2003
2004
2005
2006
2007
2008
2009
2010
2011

AAPL
0.541124
0.374283
0.467540
0.428267
0.508118
0.681434
0.707103
0.710105
0.691931

MSFT
0.745174
0.588531
0.562374
0.406126
0.658770
0.804626
0.654902
0.730118
0.800996

XOM SPX
0.661265
1
0.557742
1
0.631010
1
0.518514
1
0.786264
1
0.828303
1
0.797921
1
0.839057
1
0.859975
1

Разумеется, ничто не мешает вычислить корреляцию между столбцами.
Ниже мы вычисляем годовую корреляцию между Apple и Microsoft:
In [127]: by_year.apply(lambda g: g['AAPL'].corr(g['MSFT']))
Out[127]:
2003 0.480868
2004 0.259024
2005 0.300093
2006 0.161735
2007 0.417738
2008 0.611901
2009 0.432738
2010 0.571946
2011 0.581987
dtype: float64

Пример: групповая линейная регрессия
Следуя той же методике, что в предыдущем примере, мы можем применить
groupby для выполнения более сложного статистического анализа на группах;
главное, чтобы функция возвращала объект pandas или скалярное значение.
Например, я могу определить функцию regress (воспользовавшись эконометрической библиотекой statsmodels), которая вычисляет регрессию обычным
методом наименьших квадратов для каждого блока данных:
import statsmodels.api as sm
def regress(data, yvar, xvars):
Y = data[yvar]
X = data[xvars]
X['intercept'] = 1.
result = sm.OLS(Y, X).fit()
return result.params

Теперь для вычисления линейной регрессии AAPL от суточного оборота SPX
по годам нужно написать:
In [129]: by_year.apply(regress, 'AAPL', ['SPX'])

Сводные таблицы и перекрестное табулирование

331

Out[129]:
2003
2004
2005
2006
2007
2008
2009
2010
2011

SPX intercept
1.195406 0.000710
1.363463 0.004201
1.766415 0.003246
1.645496 0.000080
1.198761 0.003438
0.968016 –0.001110
0.879103 0.002954
1.052608 0.001261
0.806605 0.001514

10.4. Сводные таблицы
и перекрестное табулирование
Сводная таблица – это средство обобщения данных, применяемое в электронных таблицах и других аналитических программах. Оно агрегирует таблицу
по одному или нескольким ключам и строит другую таблицу, в которой одни
групповые ключи расположены в строках, а другие – в столбцах. Библиотека
pandas позволяет строить сводные таблицы с помощью описанного выше механизма groupby в сочетании с операциями изменения формы с применением
иерархического индексирования. В классе DataFrame имеется метод pivot_ta­
ble, а на верхнем уровне – функция pandas.pivot_table. Помимо удобного интерфейса к groupby функция pivot_table еще умеет добавлять частичные итоги,
которые называются маргиналами.
Вернемся к набору данных о чаевых и вычислим таблицу групповых средних (тип агрегирования по умолчанию, подразумеваемый pivot_table) по
столбцам day и smoker, расположив их в строках:
In [130]: tips.pivot_table(index=['day',
Out[130]:
size
tip tip_pct
day smoker
Fri No
2.250000 2.812500 0.151650
Yes
2.066667 2.714000 0.174783
Sat No
2.555556 3.102889 0.158048
Yes
2.476190 2.875476 0.147906
Sun No
2.929825 3.167895 0.160113
Yes
2.578947 3.516842 0.187250
Thur No
2.488889 2.673778 0.160298
Yes
2.352941 3.030000 0.163863

'smoker'])
total_bill
18.420000
16.813333
19.661778
21.276667
20.506667
24.120000
17.113111
19.190588

Это можно было бы легко сделать и с помощью groupby. Пусть теперь требуется агрегировать только столбцы tip_pct и size, добавив еще группировку
по time. Я помещу средние по smoker в столбцы таблицы, а по day – в строки:

332

Агрегирование данных и групповые операции

In [131]: tips.pivot_table(['tip_pct', 'size'], index=['time', 'day'],
.....:
columns='smoker')
Out[131]:
size
tip_pct
smoker
No
Yes
No
Yes
time
day
Dinner Fri 2.000000 2.222222 0.139622 0.165347
Sat 2.555556 2.476190 0.158048 0.147906
Sun 2.929825 2.578947 0.160113 0.187250
Thur 2.000000
NaN 0.159744
NaN
Lunch Fri 3.000000 1.833333 0.187735 0.188937
Thur 2.500000 2.352941 0.160311 0.163863

Эту таблицу можно было бы дополнить, включив частичные итоги, для чего
следует задать параметр margins=True. Тогда будут добавлены строка и столбец
с меткой All, значениями в которых будут групповые статистики по всем
данным на одном уровне.
In [132]: tips.pivot_table(['tip_pct', 'size'], index=['time',
.....:
columns='smoker', margins=True)
Out[132]:
size
tip_pct
smoker
No
Yes
All
No
Yes
time
day
Dinner Fri 2.000000 2.222222 2.166667 0.139622 0.165347
Sat 2.555556 2.476190 2.517241 0.158048 0.147906
Sun 2.929825 2.578947 2.842105 0.160113 0.187250
Thur 2.000000
NaN 2.000000 0.159744
NaN
Lunch Fri 3.000000 1.833333 2.000000 0.187735 0.188937
Thur 2.500000 2.352941 2.459016 0.160311 0.163863
All
2.668874 2.408602 2.569672 0.159328 0.163196

'day'],

All
0.158916
0.153152
0.166897
0.159744
0.188765
0.161301
0.160803

Здесь столбцы All содержат средние без учета того, является гость курящим
или некурящим, а строка All – средние по обоим уровням группировки.
Для применения другой функции агрегирования ее нужно передать в параметре aggfunc. Например, передача 'count' или len даст таблицу сопряженности размеров групп (счетчики или частоты):
In [133]: tips.pivot_table('tip_pct', index=['time', 'smoker'], columns='day',
.....:
aggfunc=len, margins=True)
Out[133]:
day
Fri Sat Sun Thur
All
time
smoker
Dinner No
3.0 45.0 57.0 1.0 106.0
Yes
9.0 42.0 19.0 NaN 70.0
Lunch No
1.0 NaN NaN 44.0 45.0
Yes
6.0 NaN NaN 17.0 23.0
All
19.0 87.0 76.0 62.0 244.0

Сводные таблицы и перекрестное табулирование

333

Для восполнения отсутствующих комбинаций можно задать параметр
fill_value:
In [134]: tips.pivot_table('tip_pct', index=['time', 'size', 'smoker'],
.....: columns='day', aggfunc='mean', fill_value=0)
Out[134]:
day
Fri
Sat
Sun
Thur
time size smoker
Dinner 1
No
0.000000 0.137931 0.000000 0.000000
Yes 0.000000 0.325733 0.000000 0.000000
2
No
0.139622 0.162705 0.168859 0.159744
Yes 0.171297 0.148668 0.207893 0.000000
3
No
0.000000 0.154661 0.152663 0.000000
Yes 0.000000 0.144995 0.152660 0.000000
4
No
0.000000 0.150096 0.148143 0.000000
Yes 0.117750 0.124515 0.193370 0.000000
5
No
0.000000 0.000000 0.206928 0.000000
Yes 0.000000 0.106572 0.065660 0.000000
...
...
...
... ...
Lunch 1
No
0.000000 0.000000 0.000000 0.181728
Yes 0.223776 0.000000 0.000000 0.000000
2
No
0.000000 0.000000 0.000000 0.166005
Yes 0.181969 0.000000 0.000000 0.158843
3
No
0.187735 0.000000 0.000000 0.084246
Yes 0.000000 0.000000 0.000000 0.204952
4
No
0.000000 0.000000 0.000000 0.138919
Yes 0.000000 0.000000 0.000000 0.155410
5
No
0.000000 0.000000 0.000000 0.121389
6
No
0.000000 0.000000 0.000000 0.173706
[21 rows x 4 columns]

В табл. 9.2 приведена сводка аргументов метода pivot_table.
Таблица 9.2. Аргументы метода pivot_table
Параметр

Описание

values

Имя (или имена) одного или нескольких столбцов, по которым производится агрегирование.
По умолчанию агрегируются все числовые столбцы
Имена столбцов или другие групповые ключи для группировки по строкам результирующей
сводной таблицы
Имена столбцов или другие групповые ключи для группировки по столбцам результирующей
сводной таблицы
Функция агрегирования или список таких функций; по умолчанию 'mean'. Можно задать
произвольную функцию, допустимую в контексте groupby
Чем заменять отсутствующие значения в результирующей таблице
Если True, не включать столбцы, в которых все значения отсутствуют
Добавлять частичные итоги и общий итог по строкам и столбцам (по умолчанию False)

index
columns
aggfunc
fill_value
dropna
margins

334

Агрегирование данных и групповые операции

Таблицы сопряженности
Таблица сопряженности, или перекрестная таблица (cross-tabulation, или
для краткости crosstab), – частный случай сводной таблицы, в которой представлены групповые частоты. Приведем пример:
In [138]: data
Out[138]:
Sample Nationality
0
1
USA
1
2
Japan
2
3
USA
3
4
Japan
4
5
Japan
5
6
Japan
6
7
USA
7
8
USA
8
9
Japan
9
10
USA

Handedness
Right–handed
Left–handed
Right–handed
Right–handed
Left–handed
Right–handed
Right–handed
Left–handed
Right–handed
Right–handed

В ходе анализа-обследования мы могли бы обобщить эти данные по национальности и праворукости/леворукости. Для этой цели можно использовать
метод pivot_table, но функция pandas.crosstab удобнее:
In [139]: pd.crosstab(data.Nationality, data.Handedness, margins=True)
Out[139]:
Handedness
Left–handed Right–handed All
Nationality
Japan
2
3
5
USA
1
4
5
All
3
7 10

Каждый из первых двух аргументов crosstab может быть массивом, объектом Series или списком массивов. Например, в случае данных о чаевых:
In [140]: pd.crosstab([tips.time, tips.day], tips.smoker, margins=True)
Out[140]:
smoker
No Yes All
time day
Dinner Fri
3 9 12
Sat 45 42 87
Sun 57 19 76
Thur 1 0
1
Lunch Fri
1 6
7
Thur 44 17 61
All
151 93 244

Заключение

335

10.5. Заключение
Уверенное владение средствами группировки, имеющимися в pandas, поможет вам как при очистке данных, так и в процессе моделирования или статистического анализа. В главе 14 мы рассмотрим дополнительные примеры
использования groupby для реальных данных.
А в следующей главе обратимся к временным рядам.

Глава 11. Временные ряды
Временные ряды – важная разновидность структурированных данных. Они
встречаются во многих областях, в том числе в финансах, экономике, экологии, нейронауках и физике. Любые результаты наблюдений или измерений
в разные моменты времени образуют временной ряд. Для многих временных
рядов характерна фиксированная частота, т. е. интервалы между соседними
точками одинаковы – измерения производятся, например, один раз в 15 секунд, 5 минут или в месяц. Но временные ряды могут быть и нерегулярными,
когда интервалы времени между соседними точками различаются. Как разметить временной ряд и обращаться к нему, зависит от приложения. Существуют следующие варианты:
• временные метки, конкретные моменты времени;
• фиксированные периоды, например январь 2007 года или весь 2010 год;
• временные интервалы, обозначаемые метками начала и конца; периоды можно считать частными случаями интервалов;
• время эксперимента или истекшее время; каждая временная метка измеряет время, прошедшее с некоторого начального момента. Например,
результаты измерения диаметра печенья с момента помещения теста
в духовку.
В этой главе меня будут интересовать в основном временные ряды трех
первых видов, хотя многие методы применимы и к экспериментальным
временным рядам, когда индекс может содержать целые или вещественные
значения, обозначающие время, прошедшее с начала эксперимента. Простейший и самый распространенный вид временных рядов – ряды, индексированные временной меткой.
pandas поддерживает также индексы, построенные по приращению времени,
это полезный способ представления времени эксперимента или истекшего
времени. Такие индексы в книге не рассматриваются, но вы можете прочитать
о них в документации (http://pandas.pydata.org/).

Типы данных и инструменты, относящиеся к дате и времени

337

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

11.1. Типы данных и инструменты,
относящиеся к дате и времени
В стандартной библиотеке Python имеются типы данных для представления даты и времени, а также средства, относящиеся к календарю. Начинать
изучение надо с модулей datetime, time и calendar. Особенно широко используется тип datetime.datetime, или просто datetime:
In [10]: from datetime import datetime
In [11]: now = datetime.now()
In [12]: now
Out[12]: datetime.datetime(2017, 9, 25, 14, 5, 52, 72973)
In [13]: now.year, now.month, now.day
Out[13]: (2017, 9, 25)

В объекте типа datetime хранятся дата и время с точностью до микросекунды. Класс datetime.timedelta представляет интервал времени между двумя
объектами datetime:
In [14]: delta = datetime(2011, 1, 7) – datetime(2008, 6, 24, 8, 15)
In [15]: delta
Out[15]: datetime.timedelta(926, 56700)
In [16]: delta.days
Out[16]: 926
In [17]: delta.seconds
Out[17]: 56700

Можно прибавить (или вычесть) объект timedelta или его произведение на
целое число к объекту datetime и получить в результате новый объект того же
типа, представляющий соответственно сдвинутый момент времени:
In [18]: from datetime import timedelta
In [19]: start = datetime(2011, 1, 7)
In [20]: start + timedelta(12)
Out[20]: datetime.datetime(2011, 1, 19, 0, 0)

338

Временные ряды

In [21]: start – 2 * timedelta(12)
Out[21]: datetime.datetime(2010, 12, 14, 0, 0)

Сводка типов данных в модуле datetime приведена в табл. 11.1. Хотя в этой
главе речь пойдет преимущественно о типах данных в pandas и высокоуровневых операциях с временными рядами, вы, без сомнения, встретите основанные на datetime типы и во многих других приложениях, написанных на
Python.
Таблица 11.1. Типы в модуле datetime
Тип

Описание

date
time
datetime
timedelta

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

tzinfo

Преобразование между строкой и datetime
Объекты типа datetime и входящего в pandas типа Timestamp, с которым мы
вскоре познакомимся, можно представить в виде отформатированной строки с помощью метода str или strftime, которому передается спецификация
формата:
In [22]: stamp = datetime(2011, 1, 3)
In [23]: str(stamp)
Out[23]: '2011–01–03 00:00:00'
In [24]: stamp.strftime('%Y–%m–%d')
Out[24]: '2011–01–03'

Полный перечень форматных кодов приведен в табл. 11.2 (повторение
списка из главы 2).
Таблица 11.2. Спецификации формата даты в классе datetime
(совместимы со стандартом ISO C89)
Спецификатор

Описание

%Y
%y
%m
%d
%H
%I
%M

Год с четырьмя цифрами
Год с двумя цифрами
Номер месяца с двумя цифрами [01, 12]
Номер дня с двумя цифрами [01, 31]
Час (в 24-часовом формате) [00, 23]
Час (в 12-часовом формате) [01, 12]
Минута с двумя цифрами [01, 59]

Типы данных и инструменты, относящиеся к дате и времени

339

Таблица 11.2 (окончание)
Спецификатор

Описание

%S
%w
%U

Секунда [00, 61] (секунды 60 и 61 високосные)
День недели в виде целого числа [0 (воскресенье), 6]
Номер недели в году [00, 53]. Первым днем недели считается воскресенье, а дни,
предшествующие первому воскресенью, относятся к неделе 0
Номер недели в году [00, 53]. Первым днем недели считается понедельник, а дни,
предшествующие первому понедельнику, относятся к неделе 0
Часовой пояс UTC в виде +HHMM или –HHMM; пустая строка, если часовой пояс
не учитывается
Сокращение для %Y–%m–%d, например 2012–4–18
Сокращение для %m/%d/%y, например 04/18/12

%W
%z
%F
%D

Многие из этих кодов используются для преобразования строк в даты методом datetime.strptime:
In [25]: value = '2011–01–03'
In [26]: datetime.strptime(value, '%Y–%m–%d')
Out[26]: datetime.datetime(2011, 1, 3, 0, 0)
In [27]: datestrs = ['7/6/2011', '8/6/2011']
In [28]: [datetime.strptime(x, '%m/%d/%Y') for x in datestrs]
Out[28]: [datetime.datetime(2011, 7, 6, 0, 0), datetime.datetime(2011, 8, 6, 0, 0)]

Метод datetime.strptime прекрасно работает, когда формат даты известен.
Однако каждый раз задавать формат даты, особенно общеупотребительный,
надоедает. В таком случае можно воспользоваться методом parser.parse из
стороннего пакета dateutil (он автоматически устанавливается вместе с pandas):
In [29]: from dateutil.parser import parse
In [30]: parse('2011–01–03')
Out[30]: datetime.datetime(2011, 1, 3, 0, 0)

dateutil умеет разбирать практически любое представление даты, понятное
человеку:
In [31]: parse('Jan 31, 1997 10:45 PM')
Out[31]: datetime.datetime(1997, 1, 31, 22, 45)

Если, как бывает в других локалях, день предшествует месяцу, то следует
задать параметр dayfirst=True:
In [32]: parse('6/12/2011', dayfirst=True)
Out[32]: datetime.datetime(2011, 12, 6, 0, 0)

Библиотека pandas, вообще говоря, ориентирована на работу с массивами
дат, используемых как в качестве осевого индекса, так и столбца в DataFrame.

340

Временные ряды

Метод to_datetime разбирает различные представления даты. Стандартные
форматы, например ISO8601, разбираются очень быстро.
In [33]: datestrs = ['2011–07–06 12:00:00', '2011–08–06 00:00:00']
In [34]: pd.to_datetime(datestrs)
Out[34]: DatetimeIndex(['2011–07–06 12:00:00', '2011–08–06 00:00:00'], dtype='dat
etime64[ns]', freq=None)

Кроме того, этот метод умеет обрабатывать значения, которые следует считать отсутствующими (None, пустая строка и т. д.):
In [35]: idx = pd.to_datetime(datestrs + [None])
In [36]: idx
Out[36]: DatetimeIndex(['2011–07–06 12:00:00', '2011–08–06 00:00:00', 'NaT'],
dtype='datetime64[ns]', freq=None)
In [37]: idx[2]
Out[37]: NaT
In [38]: pd.isnull(idx)
Out[38]: array([False, False, True], dtype=bool)

NaT (Not a Time – не время) – применяемое в pandas значение для индикации отсутствующей временной метки.
Класс dateutil.parser – полезный, но не идеальный инструмент. В частности,
он распознает строки, которые не на всякий взгляд являются датами. Например, строка ‘42’ будет разобрана как текущая календарная дата в 2042 году.

У объектов datetime имеется также ряд зависимых от локали параметров
форматирования для других стран и языков. Например, сокращенные названия месяцев в системе с немецкой или французской локалью будут не такие,
как в системе с английской локалью. Полный перечень см. в табл. 11.3.
Таблица 11.3. Спецификации формата даты, зависящие от локали
Спецификатор

Описание

%a
%A
%b
%B

%p
%x

Сокращенное название дня недели
Полное название дня недели
Сокращенное название месяца
Полное название месяца
Полная дата и время, например «Tue 01 May 2012 04:20:57 PM»
Локализованный эквивалент AM или PM
Дата в формате, соответствующем локали. Например, в США 1 мая 2012 будет
представлено в виде «05/01/2012»
Время в формате, соответствующем локали, например «04:24:12 PM»

%X

Основы работы с временными рядами

341

11.2. Основы работы с временными рядами
Самый простой вид временного ряда в pandas – объект Series, индексированный временными метками, которые часто представляются внешними по
отношению к pandas Python-строками или объектами datetime:
In [39]: from datetime import datetime
In [40]: dates = [datetime(2011, 1, 2), datetime(2011, 1, 5),
....: datetime(2011, 1, 7), datetime(2011, 1, 8),
....: datetime(2011, 1, 10), datetime(2011, 1, 12)]
In [41]: ts = pd.Series(np.random.randn(6), index=dates)
In [42]: ts
Out[42]:
2011–01–02 –0.204708
2011–01–05 0.478943
2011–01–07 –0.519439
2011–01–08 –0.555730
2011–01–10 1.965781
2011–01–12 1.393406
dtype: float64

Под капотом объекты datetime помещаются в объект типа DatetimeIndex:
In [43]: ts.index
Out[43]:
DatetimeIndex(['2011–01–02', '2011–01–05', '2011–01–07', '2011–01–08',
'2011–01–10', '2011–01–12'],
dtype='datetime64[ns]', freq=None)

Как и для других объектов Series, арифметические операции над временными рядами с различными индексами автоматически приводят к выравниванию дат:
In [44]: ts + ts[::2]
Out[44]:
2011–01–02 –0.409415
2011–01–05
NaN
2011–01–07 –1.038877
2011–01–08
NaN
2011–01–10
3.931561
2011–01–12
NaN
dtype: float64

Напомним, что конструкция ts[::2] выбирает каждый второй элемент ts.
В pandas временные метки хранятся в типе данных NumPy datetime64 с наносекундным разрешением:
In [45]: ts.index.dtype
Out[45]: dtype(' 0).value_counts()
Out[195]:
True 991475
False 10256

Чтобы упростить анализ, я ограничусь только положительными суммами
пожертвований:
In [196]: fec = fec[fec.contb_receipt_amt > 0]

Поскольку главными кандидатами являются Барак Обама и Митт Ромни,
я подготовлю также подмножество, содержащее данные о пожертвованиях
на их кампании:
In [197]: fec_mrbo = fec[fec.cand_nm.isin(['Obama, Barack', 'Romney, Mitt'])]

Статистика пожертвований по роду занятий
и месту работы
Распределение пожертвований по роду занятий – тема, которой посвящено много исследований. Например, юристы (в том числе прокуроры) обычно
жертвуют в пользу демократов, а руководители предприятий – в пользу республиканцев. Вы вовсе не обязаны верить мне на слово, можете сами проанализировать данные. Для начала решим простую задачу – получим общую
статистику пожертвований по роду занятий:
In [198]: fec.contbr_occupation.value_counts()[:10]
Out[198]:
RETIRED
233990
INFORMATION REQUESTED
35107
ATTORNEY
34286
HOMEMAKER
29931
PHYSICIAN
23432
INFORMATION REQUESTED PER BEST EFFORTS 21138
ENGINEER
14334
TEACHER
13990
CONSULTANT
13273
PROFESSOR
12555

База данных федеральной избирательной комиссии

463

Видно, что часто различные занятия на самом деле относятся к одной
и той же основной профессии с небольшими вариациями. Ниже показан
код, который позволяет произвести очистку, отобразив один род занятий на
другой. Обратите внимание на трюк с методом dict.get, который позволяет
«передавать насквозь» занятия, которым ничего не сопоставлено:
occ_mapping = {
'INFORMATION REQUESTED PER BEST EFFORTS' : 'NOT PROVIDED',
'INFORMATION REQUESTED' : 'NOT PROVIDED',
'INFORMATION REQUESTED (BEST EFFORTS)' : 'NOT PROVIDED',
'C.E.O.': 'CEO'
}
# Если ничего не сопоставлено, вернуть x
f = lambda x: occ_mapping.get(x, x)
fec.contbr_occupation = fec.contbr_occupation.map(f)

То же самое я проделаю для мест работы:
emp_mapping = {
'INFORMATION REQUESTED PER BEST EFFORTS' : 'NOT PROVIDED',
'INFORMATION REQUESTED' : 'NOT PROVIDED',
'SELF' : 'SELF–EMPLOYED',
'SELF EMPLOYED' : 'SELF–EMPLOYED',
}
# Если ничего не сопоставлено, вернуть x
f = lambda x: emp_mapping.get(x, x)
fec.contbr_employer = fec.contbr_employer.map(f)

Теперь можно воспользоваться функцией pivot_table для агрегирования
данных по партиям и роду занятий, а затем отфильтровать роды занятий, на
которые пришлось пожертвований на общую сумму не менее 2 000 000 долл.:
In [201]: by_occupation = fec.pivot_table('contb_receipt_amt',
....:
rows='contbr_occupation',
....:
cols='party', aggfunc='sum')
In [202]: over_2mm = by_occupation[by_occupation.sum(1) > 2000000]
In [203]: over_2mm
Out[203]:
party
Democrat
contbr_occupation
ATTORNEY
11141982.97
CEO
2074974.79
CONSULTANT
2459912.71
ENGINEER
951525.55
EXECUTIVE
1355161.05
...
PRESIDENT
1878509.95
PROFESSOR
2165071.08

Republican
7477194.430000
4211040.520000
2544725.450000
1818373.700000
4138850.090000
4720923.760000
296702.730000

464

Примеры анализа данных

REAL ESTATE
528902.09
RETIRED
25305116.38
SELF–EMPLOYED
672393.40
[17 rows x 2 columns]

1625902.250000
23561244.489999
1640252.540000

Эти данные проще воспринять в виде графика (параметр 'barh' означает
горизонтальную столбчатую диаграмму, см. рис. 14.12):
In [205]: over_2mm.plot(kind='barh')

Рис. 14.12. Общая сумма пожертвований по партиям
для родов занятий с максимальной суммой пожертвований

Возможно, вам интересны профессии самых щедрых жертвователей или
названия компаний, которые больше всех пожертвовали Бараку Обаме или
Митту Ромни. Для этого можно сгруппировать данные по имени кандидата,
а затем воспользоваться вариантом метода
, рассмотренного выше в этой
главе:
def get_top_amounts(group, key, n=5):
totals = group.groupby(key)['contb_receipt_amt'].sum()
return totals.order(ascending=False)[–n:]

Затем агрегируем по роду занятий и месту работы:
In [207]: grouped = fec_mrbo.groupby('cand_nm')
In [208]: grouped.apply(get_top_amounts, 'contbr_occupation', n=7)
Out[208]:
cand_nm
contbr_occupation

База данных федеральной избирательной комиссии

465

Obama, Barack RETIRED
25305116.38
ATTORNEY
11141982.97
NOT PROVIDED
4866973.96
HOMEMAKER
4248875.80
PHYSICIAN
3735124.94
LAWYER
3160478.87
CONSULTANT
2459912.71
Romney, Mitt RETIRED
11508473.59
NOT PROVIDED
11396894.84
HOMEMAKER
8147446.22
ATTORNEY
5364718.82
PRESIDENT
2491244.89
EXECUTIVE
2300947.03
C.E.O.
1968386.11
Name: contb_receipt_amt, Length: 14, dtype: float64
In [209]: grouped.apply(get_top_amounts, 'contbr_employer', n=10)
Out[209]:
cand_nm
contbr_employer
Obama, Barack RETIRED
22694358.85
SELF–EMPLOYED
18626807.16
NOT EMPLOYED
8586308.70
INFORMATION REQUESTED
5053480.37
HOMEMAKER
2605408.54
...
Romney, Mitt CREDIT SUISSE
281150.00
MORGAN STANLEY
267266.00
GOLDMAN SACH & CO.
238250.00
BARCLAYS CAPITAL
162750.00
H.I.G. CAPITAL
139500.00
Name: contb_receipt_amt, Length: 20, dtype: float64

Распределение суммы пожертвований по интервалам
Полезный вид анализа данных – дискретизация сумм пожертвований с помощью функции cut:
In [210]: bins = np.array([0, 1, 10, 100, 1000, 10000,
.....:
100000, 1000000, 10000000])
In [211]: labels = pd.cut(fec_mrbo.contb_receipt_amt, bins)
In [212]: labels
Out[212]:
411
(10, 100]
412
(100, 1000]
413
(100, 1000]
414
(10, 100]
415
(10, 100]
...

466

Примеры анализа данных

701381
(10, 100]
701382 (100, 1000]
701383
(1, 10]
701384
(10, 100]
701385 (100, 1000]
Name: contb_receipt_amt, Length: 694282, dtype: category
Categories (8, interval[int64]): [(0, 1] < (1, 10] < (10, 100] < (100, 1000] < (1
000, 10000] <
(10000, 100000] < (100000, 1000000] < (1000000,
10000000]]

Затем можно сгруппировать данные для Барака Обамы и Митта Ромни по
имени и метке интервала и построить гистограмму сумм пожертвований:
In [213]: grouped = fec_mrbo.groupby(['cand_nm', labels])
In [214]: grouped.size().unstack(0)
Out[214]:
cand_nm
Obama, Barack Romney, Mitt
contb_receipt_amt
(0, 1]
493
77
(1, 10]
40070
3681
(10, 100]
372280
31853
(100, 1000]
153991
43357
(1000, 10000]
22284
26186
(10000, 100000]
2
1
(100000, 1000000]
3
NaN
(1000000, 10000000]
4
NaN

Отсюдавидно, что Барак Обама получил гораздо больше небольших пожертвований, чем Митт Ромни. Можно также вычислить сумму размеров пожертвований и нормировать распределение по интервалам, чтобы наглядно
представить процентную долю пожертвований каждого размера от общего
их числа по отдельным кандидатам (рис. 14.13):
In [216]: bucket_sums = grouped.contb_receipt_amt.sum().unstack(0)
In [217]: normed_sums = bucket_sums.div(bucket_sums.sum(axis=1), axis=0)
In [218]: normed_sums
Out[218]:
cand_nm
contb_receipt_amt
(0, 1]
(1, 10]
(10, 100]
(100, 1000]
(1000, 10000]
(10000, 100000]
(100000, 1000000]
(1000000, 10000000]

Obama, Barack

Romney, Mitt

0.805182
0.918767
0.910769
0.710176
0.447326
0.823120
1.000000
1.000000

0.194818
0.081233
0.089231
0.289824
0.552674
0.176880
NaN
NaN

База данных федеральной избирательной комиссии

467

In [219]: normed_sums[:–2].plot(kind='barh')

Рис. 14.13. Процентная доля пожертвований каждого размера
от общего их числа для обоих кандидатов

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

Статистика пожертвований по штатам
Агрегирование данных по кандидатам и штатам – вполне рутинная задача:
In [220]: grouped = fec_mrbo.groupby(['cand_nm', 'contbr_st'])
In [221]: totals = grouped.contb_receipt_amt.sum().unstack(0).fillna(0)
In [222]: totals = totals[totals.sum(1) > 100000]
In [223]: totals[:10]
Out[223]:
cand_nm
Obama, Barack Romney, Mitt
contbr_st
AK
281840.15
86204.24
AL
543123.48
527303.51
AR
359247.28
105556.00

468
AZ
CA
CO
CT
DC
DE
FL

Примеры анализа данных
1506476.98
23824984.24
2132429.49
2068291.26
4373538.80
336669.14
7318178.58

1888436.23
11237636.60
1506714.12
3499475.45
1025137.50
82712.00
8338458.81

Поделив каждую строку на общую сумму пожертвований, мы получим
для каждого кандидата процентную долю от общей суммы, приходящуюся
на каждый штат:
In [224]: percent = totals.div(totals.sum(1), axis=0)
In [225]: percent[:10]
Out[225]:
cand_nm
Obama, Barack Romney, Mitt
contbr_st
AK
0.765778
0.234222
AL
0.507390
0.492610
AR
0.772902
0.227098
AZ
0.443745
0.556255
CA
0.679498
0.320502
CO
0.585970
0.414030
CT
0.371476
0.628524
DC
0.810113
0.189887
DE
0.802776
0.197224
FL
0.467417
0.532583

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

Приложение A. Дополнительные
сведения о библиотеке NumPy
В этом приложении мы глубже рассмотрим библиотеку NumPy, предназначенную для вычислений с массивами. Разберемся во внутренних деталях
типа ndarray и поговорим о дополнительных операциях с массивами и алгоритмах.
Приложение содержит разнородные темы, поэтому читать его последовательно не обязательно.

A.1. Внутреннее устройство объекта ndarray
Объект ndarray из библиотеки NumPy позволяет интерпретировать блок однородных данных (непрерывный или с шагом, подробнее об этом ниже) как
многомерный массив. Мы уже видели, что тип данных, или dtype, определяет,
как именно интерпретируются данные: как числа с плавающей точкой, целые, булевы или еще как-то.
Своей эффективностью ndarray отчасти обязан тому, что любой объект
массива является шаговым (strided) представлением блока данных. Может
возникнуть вопрос, как удается построить представление массива arr[::2,
::–1] без копирования данных. Дело в том, что объект ndarray не просто блок
памяти, дополненный знанием о типе в виде dtype; в нем еще хранится информация, позволяющая перемещаться по массиву шагами разного размера.
Точнее, в реализации ndarray имеется:
• указатель на данные, т. е. на блок полученной от системы памяти;
• тип данных, или dtype, описывающий значения элементов массива фиксированного размера;
• кортеж, описывающий форму массива;

470

Дополнительные сведения о библиотеке NumPy

• кортеж шагов, т. е. целых чисел, показывающих, на сколько байтов нужно сместиться, чтобы перейти к следующему элементу по некоторому
измерению.
На рис. A.1 схематически показано внутреннее устройство ndarray.
объект ndarray
данные
тип данных

форма

шаги

Рис. A.1. Объект ndarray из библиотеки NumPy

Например, у массива 10×5 будет форма (10, 5):
In [10]: np.ones((10, 5)).shape
Out[10]: (10, 5)

Для типичного массива (организованного в соответствии с принятым
в языке C соглашением) 3×4×5 чисел типа float64 (8-байтовых) кортеж шагов
имеет вид (160, 40, 8) (знать о шагах полезно, потому что в общем случае
чем больше шаг по конкретной оси, тем дороже обходятся вычисления по
этой оси):
In [11]: np.ones((3, 4, 5), dtype=np.float64).strides
Out[11]: (160, 40, 8)

Хотя типичному пользователю NumPy редко приходится интересоваться
шагами массива, они играют важнейшую роль в построении представлений
массива без копирования. Шаги могут быть даже отрицательными, что позволяет проходить массив в обратном направлении, как в случае среза вида
obj[::­1] или obj[:, ::­1].

Иерархия типов данных в NumPy
Иногда в программе необходимо проверить, что хранится в массиве: целые числа, числа с плавающей точкой, строки или объекты Python. Поскольку существует много типов с плавающей точкой (от float16 до float128), для
проверки, присутствует ли dtype в списке типов, приходится писать длинный
код. По счастью, определены такие суперклассы, как np.integer или np.floating,
которые можно использовать в сочетании с функцией np.issubdtype:
In [12]: ints = np.ones(10, dtype=np.uint16)
In [13]: floats = np.ones(10, dtype=np.float32)
In [14]: np.issubdtype(ints.dtype, np.integer)

Дополнительные манипуляции с массивами

471

Out[14]: True
In [15]: np.issubdtype(floats.dtype, np.floating)
Out[15]: True

Вывести все родительские классы данного типа dtype позволяет метод mro:
In [16]: np.float64.mro()
Out[16]:
[numpy.float64,
numpy.floating,
numpy.inexact,
numpy.number,
numpy.generic,
float,
object]

Поэтому мы также имеем:
In [17]: np.issubdtype(ints.dtype, np.number)
Out[17]: True

Большинству пользователей NumPy об этом знать необязательно, но иногда оказывается удобно. На рис. A.2 показан граф наследования dtype1.

Рис. А.2. Граф наследования dtype

A.2. Дополнительные манипуляции с массивами
Помимо прихотливого индексирования, вырезания и формирования булевых
подмножеств, существует много других способов работы с массивами. И хо1

В именах некоторых типов dtype присутствуют знаки подчеркивания. Они нужны,
чтобы избежать конфликтов между именами типов NumPy и встроенных типов Python.

472

Дополнительные сведения о библиотеке NumPy

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

Изменение формы массива
Во многих случаях изменить форму массива можно без копирования данных. Для этого следует передать кортеж с описанием новой формы методу
экземпляра массива reshape. Например, предположим, что имеется одномерный массив, который мы хотели бы преобразовать в матрицу (результат показан на рис. A.3):
In [18]: arr = np.arange(8)
In [19]: arr
Out[19]: array([0, 1, 2, 3, 4, 5, 6, 7])
In [20]: arr.reshape((4, 2))
Out[20]:
array([[0, 1],
[2, 3],
[4, 5],
[6, 7]])

arr.reshape((4, 3), order=?)
Порядок, принятый в С
(по строкам)

Порядок, принятый в Fortran
(по столбцам)

order='C'

order='F'

Рис. A.3. Изменение формы с преобразованием в двумерный массив,
организованный как в С (по строкам) и как в Fortran (по столбцам)

Форму многомерного массива также можно изменить:
In [21]: arr.reshape((4, 2)).reshape((2, 4))
Out[21]:
array([[0, 1, 2, 3],
[4, 5, 6, 7]])

Дополнительные манипуляции с массивами

473

Одно из переданных в описателе формы измерений может быть равно –1,
его значение будет выведено из данных:
In [22]: arr = np.arange(15)
In [23]: arr.reshape((5, –1))
Out[23]:
array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11],
[12, 13, 14]])

Поскольку атрибут shape массива является кортежем, его также можно передать методу reshape:
In [24]: other_arr = np.ones((3, 5))
In [25]: other_arr.shape
Out[25]: (3, 5)
In [26]: arr.reshape(other_arr.shape)
Out[26]:
array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])

Обратная операция – переход от многомерного к одномерному массиву –
называется линеаризацией:
In [27]: arr = np.arange(15).reshape((5, 3))
In [28]: arr
Out[28]:
array([[ 0, 1,
[ 3, 4,
[ 6, 7,
[ 9, 10,
[12, 13,

2],
5],
8],
11],
14]])

In [29]: arr.ravel()
Out[29]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])

Метод ravel не создает копию данных, если без этого можно обойтись (подробнее об этом ниже). Метод flatten ведет себя как ravel, но всегда возвращает копию данных:
In [30]: arr.flatten()
Out[30]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])

Данные можно линеаризовать в разном порядке. Начинающим пользователям NumPy эта тема может показаться довольно сложной, поэтому ей посвящен целиком следующий подраздел.

474

Дополнительные сведения о библиотеке NumPy

Упорядочение элементов массива в C и в Fortran
Библиотека NumPy предлагает большую гибкость в определении порядка
размещения данных в памяти. По умолчанию массивы NumPy размещаются
по строкам. Это означает, что при размещении двумерного массива в памяти
соседние элементы строки находятся в соседних ячейках памяти. Альтернативой является размещение по столбцам, тогда в соседних ячейках находятся
соседние элементы столбца.
По историческим причинам порядок размещения по строкам называется
порядком C, а по столбцам – порядком Fortran. В языке FORTRAN 77 матрицы
размещаются по столбцам.
Функции типа reshape и ravel принимают аргумент order, показывающий,
в каком порядке размещать данные в массиве. Обычно задают значение 'C'
или 'F' (о менее употребительных значениях 'A' и 'K' можно прочитать в документации по NumPy, а их действие показано на рис. A.3).
In [31]: arr = np.arange(12).reshape((3, 4))
In [32]:
Out[32]:
array([[
[
[

arr
0, 1, 2, 3],
4, 5, 6, 7],
8, 9, 10, 11]])

In [33]: arr.ravel()
Out[33]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
In [34]: arr.ravel('F')
Out[34]: array([ 0, 4, 8, 1, 5, 9, 2, 6, 10, 3, 7, 11])

Изменение формы массива, имеющего больше двух измерений, – головоломное упражнение (см. рис. A.3). Основное различие между порядком C
и Fortran состоит в том, в каком порядке перебираются измерения:
• порядок по строкам (C): старшие измерения обходятся раньше (т. е. сначала обойти ось 1, а потом переходить к оси 0);
• порядок по столбцам (Fortran): старшие измерения обходятся позже (т. е.
сначала обойти ось 0, а потом переходить к оси 1).

Конкатенация и разбиение массива
Метод numpy.concatenate принимает произвольную последовательность (кортеж, список и т. п.) массивов и соединяет их вместе в порядке, определяемом
указанной осью.
In [35]: arr1 = np.array([[1, 2, 3], [4, 5, 6]])
In [36]: arr2 = np.array([[7, 8, 9], [10, 11, 12]])
In [37]: np.concatenate([arr1, arr2], axis=0)

Дополнительные манипуляции с массивами
Out[37]:
array([[ 1,
[ 4,
[ 7,
[10,

2,
5,
8,
11,

475

3],
6],
9],
12]])

In [38]: np.concatenate([arr1, arr2], axis=1)
Out[38]:
array([[ 1, 2, 3, 7, 8, 9],
[ 4, 5, 6, 10, 11, 12]])

Есть несколько вспомогательных функций, например vstack и hstack, для
выполнения типичных операций конкатенации. Приведенные выше операции можно было бы записать и так:
In [39]: np.vstack((arr1, arr2))
Out[39]:
array([[ 1, 2, 3],
[ 4, 5, 6],
[ 7, 8, 9],
[10, 11, 12]])
In [40]: np.hstack((arr1, arr2))
Out[40]:
array([[ 1, 2, 3, 7, 8, 9],
[ 4, 5, 6, 10, 11, 12]])

С другой стороны, функция split разбивает массив на несколько частей
вдоль указанной оси:
In [41]: arr = np.random.randn(5, 2)
In [42]: arr
Out[42]:
array([[–0.2047,
[–0.5194,
[ 1.9658,
[ 0.0929,
[ 0.769 ,

0.4789],
–0.5557],
1.3934],
0.2817],
1.2464]])

In [43]: first, second, third = np.split(arr, [1, 3])
In [44]: first
Out[44]: array([[–0.2047, 0.4789]])
In [45]: second
Out[45]:
array([[–0.5194, –0.5557],
[ 1.9658, 1.3934]])
In [46]: third
Out[46]:
array([[ 0.0929, 0.2817],
[ 0.769 , 1.2464]])

476

Дополнительные сведения о библиотеке NumPy

Значение [1, 3], переданное np.split, содержит индексы, по которым массив нужно разбить на части.
Перечень всех функций, относящихся к конкатенации и разбиению, приведен в табл. A.1, хотя некоторые из них – лишь надстройки над очень общей
функцией concatenate.
Таблица A.1. Функции конкатенации массива
Функция

Описание

concatenate

Самая общая функция – конкатенирует коллекцию массивов вдоль указанной
оси
Составляет массивы по строкам (вдоль оси 0)
Составляет массивы по столбцам (вдоль оси 1)
Аналогична hstack, но сначала преобразует одномерные массивы
в двумерные векторы по столбцам
Составляет массивы в глубину (вдоль оси 2)
Разбивает массив в указанных позициях вдоль указанной оси
Вспомогательные функции для разбиения по оси 0, 1 и 2 соответственно

vstack, row_stack
hstack
column_stack
dstack
split
hsplit / vsplit / dsplit

Вспомогательные объекты: r_ и c_
В пространстве имен NumPy есть два специальных объекта: r_ и c_, благодаря которым составление массивов можно записать более кратко:
In [47]: arr = np.arange(6)
In [48]: arr1 = arr.reshape((3, 2))
In [49]: arr2 = np.random.randn(3, 2)
In [50]:
Out[50]:
array([[
[
[
[
[
[

np.r_[arr1, arr2]

In [51]:
Out[51]:
array([[
[
[
[
[
[

np.c_[np.r_[arr1, arr2], arr]

0.
,
2.
,
4.
,
1.0072,
0.275 ,
1.3529,

0.
,
2.
,
4.
,
1.0072,
0.275 ,
1.3529,

1.
],
3.
],
5.
],
–1.2962],
0.2289],
0.8864]])

1.
,
3.
,
5.
,
–1.2962,
0.2289,
0.8864,

0.
1.
2.
3.
4.
5.

],
],
],
],
],
]])

С их помощью можно также преобразовывать срезы в массивы:
In [52]: np.c_[1:6, –10:–5]

Дополнительные манипуляции с массивами
Out[52]:
array([[
[
[
[
[

1,
2,
3,
4,
5,

477

–10],
–9],
–8],
–7],
–6]])

О том, что еще могут делать объекты c_ и r_, читайте в строке документации.

Повторение элементов: функции tile и repeat
Два полезных инструмента повторения, или репликации, массивов для
порождения массивов большего размера – функции repeat и tile. Функция
repeat повторяет каждый элемент массива несколько раз и создает больший
массив:
In [53]: arr = np.arange(3)
In [54]: arr
Out[54]: array([0, 1, 2])
In [55]: arr.repeat(3)
Out[55]: array([0, 0, 0, 1, 1, 1, 2, 2, 2])
Необходимость повторять массивы при работе с NumPy возникает реже, чем
в других популярных средах программирования, например в MATLAB. Основная причина заключается в том, что укладывание (тема следующего раздела)
решает эту задачу лучше.

По умолчанию если передать целое число, то каждый элемент повторяется
столько раз. Если же передать массив целых чисел, то разные элементы могут
быть повторены разное число раз:
In [56]: arr.repeat([2, 3, 4])
Out[56]: array([0, 0, 1, 1, 1, 2, 2, 2, 2])

Элементы многомерных массивов повторяются вдоль указанной оси:
In [57]: arr = np.random.randn(2, 2)
In [58]: arr
Out[58]:
array([[–2.0016, –0.3718],
[ 1.669 , –0.4386]])
In [59]: arr.repeat(2, axis=0)
Out[59]:
array([[–2.0016, –0.3718],
[–2.0016, –0.3718],
[ 1.669 , –0.4386],
[ 1.669 , –0.4386]])

478

Дополнительные сведения о библиотеке NumPy

Отметим, что если ось не указана, то массив сначала линеаризуется, а это,
скорее всего, не то, что вы хотели. Чтобы повторить разные срезы многомерного массива различное число раз, можно передать массив целых чисел:
In [60]: arr.repeat([2, 3], axis=0)
Out[60]:
array([[–2.0016, –0.3718],
[–2.0016, –0.3718],
[ 1.669 , –0.4386],
[ 1.669 , –0.4386],
[ 1.669 , –0.4386]])
In [61]: arr.repeat([2, 3], axis=1)
Out[61]:
array([[–2.0016, –2.0016, –0.3718, –0.3718, –0.3718],
[ 1.669 , 1.669 , –0.4386, –0.4386, –0.4386]])

Функция tile (замостить) – с другой стороны, просто сокращенный способ
составления копий массива вдоль оси. Это можно наглядно представлять себе
как «укладывание плиток»:
In [62]: arr
Out[62]:
array([[–2.0016, –0.3718],
[ 1.669 , –0.4386]])
In [63]: np.tile(arr, 2)
Out[63]:
array([[–2.0016, –0.3718, –2.0016, –0.3718],
[ 1.669 , –0.4386, 1.669 , –0.4386]])

Второй аргумент – количество плиток; если это скаляр, то мощение производится по строкам, а не по столбцам. Но второй аргумент tile может быть
кортежем, описывающим порядок мощения:
In [64]: arr
Out[64]:
array([[–2.0016, –0.3718],
[ 1.669 , –0.4386]])
In [65]: np.tile(arr, (2, 1))
Out[65]:
array([[–2.0016, –0.3718],
[ 1.669 , –0.4386],
[–2.0016, –0.3718],
[ 1.669 , –0.4386]])
In [66]: np.tile(arr, (3, 2))
Out[66]:
array([[–2.0016, –0.3718, –2.0016, –0.3718],

Дополнительные манипуляции с массивами
[ 1.669 ,
[–2.0016,
[ 1.669 ,
[–2.0016,
[ 1.669 ,

–0.4386,
–0.3718,
–0.4386,
–0.3718,
–0.4386,

1.669 ,
–2.0016,
1.669 ,
–2.0016,
1.669 ,

479

–0.4386],
–0.3718],
–0.4386],
–0.3718],
–0.4386]])

Эквиваленты прихотливого индексирования:
функции take и put
В главе 4 описывался способ получить и установить подмножество массива
с помощью прихотливого индексирования массивами целых чисел:
In [67]: arr = np.arange(10) * 100
In [68]: inds = [7, 1, 2, 6]
In [69]: arr[inds]
Out[69]: array([700, 100, 200, 600])

Существуют и другие методы ndarray, полезные в частном случае, когда
выборка производится только по одной оси:
In [70]: arr.take(inds)
Out[70]: array([700, 100, 200, 600])
In [71]: arr.put(inds, 42)
In [72]: arr
Out[72]: array([ 0, 42, 42, 300, 400, 500, 42, 42, 800, 900])
In [73]: arr.put(inds, [40, 41, 42, 43])
In [74]: arr
Out[74]: array([ 0, 41, 42, 300, 400, 500, 43, 40, 800, 900])

Чтобы использовать функцию take для других осей, нужно передать именованный параметр axis:
In [75]: inds = [2, 0, 2, 1]
In [76]: arr = np.random.randn(2, 4)
In [77]: arr
Out[77]:
array([[–0.5397, 0.477 , 3.2489, –1.0212],
[–0.5771, 0.1241, 0.3026, 0.5238]])
In [78]: arr.take(inds, axis=1)
Out[78]:
array([[ 3.2489, –0.5397, 3.2489, 0.477 ],
[ 0.3026, –0.5771, 0.3026, 0.1241]])

480

Дополнительные сведения о библиотеке NumPy

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

A.3. Укладывание
Словом «укладывание» (broadcasting) описывается способ выполнения арифметических операций над массивами разной формы. Это очень мощный
механизм, но даже опытные пользователи иногда испытывают затруднения
с его пониманием. Простейший пример укладывания – комбинирование скалярного значения с массивом:
In [79]: arr = np.arange(5)
In [80]: arr
Out[80]: array([0, 1, 2, 3, 4])
In [81]: arr * 4
Out[81]: array([ 0, 4, 8, 12, 16])

Здесь мы говорим, что скалярное значение 4 уложено на все остальные
элементы в результате операции умножения.
Другой пример: мы можем сделать среднее по столбцам массива равным
нулю, вычтя из каждого столбца столбец, содержащий средние значения.
И сделать это очень просто:
In [82]: arr = np.random.randn(4, 3)
In [83]: arr.mean(0)
Out[83]: array([–0.3928, –0.3824, –0.8768])
In [84]: demeaned = arr – arr.mean(0)
In [85]: demeaned
Out[85]:
array([[ 0.3937, 1.7263,
[–0.4384, –1.9878,
[–0.468 , 0.9426,
[ 0.5126, –0.6811,

0.1633],
–0.9839],
–0.3891],
1.2097]])

In [86]: demeaned.mean(0)
Out[86]: array([–0., 0., –0.])

Эта операция показана на рис. A.4. Для приведения к нулю средних по
строкам с помощью укладывания требуется проявить осторожность. К счастью, укладывание значений меньшей размерности вдоль любого измерения
массива (например, вычитание средних по строкам из каждого столбца двумерного массива) возможно при соблюдении следующего правила:

481

Укладывание

Правило укладывания
Два массива совместимы по укладыванию, если для обоих последних измерений (т. е. отсчитываемых с конца) длины осей совпадают или хотя бы одна
длина равна 1. Тогда укладывание производится по отсутствующим измерениям или по измерениям длины 1.

Рис. A.4. Укладывание одномерного массива по оси 0

Даже я, опытный пользователь NumPy, иногда вынужден рисовать картинки, чтобы понять, как будет применяться правило укладывания. Вернемся
к последнему примеру и предположим, что мы хотим вычесть среднее значение из каждой строки, а не из каждого столбца. Поскольку длина массива
arr.mean(0) равна 3, он совместим по укладыванию вдоль оси 0, так как по
последнему измерению длины осей (три) совпадают. Согласно правилу, чтобы
произвести вычитание по оси 1 (т. е. вычесть среднее по строкам из каждой
строки), меньший массив должен иметь форму (4, 1):
In [87]: arr
Out[87]:
array([[ 0.0009,
[–0.8312,
[–0.8608,
[ 0.1198,

1.3438,
–2.3702,
0.5601,
–1.0635,

–0.7135],
–1.8608],
–1.2659],
0.3329]])

In [88]: row_means = arr.mean(1)
In [89]: row_means.shape
Out[89]: (4,)
In [90]: row_means.reshape((4, 1))
Out[90]:
array([[ 0.2104],
[–1.6874],
[–0.5222],
[–0.2036]])
In [91]: demeaned = arr – row_means.reshape((4, 1))

482

Дополнительные сведения о библиотеке NumPy

In [92]: demeaned.mean(1)
Out[92]: array([ 0., –0., 0., 0.])

Эта операция проиллюстрирована рис. A.5:

Рис. A.5. Укладывание двумерного массива по оси 1

На рис. А.6 приведена еще одна иллюстрация, где мы вычитаем двумерный
массив из трехмерного по оси 0.

Рис. A.6. Укладывание трехмерного массива по оси 0

Укладывание по другим осям
Укладывание многомерных массивов может показаться еще более головоломной задачей, но на самом деле нужно только соблюдать правило. Если
оно не соблюдено, то будет выдана ошибка вида:
In [93]: arr – arr.mean(1)
–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
ValueError
Traceback (most recent call last)
in ()
––––> 1 arr – arr.mean(1)
ValueError: operands could not be broadcast together with shapes (4,3) (4)

483

Укладывание

Очень часто возникает необходимость выполнить арифметическую операцию с массивом меньшей размерности по оси, отличной от 0. Согласно
правилу укладывания, длина «размерности укладывания» в меньшем массиве
должна быть равна 1. В примере вычитания среднего это означало, что массив средних по строкам должен иметь форму (4, 1), а не (4,):
In [94]: arr – arr.mean(1).reshape((4, 1))
Out[94]:
array([[–0.2095, 1.1334, –0.9239],
[ 0.8562, –0.6828, –0.1734],
[–0.3386, 1.0823, –0.7438],
[ 0.3234, –0.8599, 0.5365]])

В трехмерном случае укладывание по любому из трех измерений сводится
к изменению формы данных для обеспечения совместимости массивов. На
рис. А.7 наглядно показано, каковы должны быть формы для укладывания
по любой оси трехмерного массива.
Форма полного массива: (8, 5, 3)

Ось 2: (8, 5, 1)

Ось 0: (5, 3)
(1, 5, 3)

Ось 1: (8, 1, 3)

Рис. A.7. Совместимые формы двумерного массива
для укладывания в трехмерный массив

Поэтому часто приходится добавлять новую ось длины 1 специально для
укладывания, особенно в обобщенных алгоритмах. Один из вариантов – использование reshape, но для вставки оси нужно построить кортеж, описывающий новую форму. Это утомительное занятие. Поэтому в NumPy имеется
специальный синтаксис для вставки новых осей путем доступа по индексу. Чтобы вставить новую ось, мы воспользуемся специальным атрибутом
np.newaxis и «полными» срезами:
In [95]: arr = np.zeros((4, 4))
In [96]: arr_3d = arr[:, np.newaxis, :]

484

Дополнительные сведения о библиотеке NumPy

In [97]: arr_3d.shape
Out[97]: (4, 1, 4)
In [98]: arr_1d = np.random.normal(size=3)
In [99]: arr_1d[:, np.newaxis]
Out[99]:
array([[–2.3594],
[–0.1995],
[–1.542 ]])
In [100]: arr_1d[np.newaxis, :]
Out[100]: array([[–2.3594, –0.1995, –1.542 ]])

Таким образом, если имеется трехмерный массив и требуется привести его
к нулевому среднему, скажем, по оси 2, то нужно написать:
In [101]: arr = np.random.randn(3, 4, 5)
In [102]: depth_means = arr.mean(2)
In [103]: depth_means
Out[103]:
array([[–0.4735, 0.3971, –0.0228, 0.2001],
[–0.3521, –0.281 , –0.071 , –0.1586],
[ 0.6245, 0.6047, 0.4396, –0.2846]])
In [104]: depth_means.shape
Out[104]: (3, 4)
In [105]: demeaned = arr – depth_means[:, :, np.newaxis]
In [106]: demeaned.mean(2)
Out[106]:
array([[ 0., 0., –0., –0.],
[ 0., 0., –0., 0.],
[ 0., 0., –0., –0.]])

Возможно, вас интересует, нет ли способа обобщить вычитание среднего вдоль оси, не жертвуя производительностью. Есть, но придется попотеть
с индексированием:
def demean_axis(arr, axis=0):
means = arr.mean(axis)
# Это обобщает операции вида [:, :, np.newaxis] на N измерений
indexer = [slice(None)] * arr.ndim
indexer[axis] = np.newaxis
return arr – means[indexer]

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

Дополнительные способы использования универсальных функций

485

In [107]: arr = np.zeros((4, 3))
In [108]: arr[:] = 5
In [109]: arr
Out[109]:
array([[ 5., 5.,
[ 5., 5.,
[ 5., 5.,
[ 5., 5.,

5.],
5.],
5.],
5.]])

Однако если имеется одномерный массив значений, который требуется
записать в столбцы массива, то можно сделать и это – при условии совместимости формы:
In [110]: col = np.array([1.28, –0.42, 0.44, 1.6])
In [111]: arr[:] = col[:, np.newaxis]
In [112]: arr
Out[112]:
array([[ 1.28,
[–0.42,
[ 0.44,
[ 1.6 ,

1.28,
–0.42,
0.44,
1.6 ,

1.28],
–0.42],
0.44],
1.6 ]])

In [113]: arr[:2] = [[–1.37], [0.509]]
In [114]: arr
Out[114]:
array([[–1.37 ,
[ 0.509,
[ 0.44 ,
[ 1.6 ,

–1.37 ,
0.509,
0.44 ,
1.6 ,

–1.37 ],
0.509],
0.44 ],
1.6 ]])

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

Методы экземпляра u-функций
Любая бинарная u-функция в NumPy имеет специальные методы для
выполнения некоторых видов векторных операций. Все они перечислены
в табл. А.2, но я приведу и несколько конкретных примеров для иллюстрации.
Метод reduce принимает массив и агрегирует его, возможно, вдоль указанной оси, выполняя последовательность бинарных операций. Вот, например,
как можно с помощью np.add.reduce просуммировать элементы массива:

486

Дополнительные сведения о библиотеке NumPy

In [115]: arr = np.arange(10)
In [116]: np.add.reduce(arr)
Out[116]: 45
In [117]: arr.sum()
Out[117]: 45

Начальное значение (для add оно равно 0) зависит от u-функции. Если задана ось, то редукция производится вдоль этой оси. Это позволяет давать
краткие ответы на некоторые вопросы. В качестве не столь тривиального
примера воспользуемся методом np.logical_and, чтобы проверить, отсортированы ли значения в каждой строке массива:
In [118]: np.random.seed(12346)

# для воспроизводимости

In [119]: arr = np.random.randn(5, 5)
In [120]: arr[::2].sort(1) # отсортировать несколько строк
In [121]: arr[:, :–1]
Out[121]:
array([[ True, True,
[False, True,
[ True, True,
[ True, False,
[ True, True,

< arr[:, 1:]
True,
False,
True,
True,
True,

True],
False],
True],
True],
True]], dtype=bool)

In [122]: np.logical_and.reduce(arr[:, :–1] < arr[:, 1:], axis=1)
Out[122]: array([ True, False, True, False, True], dtype=bool)

Отметим, что logical_and.reduce эквивалентно методу all.
Метод accumulate соотносится с reduce, как cumsum с sum. Он порождает массив
того же размера, содержащий промежуточные «аккумулированные» значения:
In [123]: arr = np.arange(15).reshape((3, 5))
In [124]: np.add.accumulate(arr, axis=1)
Out[124]:
array([[ 0, 1, 3, 6, 10],
[ 5, 11, 18, 26, 35],
[10, 21, 33, 46, 60]])

Метод outer вычисляет прямое произведение двух массивов:
In [125]: arr = np.arange(3).repeat([1, 2, 2])
In [126]: arr
Out[126]: array([0, 1, 1, 2, 2])

Powered by TCPDF (www.tcpdf.org)

Дополнительные способы использования универсальных функций

487

In [127]: np.multiply.outer(arr, np.arange(5))
Out[127]:
array([[0, 0, 0, 0, 0],
[0, 1, 2, 3, 4],
[0, 1, 2, 3, 4],
[0, 2, 4, 6, 8],
[0, 2, 4, 6, 8]])

Размерность массива, возвращенного outer, равна сумме размерностей его
параметров:
In [128]: x, y = np.random.randn(3, 4), np.random.randn(5)
In [129]: result = np.subtract.outer(x, y)
In [130]: result.shape
Out[130]: (3, 4, 5)

Последний метод, reduceat, выполняет локальную редукцию, т. е. по существу операцию groupby, в которой агрегируется сразу несколько срезов
массива. Он принимает последовательность «границ интервалов», описывающую, как разбивать и агрегировать значения:
In [131]: arr = np.arange(10)
In [132]: np.add.reduceat(arr, [0, 5, 8])
Out[132]: array([10, 18, 17])

На выходе получаются результаты редукции (в данном случае суммирования) по срезам arr[0:5], arr[5:8] и arr[8:]. Как и другие методы, reduceat
принимает необязательный аргумент axis:
In [133]: arr = np.multiply.outer(np.arange(4), np.arange(5))
In [134]: arr
Out[134]:
array([[ 0, 0,
[ 0, 1,
[ 0, 2,
[ 0, 3,

0,
2,
4,
6,

0,
3,
6,
9,

0],
4],
8],
12]])

In [135]: np.add.reduceat(arr, [0, 2, 4], axis=1)
Out[135]:
array([[ 0, 0, 0],
[ 1, 5, 4],
[ 2, 10, 8],
[ 3, 15, 12]])

Неполный перечень методов u-функций приведен в табл. A.2.

488

Дополнительные сведения о библиотеке NumPy

Таблица A.2. Методы u-функций
Метод

Описание

reduce(x)
accumulate(x)

Агрегирует значения путем последовательного применения операции
Агрегирует значения, сохраняя все промежуточные агрегаты
Локальная редукция, или «group by». Редуцирует соседние срезы данных и порождает массив агрегатов
Применяет операцию ко всем парам элементов x и y. Результирующий массив
имеет форму x.shape + y.shape

reduceat(x, bins)
outer(x, y)

Написание новых u-функций на Python
Для создания собственных u-функций для NumPy существует несколько механизмов. Самый общий – использовать C API NumPy, но он выходит за рамки
книги. В этом разделе будем рассматривать u-функции на чистом Python.
Метод numpy.frompyfunc принимает функцию Python и спецификацию количества входов и выходов. Например, простую функцию, выполняющую поэлементное сложение, можно было бы описать так:
In [136]: def add_elements(x, y):
.....:
return x + y
In [137]: add_them = np.frompyfunc(add_elements, 2, 1)
In [137]: add_them(np.arange(8), np.arange(8))
Out[137]: array([0, 2, 4, 6, 8, 10, 12, 14], dtype=object)

Функции, созданные методом frompyfunc, всегда возвращают массивы объектов Python, что не очень удобно. По счастью, есть альтернативный, хотя
и не столь функционально богатый метод numpy.vectorize, который умеет лучше выводить типы:
In [139]: add_them = np.vectorize(add_elements, otypes=[np.float64])
In [140]: add_them(np.arange(8), np.arange(8))
Out[140]: array([ 0., 2., 4., 6., 8., 10., 12., 14.])

Оба метода позволяют создавать аналоги u-функций, которые, правда, работают очень медленно, потому что должны вызывать функцию Python для
вычисления каждого элемента, а это далеко не так эффективно, как циклы
в написанных на C универсальных функциях NumPy:
In [141]: arr = np.random.randn(10000)
In [142]: %timeit add_them(arr, arr)
4.12 ms +– 182 us per loop (mean +– std. dev. of 7 runs, 100 loops each)
In [143]: %timeit np.add(arr, arr)
6.89 us +– 504 ns per loop (mean +– std. dev. of 7 runs, 100000 loops each)

Ниже в этой главе мы покажем, как создавать быстрые u-функции на Python с помощью проекта Numba (http://numba.pydata.org/).

Структурные массивы

489

А.5. Структурные массивы
Вы, наверное, обратили внимание, что все рассмотренные до сих пор примеры ndarray были контейнерами однородных данных, т. е. блоками памяти,
в которых каждый элемент занимает одно и то же количество байтов, определяемое типом данных dtype. Создается впечатление, что представить в виде массива неоднородные данные, как в таблице, невозможно. Структурный
массив – это объект ndarray, в котором каждый элемент можно рассматривать
как аналог структуры (struct) в языке C (отсюда и название «структурный»)
или строки в таблице SQL, содержащий несколько именованных полей:
In [144]: dtype = [('x', np.float64), ('y', np.int32)]
In [145]: sarr = np.array([(1.5, 6), (np.pi, –2)], dtype=dtype)
In [146]: sarr
Out[146]:
array([(1.5, 6), (3.1416, –2)],
dtype=[('x', ' 9
assert(a + b == 10)
10
11 def calling_things():
AssertionError:
In [3]: %debug
/home/wesm/code/pydata–book/examples/ipython_bug.py(9)throws_an_exception()
8
b = 6
––––> 9
assert(a + b == 10)
10
ipdb>

Находясь в отладчике, можно выполнять произвольный Python-код и просматривать все объекты и данные (которые интерпретатор «сохранил живыми») в каждом кадре стека. По умолчанию отладчик оказывается на самом
нижнем уровне – там, где произошла ошибка. Клавиши u (вверх) и d (вниз)
позволяют переходить с одного уровня стека на другой:
ipdb> u
> /home/wesm/code/pydata–book/examples/ipython_bug.py(13)calling_things()
12
works_fine()
–––> 13
throws_an_exception()
14

Команда %pdb устанавливает режим, в котором IPython автоматически вызывает отладчик после любого исключения, многие считают этот режим особенно полезным.
Отладчик также помогает разрабатывать код, особенно когда хочется расставить точки останова либо пройти функцию или скрипт в пошаговом ре-

Средства разработки программ

509

жиме, изучая состояния после каждого шага. Сделать это можно несколькими
способами. Первый – воспользоваться функцией %run с флагом –d, которая
вызывает отладчик, перед тем как начать выполнение кода в переданном
скрипте. Для входа в скрипт нужно сразу же нажать s (step – пошаговый
режим):
In [5]: run –d examples/ipython_bug.py
Breakpoint 1 at /home/wesm/code/pydata–book/examples/ipython_bug.py:1
NOTE: Enter 'c' at the ipdb> prompt to start your script.
> (1)()
ipdb> s
––Call––
> /home/wesm/code/pydata–book/examples/ipython_bug.py(1)()
1–––> 1 def works_fine():
2
a = 5
3
b = 6

После этого вы сами решаете, каким образом работать с файлом. Например, в приведенном выше примере исключения можно было бы поставить
точку останова прямо перед вызовом метода works_fine и выполнить программу до этой точки, нажав c (continue – продолжить):
ipdb> b 12
ipdb> c
> /home/wesm/code/pydata–book/examples/ipython_bug.py(12)calling_things()
11 def calling_things():
2––> 12
works_fine()
13
throws_an_exception()

В этот момент можно войти внутрь works_fine() командой step или выполнить works_fine() без захода внутрь, т. е. перейти к следующей строке, нажав
n (next – дальше):
ipdb> n
> /home/wesm/code/pydata–book/examples/ipython_bug.py(13)calling_things()
2
12
works_fine()
–––> 13
throws_an_exception()
14

Далее мы можем войти внутрь throws_an_exception, дойти до строки, где возникает ошибка, и изучить переменные в текущей области видимости. Отметим, что у команд отладчика больший приоритет, чем у имен переменных,
поэтому для просмотра переменной с таким же именем, как у команды, необходимо предпослать ей знак !.
ipdb> s
––Call––
> /home/wesm/code/pydata–book/examples/ipython_bug.py(6)throws_an_exception()
5

510

Еще о системе IPython

––––> 6 def throws_an_exception():
7
a = 5
ipdb> n
> /home/wesm/code/pydata–book/examples/ipython_bug.py(7)throws_an_exception()
6 def throws_an_exception():
––––> 7
a = 5
8
b = 6
ipdb> n
> /home/wesm/code/pydata–book/examples/ipython_bug.py(8)throws_an_exception()
7
a = 5
––––> 8
b = 6
9
assert(a + b == 10)
ipdb> n
> /home/wesm/code/pydata–book/examples/ipython_bug.py(9)throws_an_exception()
8
b = 6
––––> 9 assert(a + b == 10)
10
ipdb> !a
5
ipdb> !b
6

Уверенное владение интерактивным отладчиком приходит с опытом
и практикой. В табл. B.2 приведен полный перечень команд отладчика. Если
вы привыкли к IDE, то консольный отладчик на первых порах может показаться неуклюжим, но со временем это впечатление рассеется. В некоторых
IDE для Python имеются отличные графические отладчики, так что всякий
пользователь найдет что-то себе по вкусу.
Таблица B.2. Команды отладчика (I)Python
Команда

Действие

h(elp)

Вывести список команд
Показать документацию по команде
Продолжить выполнение программы
Выйти из отладчика, прекратив выполнение кода
Поставить точку останова на строке с указанным номером в текущем файле
Поставить точку останова на строке с указанным номером в указанном файле
Войти внутрь функции
Выполнить текущую строку и перейти к следующей на текущем уровне
Перемещение вверх и вниз по стеку вызовов
Показать аргументы текущей функции
Выполнить предложение в новом (вложенном) отладчике
Показать текущую позицию и контекст на текущем уровне стека
Распечатать весь стек в контексте текущей позиции

help команда
c(ontinue)
q(uit)
b(reak) номер
b путь/к/файлу.py:номер
s(tep)
n(ext)
u(p) / d(own)
a(rgs)
debug предложение
l(ist) предложение
w(here)

Средства разработки программ

511

Другие способы работы с отладчиком
Существует еще два полезных способа вызова отладчика. Первый – воспользоваться специальной функцией set_trace (названной так по аналогии
с pdb.set_trace), которая по существу является упрощенным вариантом точки останова. Вот два небольших фрагмента, которые вы можете сохранить
где-нибудь и использовать в разных программах (я, например, помещаю их
в свой профиль IPython):
from IPython.core.debugger import Pdb
def set_trace():
from IPython.core.debugger import Pdb
Pdb(color_scheme='Linux').set_trace(sys._getframe().f_back)
def debug(f, *args, **kwargs):
pdb = Pdb(color_scheme='Linux')
return pdb.runcall(f, *args, **kwargs)

Первая функция, set_trace, совсем простая. Вызывайте ее в той точке кода,
где хотели бы остановиться и оглядеться (например, прямо перед строкой,
в которой происходит исключение):
In [7]: run examples/ipython_bug.py
> /home/wesm/code/pydata–book/examples/ipython_bug.py(16)calling_things()
15
set_trace()
–––> 16
throws_an_exception()
17

При нажатии c (продолжить) выполнение программы возобновится без
каких-либо побочных эффектов.
Функция debug позволяет вызвать интерактивный отладчик в момент обращения к любой функции. Допустим, мы написали такую функцию и хотели
бы пройти ее в пошаговом режиме:
def f(x, y, z=1):
tmp = x + y
return tmp / z

Обычно f используется примерно так: f(1, 2, z=3). А чтобы войти в эту
функцию, передайте f в качестве первого аргумента функции debug, а затем
ее позиционные и именованные аргументы:
In [6]: debug(f, 1, 2, z=3)
> (2)f()
1 def f(x, y, z):
––––> 2
tmp = x + y
3
return tmp / z
ipdb>

Мне эти две простенькие функции ежедневно экономят уйму времени.

512

Еще о системе IPython

Наконец, отладчик можно использовать в сочетании с функцией %run. Запустив скрипт командой %run –d, вы попадете прямо в отладчик и сможете
расставить точки останова и начать выполнение:
In [1]: %run –d examples/ipython_bug.py
Breakpoint 1 at /home/wesm/code/pydata–book/examples/ipython_bug.py:1
NOTE: Enter 'c' at the ipdb> prompt to start your script.
> (1)()
ipdb>

Если добавить еще флаг –b, указав номер строки, то после входа в отладчик
на этой строке уже будет стоять точка останова:
In [2]: %run –d –b2 examples/ipython_bug.py
Breakpoint 1 at /home/wesm/code/pydata–book/examples/ipython_bug.py:2
NOTE: Enter 'c' at the ipdb> prompt to start your script.
> (1)()
ipdb> c
> /home/wesm/code/pydata–book/examples/ipython_bug.py(2)works_fine()
1 def works_fine():
1–––> 2
a = 5
3
b = 6
ipdb>

Хронометраж программы: %time и %timeit
Для больших или долго работающих аналитических приложений бывает
желательно измерить время выполнения различных участков кода или даже
отдельных предложений или вызовов функций. Интересно получить отчет
о том, какие функции занимают больше всего времени в сложном процессе.
По счастью, IPython позволяет без труда получить эту информацию по ходу
разработки и тестирования программы.
Ручной хронометраж с помощью встроенного модуля time и его функций
time.clock и time.time зачастую оказывается скучной и утомительной процедурой, поскольку приходится писать один и тот же неинтересный код:
import time
start = time.time()
for i in range(iterations):
# здесь код, который требует хронометрировать
elapsed_per = (time.time() – start) / iterations

Так как эта операция встречается очень часто, в IPython есть две магические функции, %time и %timeit, которые помогают автоматизировать процесс.
Функция %time выполняет предложение один раз и сообщает, сколько было
затрачено времени. Допустим, имеется длинный список строк и мы хотим
сравнить различные методы выбора всех строк, начинающихся с заданного

Средства разработки программ

513

префикса. Вот простой список, содержащий 700 000 строк, и два метода выборки тех, что начинаются с 'foo':
# очень длинный список строк
strings = ['foo', 'foobar', 'baz', 'qux',
'python', 'Guido Van Rossum'] * 100000
method1 = [x for x in strings if x.startswith('foo')]
method2 = [x for x in strings if x[:3] == 'foo']

На первый взгляд производительность должна быть примерно одинаковой,
верно? Проверим с помощью функции %time:
In [561]: %time method1 = [x for x in strings if x.startswith('foo')]
CPU times: user 0.19 s, sys: 0.00 s, total: 0.19 s
Wall time: 0.19 s
In [562]: %time method2 = [x for x in strings if x[:3] == 'foo']
CPU times: user 0.09 s, sys: 0.00 s, total: 0.09 s
Wall time: 0.09 s

Наибольший интерес представляет величина Wall time (фактическое время). Похоже, первый метод работает в два раза медленнее второго, но это
не очень точное измерение. Если вы несколько раз сами замерите время работы этих двух предложений, то убедитесь, что результаты варьируются. Для
более точного измерения воспользуемся магической функцией %timeit. Она
получает произвольное предложение и, применяя внутренние эвристики, выполняет его столько раз, сколько необходимо для получения более точного
среднего времени:
In [563]: %timeit [x for x in strings if x.startswith('foo')]
10 loops, best of 3: 159 ms per loop
In [564]: %timeit [x for x in strings if x[:3] == 'foo']
10 loops, best of 3: 59.3 ms per loop

Этот на первый взгляд безобидный пример показывает, насколько важно хорошо понимать характеристики производительности стандартной библиотеки Python, NumPy, pandas и других используемых в книге библиотек.
В больших приложениях для анализа данных из миллисекунд складываются
часы!
Функция %timeit особенно полезна для анализа предложений и функций,
работающих очень быстро, за микросекунды (10-6 секунд) или наносекунды (10-9 секунд). Вроде бы совсем мизерные промежутки времени, но если
функцию, работающую 20 микросекунд, вызвать 1 000 000 раз, то будет потрачено на 15 секунд больше, чем если бы она работала всего 5 микросекунд.
В примере выше можно сравнить две операции со строками напрямую, это
даст отчетливое представление об их характеристиках в плане производительности:

514

Еще о системе IPython

In [565]: x = 'foobar'
In [566]: y = 'foo'
In [567]: %timeit x.startswith(y)
1000000 loops, best of 3: 267 ns per loop
In [568]: %timeit x[:3] == y
10000000 loops, best of 3: 147 ns per loop

Простейшее профилирование: %prun и %run -p
Профилирование кода тесно связано с хронометражем, только отвечает на
вопрос, где именно тратится время. В Python основное средство профилирования – модуль cProfile, который предназначен отнюдь не только для IPython.
cProfile исполняет программу или произвольный блок кода и следит за тем,
сколько времени проведено в каждой функции.
Обычно cProfile запускают из командной строки, профилируют программу целиком и выводят агрегированные временные характеристики каждой
функции. Пусть имеется простой скрипт, который выполняет в цикле какойнибудь алгоритм линейной алгебры (скажем, вычисляет максимальное по абсолютной величине собственное значение для последовательности матриц
размерности 100×100):
import numpy as np
from numpy.linalg import eigvals
def run_experiment(niter=100):
K = 100
results = []
for _ in range(niter):
mat = np.random.randn(K, K)
max_eigenvalue = np.abs(eigvals(mat)).max()
results.append(max_eigenvalue)
return results
some_results = run_experiment()
print('Самое большое встретившееся: {0}'.format(np.max(some_results)))

Этот скрипт можно запустить под управлением cProfile из командной
строки следующим образом:
python –m cProfile cprof_example.py

Попробуйте и убедитесь, что результаты отсортированы по имени функции. Такой отчет не позволяет сразу увидеть, где тратится время, поэтому
обычно порядок сортировки задают с помощью флага –s:
$ python –m cProfile –s cumulative cprof_example.py

Самое большое встретившееся: 11.923204422.
15116 function calls (14927 primitive calls) in 0.720 seconds

Средства разработки программ

515

Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
1
0.001 0.001
0.721 0.721 cprof_example.py:1()
100
0.003 0.000
0.586 0.006 linalg.py:702(eigvals)
200
0.572 0.003
0.572 0.003 {numpy.linalg.lapack_lite.dgeev}
1
0.002 0.002
0.075 0.075 __init__.py:106()
100
0.059 0.001
0.059 0.001 {method 'randn')
1
0.000 0.000
0.044 0.044 add_newdocs.py:9()
2
0.001 0.001
0.037 0.019 __init__.py:1()
2
0.003 0.002
0.030 0.015 __init__.py:2()
1
0.000 0.000
0.030 0.030 type_check.py:3()
1
0.001 0.001
0.021 0.021 __init__.py:15()
1
0.013 0.013
0.013 0.013 numeric.py:1()
1
0.000 0.000
0.009 0.009 __init__.py:6()
1
0.001 0.001
0.008 0.008 __init__.py:45()
262
0.005 0.000
0.007 0.000 function_base.py:3178(add_newdoc)
100
0.003 0.000
0.005 0.000 linalg.py:162(_assertFinite)
...

Показаны только первые 15 строк отчета. Читать его проще всего, просматривая сверху вниз столбец cumtime, чтобы понять, сколько времени было
проведено внутри каждой функции. Отметим, что если одна функция вызывает другую, то таймер не останавливается. cProfile запоминает моменты
начала и конца каждого вызова функции и на основе этих данных создает
отчет о затраченном времени.
cProfile можно запускать не только из командной строки, но и программно для профилирования работы произвольных блоков кода без порождения
нового процесса. В IPython имеется удобный интерфейс к этой функциональности в виде команды %prun и команды %run с флагом –p. Команда %prun принимает те же «аргументы командной строки», что и cProfile, но профилирует
произвольное предложение Python, а не py-файл:
In [4]: %prun –l 7 –s cumulative run_experiment()
4203 function calls in 0.643 seconds
Ordered by: cumulative time
List reduced from 32 to 7 due to restriction
ncalls tottime percall cumtime percall filename:lineno(function)
1
0.000 0.000 0.643
0.643 :1()
1
0.001 0.001 0.643
0.643 cprof_example.py:4(run_experiment)
100
0.003 0.000 0.583
0.006 linalg.py:702(eigvals)
200
0.569 0.003 0.569
0.003 {numpy.linalg.lapack_lite.dgeev}
100
0.058 0.001 0.058
0.001 {method 'randn'}
100
0.003 0.000 0.005
0.000 linalg.py:162(_assertFinite)
200
0.002 0.000 0.002
0.000 {method 'all' of 'numpy.ndarray' objects}

Аналогично команда %run –p –s cumulative cprof_example.py дает тот же результат, что рассмотренный выше запуск из командной строки, только не
приходится выходить из IPython.

516

Еще о системе IPython

В Jupyter-блокноте для профилирования целого блока кода можно использовать магическую команду %%prun (два знака %). Онаоткрывает отдельное
окно, в которое выводится профиль. Это полезно для быстрого ответа на
вопросы типа «Почему этот блок так долго работает?».
Существуют и другие инструменты, которые помогают интерпретировать
профиль при работе с IPython или Jupyter. Один из них – SnakeViz (https://
github.com/jiffyclub/snakeviz/) – порождает интерактивную визуализацию результатов профилирования с помощью библиотеки d3.js.

Построчное профилирование функции
Иногда информации, полученной от %prun (или добытой иным способом
профилирования на основе cProfile), недостаточно, чтобы составить полное
представление о времени работы функции. Или она настолько сложна, что
результаты, агрегированные по имени функции, с трудом поддаются интерпретации. На такой случай есть небольшая библиотека line_profiler (ее поможет установить PyPI или любой другой инструмент управления пакетами).
Она содержит расширение IPython, включающее новую магическую функцию
%lprun, которая строит построчный профиль выполнения одной или нескольких функций. Чтобы подключить это расширение, нужно модифицировать
конфигурационный файл IPython (см. документацию по IPython или раздел,
посвященный конфигурированию, ниже), добавив такую строку:
# Список имен загружаемых модулей с расширениями IPython.
c.TerminalIPythonApp.extensions = ['line_profiler']

Библиотеку line_profiler можно использовать из программы (см. полную
документацию), но, пожалуй, наиболее эффективна интерактивная работа
с ней в IPython. Допустим, имеется модуль prof_mod, содержащий следующий
код, в котором выполняются операции с массивом NumPy:
from numpy.random import randn
def add_and_sum(x, y):
added = x + y
summed = added.sum(axis=1)
return summed
def call_function():
x = randn(1000, 1000)
y = randn(1000, 1000)
return add_and_sum(x, y)

Если бы нам нужно было оценить производительность функции add_and_sum,
то команда %prun дала бы такие результаты:
In [569]: %run prof_mod
In [570]: x = randn(3000, 3000)
In [571]: y = randn(3000, 3000)

Средства разработки программ

517

In [572]: %prun add_and_sum(x, y)
4 function calls in 0.049 seconds
Ordered by: internal time
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.036 0.036 0.046 0.046 prof_mod.py:3(add_and_sum)
1 0.009 0.009 0.009 0.009 {method 'sum' of 'numpy.ndarray' objects}
1 0.003 0.003 0.049 0.049 :1()

Не слишком полезно. Но после активации расширения IPython line_profiler
становится доступна новая команда %lprun. От %prun она отличается только
тем, что мы указываем, какую функцию (или функции) хотим профилировать. Порядок вызова такой:
%lprun –f func1 –f func2 профилируемое_предложение

В данном случае мы хотим профилировать функцию add_and_sum, поэтому
пишем:
In [573]: %lprun –f add_and_sum add_and_sum(x, y)
Timer unit: 1e–06 s
File: book_scripts/prof_mod.py
Function: add_and_sum at line 3
Total time: 0.045936 s
Line
# Hits
Time Per Hit % Time Line Contents
==============================================================
3
def add_and_sum(x, y):
4
1 36510
36510.0
79.5
added = x + y
5
1
9425
9425.0
20.5
summed = added.sum(axis=1)
6
1
1
1.0
0.0
return summed

Так гораздо понятнее. В этом примере мы профилировали ту же функцию,
которая составляла предложение. Но можно было бы вызвать функцию call_
function из показанного выше модуля и профилировать ее наряду с add_and_
sum, это дало бы полную картину производительности кода:
In [574]: %lprun –f add_and_sum –f call_function call_function()
Timer unit: 1e–06 s
File: book_scripts/prof_mod.py
Function: add_and_sum at line 3
Total time: 0.005526 s
Line
# Hits
Time Per Hit % Time Line Contents
==============================================================
3
def add_and_sum(x, y):
4
1
4375
4375.0
79.2
added = x + y
5
1
1149
1149.0
20.8
summed = added.sum(axis=1)
6
1
2
2.0
0.0
return summed
File: book_scripts/prof_mod.py
Function: call_function at line 8
Total time: 0.121016 s

518

Еще о системе IPython

Line
# Hits
Time Per Hit % Time Line Contents
==============================================================
8
def call_function():
9
1
57169 57169.0
47.2
x = randn(1000, 1000)
10
1
58304 58304.0
48.2
y = randn(1000, 1000)
11
1
5543
5543.0
4.6
return add_and_sum(x, y)

Обычно я предпочитаю использовать %prun (cProfile) для «макропрофилирования», а %lprun (line_profiler) – для «микропрофилирования». Полезно
освоить оба инструмента.
Явно указывать имена подлежащих профилированию функций в команде %lp­
run необходимо, потому что накладные расходы на трассировку выполнения
каждой строки весьма значительны. Трассировка функций, не представляющих
интереса, может существенно изменить результаты профилирования.

B.4. Советы по продуктивной разработке кода
с использованием IPython
Создание кода таким образом, чтобы его можно было разрабатывать, отлаживать и в конечном счете использовать интерактивно, многим может показаться сменой парадигмы. Придется несколько изменить подходы к таким процедурным деталям, как перезагрузка кода, а также сам стиль кодирования.
Поэтому реализация стратегий, описанных в этом разделе, – скорее искусство, чем наука, вы должны будете экспериментально определить наиболее эффективный для себя способ написания Python-кода. Конечная задача – структурировать код так, чтобы с ним было легко работать интерактивно
и изучать результаты прогона всей программы или отдельной функции с наименьшими усилиями. Я пришел к выводу, что программу, спроектированную
в расчете на IPython, использовать проще, чем аналогичную, но построенную
как автономное командное приложение. Это становится особенно важно, когда возникает какая-то проблема и нужно найти ошибку в коде, написанном
вами или кем-то еще несколько месяцев или лет назад.

Перезагрузка зависимостей модуля
Когда в Python-программе впервые встречается предложение import some_
lib, выполняется код из модуля some_lib и все переменные, функции и импортированные модули сохраняются во вновь созданном пространстве имен
модуля some_lib. При следующей обработке предложения import some_lib будет
возвращена ссылка на уже существующее пространство имен модуля. При
интерактивной разработке кода возникает проблема: как быть, когда, скажем,
с помощью команды %run выполняется скрипт, зависящий от другого модуля,
в который вы внесли изменения? Допустим, в файле test_script.py находится
такой код:

Советы по продуктивной разработке кода с использованием IPython

519

import some_lib
x = 5
y = [1, 2, 3, 4]
result = some_lib.get_answer(x, y)

Если выполнить %run test_script.py, а затем изменить some_lib.py, то при
следующем выполнении %run test_script.py мы получим старую версию some_
lib.py из-за принятого в Python механизма однократной загрузки. Такое поведение отличается от некоторых других сред анализа данных, например от
MATLAB, в которых изменения кода распространяются автоматически1. Справиться с этой проблемой можно двумя способами. Во-первых, использовать
функцию reload из модуля importlib стандартной библиотеки:
import some_lib
import importlib
importlib.reload(some_lib)

При этом гарантируется получение новой копии some_lib.py при каждом
запуске test_script.py. Очевидно, что если глубина вложенности зависимостей больше единицы, то вставлять reload повсюду становится утомительно.
Поэтому в IPython имеется специальная функция dreload (не магическая), выполняющая «глубокую» (рекурсивную) перезагрузку модулей. Если в файле
some_lib.py имеется предложение dreload(some_lib), то интерпретатор постарается перезагрузить как модуль some_lib, так и все его зависимости. К сожалению, это работает не во всех случаях, но если работает, то оказывается
куда лучше перезапуска всего IPython.

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

Сохраняйте ссылки на нужные объекты и данные
Программы, рассчитанные на запуск из командной строки, нередко структурируются, как показано в следующем тривиальном примере:
from my_functions import g
def f(x, y):
return g(x + y)
def main():
x = 6
1

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

520

Еще о системе IPython

y = 7.5
result = x + y
if __name__ == '__main__':
main()

Вы уже видите, что случится, если эту программу запустить в IPython?
После ее завершения все результаты или объекты, определенные в функции
main, будут недоступны в оболочке IPython. Лучше, если любой код, находящийся в main, будет исполняться прямо в глобальном пространстве имен модуля (или в блоке if __name__ == '__main__':, если вы хотите, чтобы и сам модуль
был импортируемым). Тогда после выполнения кода командой %run сможете
просмотреть все переменные, определенные в main. Это эквивалентно определению переменных верхнего уровня в ячейках Jupyter-блокнота.

Плоское лучше вложенного
Глубоко вложенный код напоминает мне луковицу. Сколько чешуй придется снять при тестировании или отладке функции, чтобы добраться до интересующего кода? Идея «плоское лучше вложенного» – часть «Дзен Python»,
применимая и к разработке кода, предназначенного для интерактивного
использования. Чем более модульными являются классы и функции и чем
меньше связей между ними, тем проще их тестировать (если вы пишете автономные тесты), отлаживать и использовать интерактивно.

Перестаньте бояться длинных файлов
Если вы раньше работали с Java (или аналогичным языком), то, наверное,
вам говорили, что чем файл короче, тем лучше. Во многих языках это разумный совет; длинный файл несет в себе дурной запашок и наводит на мысль
о необходимости рефакторинга или реорганизации. Однако при разработке
кода в IPython наличие десяти мелких (скажем, не более чем из 100 строчек)
взаимосвязанных файлов с большей вероятностью вызовет проблемы, чем
при работе всего с одним, двумя или тремя файлами подлиннее. Чем меньше
файлов, тем меньше нужно перезагружать модулей и тем реже приходится
переходить от файла к файлу в процессе редактирования. Я пришел к выводу, что сопровождение крупных модулей с высокой степенью внутренней
сцепленности гораздо полезнее и лучше соответствует духу Python. По мере
приближения к окончательному решению, возможно, имеет смысл разбить
большие файлы на мелкие.
Понятно, что я не призываю бросаться из одной крайности в другую, т. е.
помещать весь код в один гигантский файл. Для отыскания разумной и интуитивно очевидной структуры модулей и пакетов, составляющих большую
программу, нередко приходится потрудиться, но при коллективной работе
это очень важно. Каждый модуль должен обладать внутренней сцепленностью, а местонахождение функций и классов, относящихся к каждой области
функциональности, должно быть как можно более очевидным.

Дополнительные возможности IPython

521

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

Делайте классы дружественными к IPython
В IPython предпринимаются все меры к тому, чтобы вывести на консоль
понятное строковое представление инспектируемых объектов. Для многих
объектов, в частности словарей, списков и кортежей, красивое форматирование обеспечивается за счет встроенного модуля pprint. Однако в классах,
определенных пользователем, порождение строкового представления возлагается на автора. Рассмотрим такой простенький класс:
class Message:
def __init__(self, msg):
self.msg = msg

Вы будете разочарованы тем, как такой класс распечатывается по умолчанию:
In [576]: x = Message('I have a secret')
In [577]: x
Out[577]:

IPython принимает строку, возвращенную магическим методом __repr__
(выполняя предложение output = repr(obj)), и выводит ее на консоль. Но раз
так, то мы можем включить в класс простой метод __repr__, который создает
более полезное представление:
class Message:
def __init__(self, msg):
self.msg = msg
def __repr__(self):
return 'Message: %s' % self.msg
In [579]: x = Message('У меня есть секрет')
In [580]: x
Out[580]: Message: У меня есть секрет

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

522

Еще о системе IPython

1. Изменить цветовую схему.
2. Изменить вид приглашений ввода и вывода или убрать пустую строку,
печатаемую после Out и перед следующим приглашением In.
3. Выполнить список произвольных предложений Python. Это может быть,
например, импорт постоянно используемых модулей или вообще все,
что должно выполняться сразу после запуска IPython.
4. Включить расширения IPython, например магическую функцию %lprun
в модуле line_profiler.
5. Включить расширения Jupyter.
6. Определить собственные магические функции или псевдонимы системных.
Конфигурационные параметры задаются в файле ipython_config.py, находящемся в подкаталоге .ipython/ вашего домашнего каталога. Конфигурирование производится на основе конкретного профиля. При обычном запуске
IPython загружается профиль по умолчанию, который хранится в каталоге profile_default. Следовательно, в моей Linux-системе полный путь к конфигурационному файлу IPython по умолчанию будет таким:
/home/wesm/.ipython/profile_default/ipython_config.py

Для инициализации этого файла в своей системе выполните в терминале
команду
ipython profile create

Не стану останавливаться на технических деталях содержимого этого файла. По счастью, все параметры в нем подробно прокомментированы, так что
оставляю их изучение и изменение читателю. Еще одна полезная возможность – поддержка сразу нескольких профилей. Допустим, имеется альтернативная конфигурация IPython для конкретного приложения или проекта.
Чтобы создать новый профиль, нужно всего лишь ввести такую строку:
ipython profile create secret_project

Затем отредактируйте конфигурационные файлы во вновь созданном каталоге profile_secret_project и запустите IPython следующим образом:
$ ipython ––profile=secret_project
Python 3.5.1 | packaged by conda–forge | (default, May 20 2016, 05:22:56)
Type "copyright", "credits" or "license" for more information.
IPython 5.1.0 –– An enhanced Interactive Python.
?
–> Introduction and overview of IPython's features.
%quickref –> Quick reference.
help
–> Python's own help system.
object? –> Details about 'object', use 'object??' for extra details.
IPython profile: secret_project

Заключение

523

Как всегда, дополнительные сведения о профилях и конфигурировании
можно найти в документации по IPython в Сети.
Для Jupyter конфигурирование устроено несколько иначе, потому что его
блокноты можно использовать не только с Python, но и с другими языками.
Чтобы создать аналогичный конфигурационный файл Jupyter, выполните команду:
jupyter notebook ––generate–config

Она создаст конфигурационный файл по умолчанию в подкаталоге .jupyter/
jupyter_notebook_config.py вашего начального каталога. Вы можете отредактировать его и переименовать, например:
$ mv ~/.jupyter/jupyter_notebook_config.py ~/.jupyter/my_custom_config.py

Тогда при запуске Jupyter добавьте аргумент ––config:
jupyter notebook ––config=~/.jupyter/my_custom_config.py

B.6. Заключение
Когда вы будете прорабатывать примеры кода в этой книге и расширять свои
навыки программирования на Python, рекомендую постоянно интересоваться экосистемами IPython и Jupyter. Эти проекты создавались специально, чтобы повысить продуктивность пользователя, поэтому работать с ними проще,
чем на самом языке Python с его вычислительными библиотеками.
А на сайте nbviewer (https://nbviewer.jupyter.org/) вы найдете много интересных Jupyter-блокнотов.

Предметный указатель
Символы
%alias, магическая функция, 506
%automagic, магическая функция, 49
%a, формат даты, 340
%A, формат даты, 340
%bookmark, магическая
функция, 505, 507
%b, формат даты, 340
%B, формат даты, 340
%cd, магическая функция, 505
!cmd, команда, 505
%cpaste, магическая функция, 47
%C, формат даты и времени, 340
%debug, магическая функция, 507
%dhist, магическая функция, 505
%dirs, магическая функция, 505
%env, магическая функция, 505
%hist, магическая функция, 49, 505
%lprun, магическая функция, 516,
518
%magic, магическая функция, 49
%page, магическая функция, 50
%paste, магическая функция, 49
%pdb, магическая функция, 508
%popd, магическая функция, 505
%prun, магическая функция, 50, 516
%pushd, магическая функция, 505
%pwd, магическая функция, 505
%p, формат времени, 340
%quickref, магическая функция, 49

%reset, магическая функция, 50, 505
*rest, синтаксис, 74
%run, магическая функция, 38, 45, 50
%timeit, магическая функция, 50, 512
%time, магическая функция, 50, 512
%who_ls, магическая функция, 50
%whos, магическая функция, 50
%who, магическая функция, 50
%xdel, магическая функция, 50, 505
%X, формат времени, 340
%x, формат даты, 340
_ (знак подчеркивания), 42, 504
__ (два знака подчеркивания), 504
# (знак решетки), 52
[] (квадратные скобки), 72, 74
\ (обратная косая черта), 61
>>> (приглашение), 37
{} (фигурные скобки), 81

А
Агрегирование, 131
Агрегирование данных, 313
возврат данных
в неиндексированном виде, 319
применение нескольких
функций, 316
Алгоритмы сортировки, 493
Аннотирование в matplotlib, 284
Анонимные функции, 93
Арифметические операции, 166

525

Предметный указатель

восполнение значений, 168
между DataFrame и Series, 170
Атрибуты
в Python, 55
начинающиеся знаком
подчеркивания, 42

Б
База данных федеральной
избирательной комиссии
за 2012 год, 459
распределение суммы
пожертвований по интервалам, 465
статистика пожертвований
по роду занятий и месту
работы, 462
статистика пожертвований
по штатам, 467
Бета-распределение, 139
Библиотеки, 25
matplotlib, 27
NumPy, 25
pandas, 26
SciPy, 28
Бинарные универсальные
функции, 127
Биномиальное распределение, 139
Булево индексирование
массивов, 119
Булевы значения, тип данных, 63
Булевы массивы, 132

В
Векторизация, 113
определение, 127
Векторные строковые функции, 237
Вложенные типы данных, 489
Восполнение отсутствующих
данных, 215, 324
Временные интервалы, 336
Временные метки
определение, 336
преобразование в периоды, 363

Временные ряды
диапазоны дат, 347
класс TimeSeries
выборка, 342
индексирование, 342
неуникальные индексы, 345
передискретизация, 351.
См. Передискретизация
периодов
скользящие оконные функции.
См. Скользящие оконные функции
типы данных, 337
часовые пояса, 354
частоты, 349
неделя месяца, 351
Выбросы, фильтрация, 226
Выравнивание данных, 166
восполнение значений
в арифметических методах, 168
операции между DataFrame
и Series, 170
Вырезание
в массивах, 114
в списках, 78
Выходные переменные, 504

Г
Гамма-распределение, 139
Генераторы, 94
генераторные выражения, 95
модуль itertools, 96
Генерация случайных чисел, 138
Гистограммы, 296
Глобальная блокировка
интерпретатора (GIL), 24
Глобальная область видимости, 90
Графики плотности, 296
Графики ядерной оценки плотности
(KDE), 297
Группировка
groupby, метод. См. groupby, метод
агрегирование данных, 313

526
применение нескольких
функций, 316
база данных федеральной
избирательной комиссии
за 2012 год, 459
распределение суммы
пожертвований
по интервалам, 465
статистика пожертвований
по роду занятий и месту
работы, 462
статистика пожертвований
по штатам, 467
возврат данных
в неиндексированном виде, 319
восполнение отсутствующих
данных, 324
групповое взвешенное
среднее, 328
квантильный анализ, 322
кросс-табуляция, 331
линейная регрессия, 330
метод apply, 319
сводные таблицы, 331
случайная выборка, 326
Групповые ключи, 322

Д
Дата и время
date_parser, аргумент, 193
date_range, функция, 347
dateutil, пакет, 339
диапазоны дат, 347
типы данных, 64, 338
Двоеточие, 51
Двоичные форматы данных, 203
Microsoft Excel, 206
хранение массивов, 135
Двоичный поиск в списке, 77
Динамическая типизация
в Python, 56
Дискретизация, 223

Предметный указатель

З
Завершение по нажатии клавиши
Tab, 42
Закладки на каталоги в IPython, 507
Запись в текстовый файл, 195

И
Иерархическое индексирование
в pandas, 241
сводная статистика
по уровню, 245
столбцы DataFrame, 246
уровни сортировки, 244
изменение формы, 263
Изменение формы
массива, 472
определение, 263
с помощью иерархического
индексирования, 263
Изменяемые объекты, 58
Именованные аргументы, 53, 89
Индексы
в pandas, 177
в классе TimeSeries, 341
массивов, 114
определение, 144
осей, 222
слияние данных, 252
Индикаторные переменные, 229
Интегрированные среды разработки
(IDE), 32
Интроспекция, 44
Исключения
автоматический вход
в отладчик, 49
обработка в Python, 97
История команд, поиск, 47
Итератора протокол, 56, 94

К
Категориальные данные
и библиотека Patsy, 410

527

Предметный указатель

общие сведения, 383
фасетные сетки, 301
Квантильный анализ, 322
Ковариация, 181
Команды. См. также Магические
команды
история в IPython, 503
входные и выходные
переменные, 504
повторное выполнение, 503
отладчика, 510
поиск, 47
Комбинации клавиш в IPython, 47
Комбинирование
перекрывающихся данных, 261
списков, 76
Комментарии в Python, 52
Конкатенация
вдоль оси, 256
массивов, 474
Контейнер однородных
данных, 489
Конференции, 33
Концы интервалов, 370
Координированное универсальное
время (UTC), 354
Корреляция, 181
Кортежи, 71
методы, 74
распаковка, 73
Косвенная сортировка, 492

Л
Лексикографическая сортировка
lexsort, метод, 492
определение, 493
Линеаризация, 473
Линейная алгебра, 136
Линейная регрессия, 330
Линейные графики, 289
Локализация временных рядов, 355
Локальная область видимости, 90
Лямбда-функции, 93, 238, 316

М
Магические команды, 48
Магические методы, 42
Манипулирование данными
манипуляции со строками, 232
векторные строковые
функции, 237
методы, 232
регулярные выражения, 234
преобразование данных, 217
дискретизация, 223
замена значений, 221
индикаторные переменные, 229
переименование индексов
осей, 222
перестановка, 228
с помощью функции или
отображения, 219
устранение дубликатов, 217
фильтрация выбросов, 226
пример базы данных о продуктах
питания, 453
слияние данных, 247
комбинирование
перекрывающихся данных, 261
конкатенация вдоль оси, 256
слияние объектов
DataFrame, 247
Маргиналы, 331
Маркеры, 279
Массивы
where, функция, 129
булево индексирование, 119
булевы, 132
в NumPy, 471
c_, объект, 476
r_, объект, 476
изменение формы, 472
конкатенация, 474
получение и установка
подмножеств, 479
разбиение, 474
размещение в памяти, 474

528
репликация в памяти, 477
сохранение в файле, 498
вырезание, 114
индексы, 114
логические условия как операции
с массивами, 129
операции между, 113
перестановка осей, 123
поиск в отсортированном
массиве, 495
прихотливое индексирование, 121
создание, 108
создание PeriodIndex, 365
сортировка, 133
статистические операции, 131
структурные, 489
вложенные типы данных, 489
достоинства, 490
типы данных, 110
установка элементов с помощью
укладывания, 484
устранение дубликатов, 134
файловый ввод-вывод, 135
хранение массивов
на диске в двоичном
формате, 135
Матрица плана, 405
Методы
булевых массивов, 132
интроспекция объекта, 44
категориальные, 390
кортежа, 74
определение, 53
оптимизированные
для GroupBy, 313
сводные статистики, 183
скрытые, 42
статистические, 131
строковых объектов, 232
сцепление, 399
экземпляра u-функций, 485
Модули, 56
Момент первого пересечения, 140

Предметный указатель

Н
Надпериод, 374
Непрерывная память, 500
Нормализованный набор временных
меток, 349
Нормальное распределение, 139, 142

О
Область видимости, 90
Объектная модель, 52
Олсона база данных, 354
Оси
конкатенация вдоль, 256
метки, 282
переименование индексов, 222
перестановка, 123
укладывание по, 482
Отладчик IPython, 507
Отступы в Python, 51
Отсутствующие данные, 211
восполнение, 215
фильтрация, 213
Очистка экрана, комбинация
клавиш, 47

П
Патчи, 285
Передискретизация, 367
OHLC, 371
периодов, 373
повышающая, 371
Переменные среды, 505
Перестановки, 228
Переформатирование, 35
Периоды, 359
квартальные, 362
определение, 336, 359
передискретизация, 373
преобразование временных
меток, 363
преобразование частоты, 360
создание PeriodIndex
из массивов, 365

529

Предметный указатель

Подграфики, 275
Подпериод, 374
Позиционные аргументы, 53
Понижающая
передискретизация, 367
Посторонний столбец, 308
Поток управления, 66
range, функция, 68
xrange, функция, 68
обработка исключений, 97
предложение if, 66
предложение pass, 68
тернарное выражение, 69
циклы for, 67
циклы while, 68
Представления, 114, 152
Преобразование
между временными метками
и периоды, 363
между строкой и datetime, 338
Преобразование данных, 217
дискретизация, 223
замена значений, 221
индикаторные
переменные, 229
переименование индексов
осей, 222
перестановка, 228
с помощью функции или
отображения, 219
устранение дубликатов, 217
фильтрация выбросов, 226
Прерывание программы, 46, 47
Приведение типов, 64, 111
Прихотливое индексирование, 121,
479
Проецирование файла
на память, 498
Промежуток, 378
Пространства имен, 90
Профили в IPython, 521
Псевдокод, 35
Пустое пространство имен, 45

Р
Рабочий каталог, 505
Разбиение массивов, 474
Разделение-применениеобъединение, 305
Ранжирование данных, 174
Регулярные выражения, 234
Редукция, 179
Репликация массивов, 477

С
Сводные статистики, 179
isin, метод, 184
unique, метод, 184
value_counts, метод, 184
корреляция и ковариация, 181
по уровню, 245
Сводные таблицы
pivot, метод, 268
поворот данных, 263
таблицы сопряженности, 334
Связывание (определение), 53
Сдвиг временного ряда, 351
Системные команды, задание
псевдонимов, 505
Скользящие оконные функции, 374
Скрытые атрибуты, 42
Скрытые методы, 42
Слияние данных, 247
комбинирование
перекрывающихся, 261
конкатенация вдоль оси, 256
по индексу, 252
слияние объектов DataFrame, 247
Словари, 81
возврат переменных среды, 505
группировка с помощью, 311
значения по умолчанию, 83
ключи, 84
словарное включение, 87
создание, 83
Случайное блуждание, 139

530
Смещения во временных рядах, 352
Согласование с индексом, 156
Сортировка
в NumPy, 491
в pandas, 174
массивов, 133
поиск в отсортированном
массиве, 495
списков, 77
уровни, 244
Сортировка на месте, 491
Списки, 74
Списковое включение, 87
вложенное, 88
Среднее с расширяющимся
окном, 376
Ссылки, 53
Статистические операции, 131
Стилизация в matplotlib, 278
Строго типизированные языки, 54
Строки
преобразование в datetime, 338
тип данных, 60, 112
манипуляции, 232
Структурные массивы, 489
вложенные типы данных, 489
достоинства, 490
Структуры данных в pandas, 144
DataFrame, 148
Index, 154
Series, 144

Т
Текстовые файлы
HTML-файлы, 200
lxml, библиотека, 200
XML-файлы, 201
вывод данных, 195
данные в формате JSON, 198
формат с разделителями, 196
чтение порциями, 193
Тернарное выражение, 69
Тип данных NumPy, 111

Предметный указатель

Типы данных
в NumPy, 470
в Python, 59
None, 64
булев, 63
дата и время, 64
приведение, 64
строки, 60
числовые, 59
для массивов, 110
преобразование
между строкой и datetime, 338
Транспонирование массивов, 123

У
Укладывание, 480
определение, 114, 480
по другим осям, 482
установка элементов массива, 484
Унарные универсальные
функции, 126
Унарные функции, 125
Универсальные функции, 125, 485
в pandas, 172
методы экземпляра, 485
Уровни
группировка по, 313
определение, 241
сводная статистика, 245
сортировки, 244
Устойчивая сортировка, 493

Ф
Файловый ввод-вывод
Web API, 207
в Python, 100
двоичные форматы данных, 203
Microsoft Excel, 206
массивов, 135
HDF5, 500
сохранение в двоичном
формате, 135

531

Предметный указатель

файлы, спроецированные
на память, 498
сохранение графиков в файле, 286
текстовые файлы
HTML-файлы, 200
lxml, библиотека, 200
XML-файлы, 201
вывод данных, 195
данные в формате JSON, 198
формат с разделителями, 196
чтение порциями, 193
Фильтрация
в pandas, 161
выбросов, 226
отсутствующих данных, 213
Форма, 469
Функции, 52, 89
анонимные, 93
возврат нескольких значений, 91
как объекты, 91
лямбда, 93
область видимости, 90
пространства имен, 90
чтения в pandas, 188

Х
Хешируемость, 84
Хи-квадрат распределение, 139
Хронометраж программы, 512

Ч
Частичное индексирование, 242
Частоты, 349
неделя месяца, 351
преобразование, 360

Ш
Шаговое представление, 469

Э
Экспоненциально взвешенные
функции, 378

Я
Ядра, 297
Ядро, 27, 39

A
abs, функция, 126
accumulate, метод, 486
add_patch, метод, 286
add_subplot, метод, 275
add, метод, 85, 125, 169, 170
aggfunc, параметр, 333
aggregate, метод, 314, 316
all, метод, 132, 486
alpha, аргумент, 290
and, ключевое слово, 63, 67
any, метод, 132, 141, 227
append, метод, 75, 156
apply, метод, 173, 186, 319, 325, 447
arange, функция, 110
arccosh, функция, 127
arccos, функция, 127
arcsinh, функция, 127
arcsin, функция, 127
arctanh, функция, 127
arctan, функция, 127
argmax, метод, 132, 181
argmin, метод, 132, 181
arrow, функция, 284
asarray, функция, 110
asfreq, метод, 360, 373
astype, метод, 111
average, способ, 177
AxesSubplot, объект, 276
axis, аргумент, 261
axis, метод, 180
ax, аргумент, 290
a, режим открытия файла, 101

B
bbox_inches, параметр, 287
bisect, модуль, 77
Bokeh библиотека, 303

532
break, ключевое слово, 67
b, режим открытия файла, 101

C
calendar, модуль, 337
Categorical, объект, 224, 323, 383
cat, команда Unix, 189
cat, метод, 239
ceil, функция, 126
center, метод, 240
chunksize, аргумент, 193
clock, функция, 512
close, метод, 103
collections, модуль, 84
cols, параметр, 333
column_stack, функция, 476
combinations, функция, 97
combine_first, метод, 247, 262
comment, аргумент, 193
compile, метод, 235
complex64, тип данных, 111
complex128, тип данных, 111
complex256, тип данных, 111
concatenate, функция, 474, 476
concat, функция, 247, 256, 257, 321,
441
contains, метод, 239
continue, ключевое слово, 67
convention, аргумент, 369
copysign, функция, 127
copy, аргумент, 252
copy, метод, 152
corrwith, метод, 183
corr, метод, 182
cosh, функция, 127
cos, функция, 127
Counter, класс, 425
count, метод, 74, 181, 233, 239, 314
cov, метод, 182
crosstab, функция, 334
cummax, метод, 181
cummшт, метод, 181
cumprod, метод, 181

Предметный указатель

cumsum, метод, 181
cut, функция, 224, 225, 322, 465
c_, объект, 476

D
DataFrame, структура данных, 144,
148, 426, 433
операции между DataFrame
и Series, 170
слияние, 247
dayfirst, аргумент, 193
debug, функция, 511
def, ключевое слово, 89
delete, метод, 156
del, ключевое слово, 82, 152, 505
describe, метод, 181, 321
det, функция, 137
diag, функция, 137
difference, метод, 86
diff, метод, 156, 181
divide, функция, 127
div, метод, 170
dmatrices функция, 405
dot, функция, 136, 137
doublequote, параметр, 198
dpi, параметр, 287
dreload, функция, 519
drop_duplicates, метод, 218
drop, метод, 156, 159
dsplit, функция, 476
dstack, функция, 476
dumps, функция, 199
duplicated, метод, 218

E
edgecolor, параметр, 287
eig, функция, 137
empty, функция, 110
encoding, аргумент, 193
endswith, метод, 234, 239
enumerate, функция, 79
equal, функция, 127

533

Предметный указатель

escapechar, параметр, 198
ExcelFile, класс, 206
except, блок, 97
exec, ключевое слово, 505
exit, команда, 37
exp, функция, 126
extend, метод, 76
eye, функция, 110

F
fabs, функция, 126
facecolor, параметр, 287
figsize, аргумент, 291
Figure, объект, 275, 278
fill_method, аргумент, 368
fillna, метод, 215, 221, 324, 372, 427
fill_value, параметр, 333
findall, метод, 235, 237, 239
finditer, метод, 237
find, метод, 233
first, способ, 177
float16, тип данных, 111
float32, тип данных, 111
float64, тип данных, 111
float128, тип данных, 111
float, тип данных, 59, 111, 470
float, функция, 97
floor, функция, 126
flush, метод, 103
fmax, функция, 127
fmin функция, 127
fname, параметр, 287
format, параметр, 287
for, циклы, 67, 88, 113
frompyfunc, метод, 488
full_like функция, 110
full функция, 110
functools, модуль, 94

G
getattr, функция, 56
get_chunk, метод, 195

get_dummies, метод, 229, 232
get_value, метод, 165
get_xlim, метод, 281
get, метод, 83, 239
greater_equal, функция, 127
greater, функция, 127
grid, аргумент, 290
groupby, метод, 96, 305, 346, 447, 496
группировка по столбцу, 310
группировка с помощью
функций, 312
обход групп, 308
с помощью словарей, 311

H
hasattr, функция, 56
HDF5, формат данных, 500
header, аргумент, 192
'heapsort', алгоритм сортировки, 494
hist, метод, 297
how, аргумент, 252, 371
hsplit, функция, 476
hstack, функция, 476

I
idxmax, метод, 181
idxmin, метод, 181
if, предложение, 66, 83
ignore_index, аргумент, 261
import, директива
в Python, 56
использование в этой книге, 35
imshow, функция, 128
in1d, метод, 135
index_col, аргумент, 193
index, метод, 233, 234
insert метод, 75, 156
insort метод, 77
intersect1d, метод, 134
intersection, метод, 86, 156
int, тип данных, 59, 64, 111
inv, функция, 138

534
IPython
выполнение команд
оболочки, 505
завершение по нажатии клавиши
Tab, 42
закладки на каталоги, 507
интеграция с matplotlib, 50
интроспекция, 43
история команд, 503
команда %run, 45
комбинации клавиш, 47
краткая справка, 49
магические команды, 48
обеспечение дружественности
классов, 521
перезагрузка зависимостей
модуля, 518
профили, 521
советы по проектированию, 519
средства разработки, 507
отладчик, 507
построчное
профилирование, 516
профилирование, 514
хронометраж, 512
ipython_config.py, файл, 522
isdisjoint, метод, 86
isfinite, функция, 127
isinf, функция, 127
isinstance, функция, 55
isin, метод, 156
is_monotonic, метод, 156
isnan, функция, 126
isnull, аргумент, 213
isnull, функция, 146
issubdtype, функция, 470
issubset, метод, 86
issuperset, метод, 86
is_unique, метод, 156
is, ключевое слово, 57
iterator, аргумент, 193
itertools, модуль, 96
iter, функция, 56

Предметный указатель

J
join, метод, 233, 240, 255
JSON (JavaScript Object Notation),
199, 422, 454
jupyter notebook, команда, 40
Jupyter-блокнот
запуск, 39
команда %load, 46
нюансы построения графиков, 276
общие сведения, 28

K
keep_date_col, аргумент, 193
KeyboardInterrupt, исключение, 46
kind, аргумент, 290, 369
kurt, метод, 181

L
label, аргумент, 290, 368, 370
last, метод, 314
left_index, аргумент, 252
left_on, аргумент, 252
left, аргумент, 252
len, метод, 240, 312
less_equal, функция, 127
less, функция, 127
level, метод, 180
level, параметр, 313
limit, аргумент, 369
linalg, модуль, 136
line_profiler, расширение, 516
list, функция, 74
ljust, метод, 234
loads, функция, 423
load, метод, 204
load, функция, 135, 498
loffset, аргумент, 369
log1p, функция, 126
log2, функция, 126
log10, функция, 126
logical_and, функция, 127
logical_not, функция, 127

535

Предметный указатель

logical_or, функция, 127
logical_xor, функция, 127
logy, аргумент, 290
log, функция, 126
lower, метод, 234, 240
lstrip, метод, 234, 240
lstsq, функция, 138
lxml, библиотека, 200

M
mad, метод, 181
map, метод, 93, 174, 238
margins, параметр, 333
match, метод, 235, 240
matplotlib, 27
аннотирование, 284
интеграция с IPython, 50
конфигурирование, 288
метки осей, 282
названия осей, 282
подграфики, 275
пояснительные надписи, 283
риски, 282
сохранение в файле, 286
стили линий, 278
matplotlibrc, файл, 288
maximum, функция, 125, 127
max, метод, 95, 132, 181, 314
max, способ, 177
mean, метод, 131, 181, 306, 313, 314
median, метод, 181, 314
memmap, объект, 498
mergesort', алгоритм сортировки, 494
meshgrid, функция, 127
Microsoft Excel, файлы, 206
min, метод, 95, 132, 181, 314
min, способ, 177
modf, функция, 126
mod, функция, 127
MovieLens 1M, пример набора
данных, 432
mro, метод, 471
multiply, функция, 127

N
names, аргумент, 193, 261
NaN (не число), 132, 146, 212
na_values, аргумент, 193
ncols, параметр, 278
ndarray, 107
булево индексирование, 119
вырезание, 114
индексы, 114
операции между массивами, 113
перестановка осей, 123
прихотливое индексирование, 121
создание массивов, 108
типы данных, 110
транспонирование, 123
None, тип данных, 59, 64
notnull, аргумент, 213
notnull, функция, 146
npy, расширение имени файла, 135
npz, расширение имени файла, 135
nrows, параметр, 193, 278
NumPy, 25
генерация случайных чисел, 138
линейная алгебра, 136
логические условия как операции
с массивами, 129
массивы. См. Массивы в NumPy
массивы ndarray. См. ndarray
методы булевых массивов, 132
обработка данных с применением
массивов, 127
производительность, 500
непрерывная память, 500
случайное блуждание, 139
сообщества и конференции, 33
сортировка, 491
сортировка массивов, 133
статистические операции, 131
структурные массивы, 489
типы данных, 470
укладывание. См. Укладывание
универсальные функции, 125, 485
в pandas, 172

536
методы экземпляра, 485
устранение дубликатов, 134
файловый ввод-вывод
массивов, 135

O
objectify, функция, 200, 201
objs, аргумент, 261
ones, функция, 110
on, аргумент, 252
open, функция, 100
order, метод, 493
or, ключевое слово, 63, 67
outer, метод, 486

P
pad, метод, 240
pandas, 26
drop, метод, 159
reindex, функция, 156
арифметические операции
и выравнивание данных, 166
выборка объектов, 161
иерархическое индексирование.
См. Иерархическое
индексирование в pandas
индексы, 177
обработка отсутствующих
данных, 211
восполнение, 215
фильтрация, 213
построение графиков
гистограммы, 296
графики плотности, 297
диаграммы рассеяния, 299
линейные графики, 289
применение универсальных
функций NumPy, 172
ранжирование, 174
редукция, 179
сводные статистики
isin, метод, 184

Предметный указатель

unique, метод, 184
value_counts, метод, 184
корреляция и ковариация, 181
сортировка, 174
структуры данных. См. Структуры
данных в pandas
фильтрация, 161
parse_dates, аргумент, 193
parse, метод, 339
partial, функция, 94
pass, предложение, 68
path, аргумент, 192
Patsy, библиотека, 405
и категориальные данные, 410
преобразование данных
в формулах, 408
создание описаний моделей, 405
pct_change, метод, 181
pdb, отладчик, 507
percentileofscore, функция, 381
PeriodIndex, индексный объект, 364
PeriodIndex класс, 359
PeriodIndex, класс, 365
period_range, функция, 359
Period, класс, 359
permutations, функция, 97
pickle, формат сериализации, 203
pinv, функция, 138
Plotly, библиотека, 303
plot, метод, 289, 444, 451
pop, метод, 75, 82
pprint, модуль, 521
prod, метод, 314
pydata, группа Google, 33
pystatsmodels, список рассылки, 33
Python
генераторы, 94
генераторные выражения, 95
модуль itertools, 96
достоинства, 23
интегрированные среды
разработки (IDE), 32
интерпретатор, 37

537

Предметный указатель

кортежи, 71
методы, 74
распаковка, 73
множества, 85
множественное включение, 87
необходимые библиотеки.
См. Библиотеки
поток управления. См. Поток
управления
семантика, 51
атрибуты, 55
динамическая типизация, 56
директива импорта, 56
изменяемые объекты, 58
комментарии, 52
методы, 52
объектная модель, 52
операторы, 57
отступы, 51
переменные, 53
ссылки, 53
строго типизированный
язык, 54
функции, 52
словари, 81
словарное включение, 87
списки. См. Списки
списковое включение, 87
типы данных, 59
установка и настройка, 30
Linux, 31
файловый ввод-вывод, 100
функции. См. Функции
функции последовательностей, 79
enumerate, 79
reversed, 81
sorted, 80
zip, 80
pytz, библиотека, 354

Q
qcut, метод, 225, 322
qr, функция, 138

quantile, метод, 181
quicksort', алгоритм сортировки, 494
quotechar, параметр, 198
quoting, параметр, 198

R
randint, функция, 139
randn, функция, 119, 139
rand, функция, 139
range, функция, 68, 110
ravel, метод, 473
rc, метод, 288
read_clipboard, функция, 188
read_csv, функция, 100, 188, 194
read_fwf, функция, 188
readlines, метод, 103
read_table, функция, 188, 191, 196
read, метод, 103
reduceat, метод, 487
reduce, метод, 485
regress, функция, 330
reindex, метод, 156, 372
reload, функция, 519
remove, метод, 76
rename, метод, 223
repeat, метод, 240, 477
replace, метод, 221, 233, 240
reset_index, метод, 247
reshape, метод, 472, 483
return, предложение, 89
reversed, функция, 81
re, модуль, 234
rfind, метод, 234
right_index, аргумент, 252
right_on, аргумент, 252
right, аргумент, 252
rint, функция, 126
rjust, метод, 234
rollback, метод, 353
rollforward, метод, 353
rolling_apply, функция, 381
rolling_corr, функция, 380
rolling, функция, 375, 377

538
rot, аргумент, 290
rows, параметр, 333
rstrip, метод, 234, 240
r_, объект, 476
r, режим открытия файла, 101
r+, режим открытия файла, 101

S
savefig, метод, 286
savez, функция, 135
save, метод, 203
save, функция, 135, 498
scatter_matrix, функция, 300
SciPy, библиотека, 28
seaborn, библиотека, 288
searchsorted, метод, 495
search, метод, 235, 237
seed, функция, 139
seek, метод, 103
Series, структура данных, 144
арифметические операции
с DataFrame, 170
группировка с помощью, 311
setattr, функция, 56
setdefault, метод, 84
setdiff1d, метод, 135
set_index, метод, 246, 269
set_title, метод, 282
set_trace, функция, 511
set_value, метод, 165
set_xlabel, метод, 282
set_xlim, метод, 281
setxor1d, метод, 135
set_xticklabels, метод, 282
set_xticks, метод, 282
set, функция, 85
sharex, параметр, 278, 291
sharey, параметр, 278, 291
shuffle, функция, 139
sign, функция, 126
sinh, функция, 127
sin, функция, 127
size, метод, 308

Предметный указатель

skew, метод, 181
skipinitialspace, параметр, 198
skipna, метод, 180
skiprows, аргумент, 193
slice, метод, 240
solve, функция, 138
sort_columns, аргумент, 291
sorted, функция, 80
sort_index, метод, 174, 245, 493
sortlevel, функция, 245
sort, аргумент, 252
sort, метод, 77, 94, 133, 491
split, метод, 198, 233, 234, 237, 240,
475
split, функция, 476
SQLite, база данных, 209
sqrt, функция, 125, 126
square, функция, 126
squeeze, аргумент, 193
startswith, метод, 234, 239
statsmodels, библиотека
общие сведения, 29, 412
оценивание линейных
моделей, 413
оценивание процессов с
временными рядами, 416
регрессия обычным методом
наименьших квадратов, 330
std, метод, 132, 181, 314
strftime, метод, 338
strip, метод, 234, 240
strptime, метод, 65
style, аргумент, 290
subn, метод, 237
subplot_kw, параметр, 278
subplots_adjust, метод, 278
subplots, метод, 277
sub, метод, 170, 237
suffixes, аргумент, 252
sum, метод, 95, 131, 173, 179, 181,
313, 314
svd, функция, 138
swapaxes, метод, 124
swaplevel, метод, 244

539

Предметный указатель

T

V

take, метод, 228, 479
tanh, функция, 127
tan, функция, 127
tell, метод, 103
TextParser, класс, 193, 195
thousands, аргумент, 193
thresh, аргумент, 214
tile, функция, 478
to_csv, метод, 195, 196
to_datetime, метод, 340
to_period, метод, 363
top, функция, 321, 464
trace, функция, 137
transpose, метод, 123, 124
truncate, метод, 345
try/except, блок, 97
TypeError, исключение, 98, 112
tz_convert, метод, 357
tz_localize, метод, 357

ValueError, исключение, 97
values, метод, 82
values, параметр, 333
var, метод, 132, 181, 314
vectorize, функция, 488
verbose, аргумент, 193
verify_integrity, аргумент, 261
vsplit, функция, 476

U
uint8, тип данных, 111
uint16, тип данных, 111
uint32, тип данных, 111
uint64, тип данных, 111
unicode, тип данных, 111
uniform, функция, 139
union, метод, 86, 135, 156
unique, метод, 134, 156, 184, 460
unstack, метод, 243
upper, метод, 234, 240
use_index, аргумент, 290
U, режим открытия файла, 101

W
where, функция, 129, 261
writelines, метод, 102, 103
writer, метод, 198
write, метод, 102, 103
w, режим открытия файла, 101

X
xlim, аргумент, 290
xrange, функция, 68
xticklabels, метод, 281
xticks, аргумент, 290

Y
yield, ключевое слово, 95
ylim, аргумент, 290
yticks, аргумент, 290

Z
zeros, функция, 110
zip, функция, 80

Книги издательства «ДМК Пресс» можно заказать
в торгово-издательском холдинге «Планета Альянс» наложенным платежом,
выслав открытку или письмо по почтовому адресу:
115487, г. Москва, 2-й Нагатинский пр-д, д. 6А.
При оформлении заказа следует указать адрес (полностью),
по которому должны быть высланы книги;
фамилию, имя и отчество получателя.
Желательно также указать свой телефон и электронный адрес.
Эти книги вы можете заказать и в интернет-магазине: www.a-planeta.ru.
Оптовые закупки: тел. (499) 782-38-89.
Электронный адрес: books@alians-kniga.ru.

Уэс Маккини
Python и анализ данных
Главный редактор

Мовчан Д. А.

dmkpress@gmail.com

Перевод
Корректор
Верстка
Дизайн обложки

Слинкин А. А.
Синяева Г. И., Юрьева В. И.
Чаннова А. А.
Мовчан А. Г.

Формат 70×100 1/16.
Гарнитура PT Serif. Печать офсетная.
Усл. печ. л. 43,88. Тираж 200 экз.
Веб-сайт издательства: www.dmkpress.com

Powered by TCPDF (www.tcpdf.org)