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

Предварительная подготовка данных в Python: Том 1. Инструменты и валидация [Артём Владимирович Груздев] (pdf) читать онлайн

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


 [Настройки текста]  [Cбросить фильтры]
А. В. Груздев

Предварительная
подготовка данных
в Python
Том 1
Инструменты и валидация

Москва, 2023

УДК 004.04Python
ББК 32.372
Г90

Г90

Груздев А. В.
Предварительная подготовка данных в Python: Том 1. Инструменты
и валидация. – М.: ДМК Пресс, 2023. – 816 с.: ил.
ISBN 978-5-93700-156-6
В двухтомнике представлены материалы по применению классических методов машинного обучения в различных промышленных задачах. Первый том
посвящен инструментам Python – основным библиотекам, классам и функциям,
необходимым для предварительной подготовки данных, построения моделей
машинного обучения, выполнения различных стратегий валидации. В конце
первого тома разбираются задачи с собеседований по SQL, Python, математической статистике и теории вероятностей.
Издание рассчитано на специалистов по анализу данных, а также может быть
полезно широкому кругу специалистов, интересующихся машинным обучением.

УДК 004.04Python
ББК 32.372

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

ISBN 978-5-93700-156-6

© Груздев А. В., 2023
© Оформление, издание, перевод, ДМК Пресс, 2023

Моей Матильде, прошедшей
путь бескорыстной любви
длиной в 22 года, посвящается

Оглавление

Введение............................................................................................. 10
ЧАСТЬ 1. НЕМНОГО МАТЕМАТИКИ.......................................... 11
1.1. Функция.......................................................................................................11
1.2. Производная................................................................................................12
1.3. Дифференцирование сложных функций..................................................15
1.4. Частная производная..................................................................................16
1.5. Градиент......................................................................................................17
1.6. Функция потерь и градиентный спуск......................................................18

Часть 2. Инструменты........................................................................ 23
1. Введение......................................................................................... 23
1.1. Структуры данных......................................................................................23
1.1.1. Кортеж (tuple).......................................................................................23
1.1.2. Список (list)..........................................................................................24
1.1.3. Словарь (dictionary)..............................................................................27
1.1.4. Множество (set)....................................................................................31
1.2. Функция.......................................................................................................34
1.3. Полезные встроенные функции.................................................................35
1.3.1. Функция enumerate()...........................................................................35
1.3.2. Функция sorted()..................................................................................36
1.3.3. Функция zip()........................................................................................36
1.4. Класс.............................................................................................................38
1.5. Знакомство с Anaconda...............................................................................43

2. IPython и Jupyter Notebook........................................................... 44
3. NumPy.............................................................................................. 50
3.1. Создание массивов NumPy.........................................................................50
3.2. Обращение к элементам массива..............................................................55
3.3. Получение краткой информации о массиве.............................................57
3.4. Изменение формы массива........................................................................58
3.5. Конкатенация массивов.............................................................................61
3.6. Функции математических операций, знакомство
с правилами транслирования...........................................................................65
3.7. Обработка пропусков..................................................................................70
3.8. Функция np.linspace().................................................................................72
3.9. Функция np.logspace().................................................................................74

Оглавление  5
3.10. Функция np.digitize()................................................................................75
3.11. Функция np.searchsorted()........................................................................76
3.12. Функция np.bincount()..............................................................................78
3.13. Функция np.apply_along_axis()..................................................................79
3.14. Функция np.insert()...................................................................................80
3.15. Функция np.repeat()..................................................................................81
3.16. Функция np.unique().................................................................................82
3.17. Функция np.take_along_axis()....................................................................84
3.18. Функция np.array_split()............................................................................86

4. Библиотеки Numba, datatable, bottleneck
для ускорения вычислений.............................................................. 88
4.1. Numba..........................................................................................................88
4.2. Datatable......................................................................................................94
4.3. Bottleneck.....................................................................................................98

5. SciPy................................................................................................. 99
6. pandas............................................................................................111
6.1. Почему pandas? ........................................................................................111
6.2. Библиотека pandas построена на NumPy................................................111
6.3. pandas работает с табличными данными................................................111
6.4. Объекты DataFrame и Series.....................................................................111
6.5. Задачи, выполняемые pandas..................................................................113
6.6. Кратко о типах данных.............................................................................113
6.7. Представление пропусков........................................................................114
6.8. Какую версию pandas использовать?......................................................115
6.9. Подробно знакомимся с типами данных................................................115
6.9.1. Типы данных для работы с числами
и логическими значениями........................................................................115
6.9.2. Типы данных для работы со строками.............................................126
6.10. Чтение данных........................................................................................136
6.11. Получение общей информации о датафрейме.....................................137
6.12. Изменение настроек вывода с помощью функции get_options()........139
6.13. Знакомство с индексаторами [], loc и iloc.............................................140
6.14. Фильтрация данных................................................................................147
6.14.1. Одно условие....................................................................................147
6.14.2. Несколько условий...........................................................................148
6.14.3. Несколько условий в одном столбце...............................................148
6.14.4. Использование метода .query().......................................................149
6.15. Агрегирование данных...........................................................................151
6.15.1. Группировка и агрегирование с помощью одного столбца..........151
6.15.2. Группировка и агрегирование с помощью нескольких столбцов.153
6.15.3. Группировка с помощью сводных таблиц......................................156
6.16. Анализ частот с помощью таблиц сопряженности...............................166
6.17. Выполнение SQL-запросов в pandas..................................................169

6



Оглавление

7. scikit-learn......................................................................................179
7.1. Основы работы с классами, строящими модели предварительной
подготовки данных и модели машинного обучения....................................179
7.2. Строим свой первый конвейер моделей.................................................198
7.3. Разбираемся с дилеммой смещения–дисперсии и знакомимся
с бутстрепом....................................................................................................210
7.4. Обработка пропусков с помощью классов MissingIndicator
и SimpleImputer...............................................................................................228
7.5. Выполнение дамми-кодирования с помощью класса OneHotEncoder
и функции get_dummies(), знакомство с разреженными матрицами.........235
7.6. Автоматическое построение конвейеров моделей с помощью
класса Pipeline..................................................................................................246
7.7. Знакомство с классом ColumnTransformer...............................................250
7.8. Класс FeatureUnion....................................................................................263
7.9. Выполнение перекрестной проверки с помощью функции
cross_val_score(), получение прогнозов перекрестной проверки
с помощью функции cross_val_predict(), сохранение моделей
перекрестной проверки с помощью функции cross_validate().....................264
7.10. Виды перекрестной проверки для данных формата
«один объект – одно наблюдение» (отсутствует ось времени) ...................273
7.10.1. Обычная нестратифицированная k-блочная перекрестная
проверка с помощью класса KFold.............................................................274
7.10.2. Обычная стратифицированная k-блочная перекрестная проверка
с помощью класса StratifiedKFold...............................................................281
7.10.3. Повторная нестратифицированная k-блочная перекрестная проверка с помощью класса RepeatedKFold....................................................283
7.10.4. Повторная стратифицированная k-блочная перекрестная проверка с помощью класса RepeatedStratifiedKFold...........................................286
7.10.5. k-кратное случайное разбиение на обучающую
и тестовую выборки (перекрестная проверка Монте-Карло)..................288
7.10.6. Перекрестная проверка со случайными перестановками
при разбиении с помощью класса ShuffleSplit..........................................294
7.10.7. Стратифицированная перекрестная проверка
со случайными перестановками при разбиении
с помощью класса StratifiedShuffleSplit....................................................296
7.10.8. Перекрестная проверка с исключением по одному
с помощью класса LeaveOneOut.................................................................297
7.10.9. Перекрестная проверка с исключением p наблюдений
с помощью класса LeavePOut......................................................................299
7.11. Виды перекрестной проверки для данных формата
«один объект – несколько наблюдений» и стратифицированных
данных (отсутствует ось времени) ...............................................................301
7.11.1. Перекрестная проверка, учитывающая группы связанных
наблюдений, с помощью классов GroupKFold ..........................................301
7.11.2. Перекрестная проверка, учитывающая группы связанных
наблюдений с исключением из обучения одной группы, с помощью
класса LeaveOneGroupOut ..........................................................................302

Оглавление  7
7.11.3. Перекрестная проверка, учитывающая группы связанных
наблюдений с исключением из обучения p групп, с помощью
класса LeavePGroupsOut..............................................................................304
7.11.4. Перекрестная проверка, учитывающая группы связанных
наблюдений и распределение классов, с помощью класса
StratifiedGroupKFold....................................................................................305
7.11.5. Перекрестная проверка со случайными перестановками
при разбиении и учитывающая группы связанных наблюдений
с помощью класса GroupShuffleSplit..........................................................307
7.12. Обычный и случайный поиск наилучших гиперпараметров
по сетке с помощью классов GridSearchCV и RandomizedSearchCV.............309
7.12.1. Обычный поиск оптимальных значений гиперпараметров
моделей предварительной подготовки и модели
машинного обучения..................................................................................312
7.12.2. Обычный поиск оптимальных значений гиперпараметров
моделей предварительной подготовки и модели машинного
обучения с добавлением строки прогресса...............................................318
7.12.3. Случайный поиск оптимальных значений
гиперпараметров моделей предварительной подготовки
и модели машинного обучения..................................................................320
7.12.4. Обычный поиск оптимальных значений гиперпараметров
для CatBoost при обработке категориальных признаков «как есть»
(заданы индексы категориальных признаков).........................................321
7.12.5. Отбор оптимальной модели предварительной подготовки
данных в рамках отдельного трансформера.............................................324
7.12.6. Отбор оптимального метода машинного обучения среди
разных методов машинного обучения (перебор значений
гиперпараметров с отдельной предобработкой данных
под каждый метод машинного обучения).................................................329
7.13. Вложенная перекрестная проверка.......................................................335
7.14. Классы PowerTransformer, KBinsDiscretizer и FunctionTransformer......341
7.15. Написание собственных классов предварительной подготовки
для применения в конвейере.........................................................................350
7.16. Модификация классов библиотеки scikit-learn для работы
с датафреймами...............................................................................................375
7.17. Полный цикл построения конвейера моделей в scikit-learn................381
7.17.1. Первая задача....................................................................................381
7.17.2. Вторая задача....................................................................................393
7.18. Калибровка модели.................................................................................404
7.18.1. Актуальность калибровки................................................................404
7.18.2. Функция calibration_curve().............................................................406
7.18.3. Оценка Брайера................................................................................413
7.18.4. Оценка качества калибровки моделей до применения
калибратора.................................................................................................415
7.18.5. Класс CalibratedClassifierCV.............................................................420
7.18.6. Оценка качества калибровки моделей после применения
калибратора.................................................................................................421

8



Оглавление

7.18.7. Оценка качества калибровки моделей после применения
калибратора с уже обученным классификатором.....................................423
7.18.8. Калибровка на основе сплайнов.....................................................426
7.19. Полезные классы CountVectorizer и TfidfVectorizer для работы
с текстом...........................................................................................................436
7.20. Сравнение моделей, полученных в ходе поиска по сетке,
с помощью статистических тестов.................................................................450
7.20.1. Простое сравнение всех построенных моделей.............................451
7.20.2. Сравнение двух моделей: частотный подход.................................454
7.20.3. Сравнение двух моделей: байесовский подход..............................458
7.20.4. Попарное сравнение всех моделей: частотный подход................463
7.20.5. Попарное сравнение всех моделей: байесовский подход.............465
7.20.6. Итоговые выводы.............................................................................467
7.21. Разбиение на обучающую, проверочную и тестовую выборки
с учетом временной структуры для валидации временных рядов ............468
7.22. Виды перекрестной проверки для данных формата
«один объект – одно наблюдение» (присутствует ось времени) ................521
7.22.1. Перекрестная проверка расширяющимся окном..........................525
7.22.2. Перекрестная проверка скользящим окном..................................542
7.22.3. Перерестная проверка расширяющимся/скользящим
окном с гэпом..............................................................................................552
7.23. Перекрестная проверка для данных формата
«один объект – несколько наблюдений» (присутствует ось времени) .......563
7.24. Многоклассовая классификация:
подходы «один против всех», «один против одного» и «коды,
исправляющие ошибки».................................................................................567
7.24.1. Подход «один против остальных» или «один против всех»
(«one versus rest», «one versus all»)..............................................................568
7.24.2. Подход «один против одного» («one versus one»)...........................573
7.24.3. Подход «коды, исправляющие ошибки»
(«error-correcting output codes»)..................................................................592

ЧАСТЬ 3. ДРУГИЕ ПОЛЕЗНЫЕ БИБЛИОТЕКИ......................602
1. Библиотеки визуализации matplotlib, seaborn и plotly.........602
1.1. Matplotlib...................................................................................................602
1.2. Seaborn.......................................................................................................621
1.3. Plotly...........................................................................................................629

2. Библиотека прогнозирования временных рядов ETNA........634
2.1. Общее знакомство....................................................................................634
2.2. Создание объекта TSDataset ....................................................................641
2.3. Визуализация рядов объекта TSDataset..................................................645
2.4. Получение сводки характеристик по объекту TSDataset ......................646
2.5. Модель наивного прогноза......................................................................647
2.6. Модель скользящего среднего.................................................................654

Оглавление  9
2.7. Модель сезонного скользящего среднего................................................658
2.8. Модель SARIMAX.......................................................................................662
2.9. Модель Хольта–Винтерса (модель тройного экспоненциального
сглаживания, модель ETS)...............................................................................671
2.10. Модель Prophet........................................................................................677
2.11. Модель CatBoost......................................................................................689
2.12. Модель линейной регрессии с регуляризацией «эластичная сеть».....709
2.13. Объединение процедуры построения модели, оценки качества
и визуализации прогнозов в одной функции...............................................714
2.14. Перекрестная проверка нескольких моделей.......................................717
2.15. Ансамбли.................................................................................................722
2.16. Стекинг....................................................................................................724
2.17. Создание собственных классов для обучения моделей........................725
2.18. Импутация пропусков............................................................................741
2.19. Работа с трендом и сезонностью...........................................................751
2.20. Обработка выбросов...............................................................................766
2.21. Собираем все вместе...............................................................................772
2.22. Модели нейронных сетей.......................................................................787
2.23. Оптимизация гиперпараметров с помощью
Optuna от разработчиков................................................................................789

Ответы на вопросы с собеседований...........................................794

Введение
Настоящая книга является коллекцией избранных материалов из первого модуля Подписки – обновляемых в режиме реального времени материалов по
применению классических методов машинного обучения в различных промышленных задачах, которые автор делает вместе с коллегами и учениками.
Автор благодарит Игоря Яковлева за предоставленные материалы к первой части, Антона Вахрушева за помощь в подготовке раздела, посвященного
NumPy, во второй части книги, Теда Петру за помощь в подготовке раздела,
посвященного pandas, во второй части книги.
Первая и вторая части книги содержат несложные вопросы с собеседований
по SQL, Python, математической статистике и теории вероятностей. Автором
не ставится задача закрыть пробелы соискателей в этих областях, вопросы
даны как напоминание, что помимо машинного обучения потребуются знания
и в некоторых других сферах. В конце книги вы найдете ответы к вопросам.
В первом томе мы сконцентрируемся на инструментах предварительной
обработки данных и рассмотрим различные способы валидации модели.

Часть 1

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

Рис. 1 График температуры воздуха в течение суток

С помощью этого графика для каждого момента времени t (в часах), где
0 ≤ t ≤ 24, можно найти соответствующую температуру p (в градусах Цельсия).
Например:
 если t = 7, то p = –4;
 если t = 12, то p = 2;
 если t = 17, то p = 3;
 если t = 22, то p = 0.
Здесь t является независимой переменной, а p – зависимой переменной.
В рассмотренном примере каждому значению независимой переменной соответствует единственное значение зависимой переменной. Такую зависимость
одной переменной от другой называют функциональной зависимостью, или
функцией. Например, когда мы пишем y = f(x) (читается «как y, равное f от x»),

12



Немного математики

мы как раз и имеем в виду эту идею зависимости: переменная y зависит от
переменной x по определенному закону (предписанию, правилу). Закон этот
обозначен буквой f.
Независимую переменную иначе называют аргументом, а о зависимой переменной говорят, что она является функцией от этого аргумента. Путь, пройденный автомобилем с постоянной скоростью, является функцией от времени движения. Например, если автомобиль движется с постоянной скоростью
60 км/ч, зависимость пути от времени можно задать формулой s = 60t, где s –
пройденный путь (в километрах), t – время (в часах).
Значения зависимой переменной называют значениями функции. Все значения, которые принимает независимая переменная, образуют область определения функции. Значения функции в точке максимума (минимума) функции
называются, соответственно, максимумом и минимумом функции. Минимальное или максимальное значения функции на заданном множестве называют
экстремумом функции. Минимум и максимум функции может быть локальным
и глобальным.

Рис. 2 Минимум и максимум функции

1.2. Производная
Теперь выясним, что такое производная функции. Для объяснения производной воспользуемся открытыми материалами Игоря Яковлева1.
Представьте, вы едете на автомобиле и спидометр показывает 60 км/ч. Что
это значит? Ответ простой: если автомобиль будет ехать так в течение часа, то
он проедет 60 км.
Допустим, что автомобиль вовсе не собирается ехать так целый час. Например, водитель разгоняет автомобиль с места, давит на газ, в какой-то момент
бросает взгляд на спидометр и видит стрелку на отметке 60 км/ч. В следующий
момент стрелка уползет еще выше. Как же понимать, что в данный момент
времени скорость равна 60 км/ч?
Давайте выясним это на примере. Предположим, что путь s, пройденный
автомобилем, зависит от времени t следующим образом:
s(t) = t2,
1

https://mathus.ru/math/der.pdf.

1.2. Производная  13
где путь измеряется в метрах, а время – в секундах. То есть при t = 0 путь равен
нулю, к моменту времени t = 1 пройденный путь равен s(1) = 1, к моменту времени t = 2 пройденный путь равен s(2) = 4, к моменту времени t = 3 пройденный путь равен s(3) = 9 и так далее.
Видно, что идет разгон – автомобиль набирает скорость с течением времени. Действительно: за первую секунду пройдено расстояние 1; за вторую секунду пройдено расстояние s(2) – s(1) = 4 – 1 = 3; за третью секунду пройдено
расстояние s(3) – s(2) = 9 – 4 =5, и далее по нарастающей.
А теперь вопрос. Пусть, например, через три секунды после начала движения наш водитель взглянул на спидометр. Что покажет стрелка? Иными словами, какова мгновенная скорость автомобиля в момент времени t = 3?
Просто поделить путь на время не получится: привычная формула v = s/t
работает только для равномерного движения (то есть когда стрелка спидометра
застыла в некотором фиксированном положении). Но именно эта формула лежит в основе способа, позволяющего найти мгновенную скорость.
Идея способа такова. Отсчитаем от нашего момента t = 3 небольшой промежуток времени ∆t, найдем путь ∆s, пройденный автомобилем за этот промежуток, и поделим ∆s на ∆t. Чем меньше будет ∆t, тем точнее мы приблизимся
к искомой величине мгновенной скорости.
Давайте посмотрим, как эта идея реализуется. Возьмем для начала ∆t = 1.
Тогда
∆s = s(4) – s(3) = 42 – 32 = 16 – 9 = 7,
и для скорости (измеряется в м/с) получаем:


(1)

Будем уменьшать промежуток ∆t. Берем ∆t = 0,1:
∆s = s(3,1) – s(3) = 3,12 – 32 = 9,61 – 9 = 0,61,
(2)


Теперь берем ∆t = 0,01:
∆s = s(3,01) – s(3) = 3,012 – 32 = 9,0601 – 9 = 0,0601,

(3)


Наконец, возьмем ∆t = 0,001:
∆s = s(3,001) – s(3) = 3,0012 – 32 = 9,006001 – 9 = 0,006001,


(4)

14



Немного математики

Глядя на значения (1)–(4), мы понимаем, что величина ∆s/∆t приближается
к числу 6. Это обозначает, что мгновенная скорость автомобиля в момент времени t = 3 составляет 6 м/с.
Таким образом, при безграничном уменьшении ∆t путь ∆s также стремится
к нулю, но отношение ∆s/∆t стремится к некоторому пределу v, который и называется мгновенной скоростью в данный момент времени t:


(5)

Можно написать и так:


(6)

Давайте вернемся к нашему примеру с s(t) = t2 и проделаем в общем виде те
выкладки, которые выше были выполнены с числами. Итак:
∆s = s(t + ∆t) – s(t) = (t + ∆t)2 – t2 = t2 + 2t∆t + ∆t2 – t2 = ∆t(2t + ∆t),
и для мгновенной скорости имеем:


(7)

В частности, при t = 3 формула (7) дает: v(3) = 2×3 = 6, как и было получено
выше. Скорость бывает не только у автомобиля. Мы можем говорить о скорости изменения чего угодно – например, физической величины или экономического показателя. И производная как раз и служит обобщением понятия
мгновенной скорости на случай абстрактных математических функций.
Рассмотрим функцию y = f(x). Напомним, что x называется аргументом данной функции. Отметим на оси X некоторое значение аргумента x, а на оси Y –
соответствующее значение функции f(x).
Дадим аргументу x некоторое приращение, обозначаемое ∆x. Попадем в точку x + ∆x. Обозначим ее на рисунке вместе с соответствующим значением
функции f(x + ∆x). Величина f(x + ∆x) – f(x) называется приращением функции,
которое отвечает данному приращению аргумента ∆x.
Видите сходство с примером, когда мы вычисляли мгновенную скорость
автомобиля? Приращение аргумента ∆x есть абстрактный аналог промежутка
времени ∆t, а соответствующее приращение функции ∆f – это аналог пути ∆s,
пройденного за время ∆t. Производная – это в точности аналог мгновенной
скорости.
Давайте дадим строгое определение производной. Производная f´(x) функции f(x) в точке x – это предел отношения приращения функции к приращению
аргумента, когда приращение аргумента стремится к нулю:

Сравните с формулами (5) и (6). По сути, написано одно и то же. Можно сказать, что производная – это мгновенная скорость изменения функции.

1.3. Дифференцирование сложных функций  15

Рис. 3 Приращение аргумента и приращение функции

Для производной используются обозначения: f´(x) (читается как «f штрих
от x»), y´ (читается как «y штрих»),

(читается как «dy по dx»).

Функцию, имеющую конечную производную (в некоторой точке), называют
дифференцируемой (в данной точке).

1.3. Дифференцирование сложных функций
Процесс вычисления производной называется дифференцированием. Производ­
ные простых функций можно легко вычислить с помощью формулы (7).
В машинном обучении вам часто придется дифференцировать степенную
функцию – функцию вида f(x) = xa. Формула производной степенной функции
выглядит так:
(xa)´ = axa–1, a ∈.
Производную степенной функции относят к табличным производным, которые нужно знать наизусть.
Еще чаще в машинном обучении нам придется находить производные сложных функций. Дифференцирование сложной функции происходит следующим
образом. Сначала находим производную второй («внешней») функции и затем
умножаем ее на производную первой («внутренней») функции. Эту процедуру
мы еще называем правилом цепочки. Зная небольшое число табличных производных и располагая правилами дифференцирования, можно вычислять
производные огромного количества функций.
Например, нужно вычислить производную функции y = (θ – 5)2.
Функция (x – 5)2 является композицией f(g(x)) двух функций: f(u) = u2 и u =
g(x) = x – 5.

16



Немного математики

Применяем правило цепочки

Применяем правило дифференцирования степенной функции:

.

Производная от суммы (разности) равна сумме (разности) производных
функций:

Применяем правило дифференцирования степенной функции:
другими словами,

Производная константы равна 0:

Таким образом, ((x – 5)2)´ = 2x – 10.

1.4. Частная производная
Встречаются зависимости не только от одной, но и от нескольких переменных.
Например, площадь прямоугольника можно записать как S = xy. Значения S будут определяться совокупностью значений x и y. Это уже будет функция двух
переменных. Объем V прямоугольного параллелепипеда с ребрами x, y и z выражается формулой V = xyz, т. е. значения V зависят от трех переменных. Это
уже будет функция трех переменных.
Обобщением понятия производной на случай функции нескольких переменных будет частная производная.

1.5. Градиент  17
Частная производная – это предел отношения приращения функции по выбранной переменной к приращению этой переменной, при стремлении этого
приращения к нулю.
Возьмем функцию двух переменных f(x, y). Часто бывает важно знать, с какой скоростью меняются значения функции, когда мы перемещаемся по плос­
кости xy.
Частная производная по x от функции f(x, y) обозначается

. Таким образом,

по определению, частной производной по x от функции f(x, y) будет предел отношения частного приращения ∆xf по x к приращению ∆x при стремлении ∆x
к нулю:

Частная производная по y от функции обозначается

. Частной производ-

ной по y от функции f(x, y) будет предел отношения частного приращения ∆yf
по y к приращению ∆y при стремлении ∆y к нулю:

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

мы воспринимаем переменную

, мы считаем x константой. Из этого

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

1.5. Градиент
Градиентом функции многих переменных в данной точке называется вектор,
координаты которого равны частным производным по соответствующим аргументам, вычисленным в данной точке.
Рассмотрим функцию двух переменных f(x, y). Градиентом функции f(x, y)
будет вектор gradf = ∇f =

. Производные

и

вычисляются в каждой

точке (x, y). Таким образом, градиент задан в каждой точке и будет меняться от
точки к точке.
Чем интересен градиент? Он интересен тем, что в данной точке он будет
показывать нам направление наибольшего возрастания функции. Например,
если взять высоту поверхности Земли над уровнем моря, то ее градиент в каж­

18



Немного математики

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

Рис. 4 Визуальная интерпретация градиента. Операция градиента преобразует холм (слева),
если смотреть на него сверху, в поле векторов (справа). Видно, что векторы направлены
«в горку» и чем длиннее, тем круче наклон. Источник: Википедия

1.6. Функция потерь и градиентный спуск
Функция, для которой мы будем искать экстремум, в машинном обучении называется целевой функцией (objective function). Задача по нахождению экстремума функции называется задачей оптимизации. Если речь идет о поиске
минимума, то употребляют термины функция стоимости (cost function),
функция потерь (loss function), функция ошибок (error function).
Если функция дифференцируема, то найти точки, подозрительные на экстремум, можно с помощью необходимого условия экстремума: все частные
производные должны равняться нулю, а значит, вектор градиента – нулевому
вектору. Но не всегда задачу можно решать аналитически. В таком случае используется численная оптимизация. Наиболее простым в реализации из всех
методов численной оптимизации является метод градиентного спуска, тесно
связанный с понятием градиента.
Градиентный спуск – итерационный метод. Основная идея градиентного
спуска состоит в том, чтобы двигаться к минимуму в направлении наиболее
быстрого убывания функции потерь, которое определяется антиградиентом.
В ходе градиентного спуска мы итеративно применяем следующее правило
обновления:
wt = wt–1 – η∇Q(wt–1),
где ∇Q(wt–1) – это градиент функции потерь, которую мы пытаемся минимизировать, а η — размер шага градиентного спуска, называемый темпом обучения,
или скоростью обучения (learning rate).

1.6. Функция потерь и градиентный спуск  19
Мы выбираем каким-либо способом начальную точку, вычисляем в ней
градиент рассматриваемой функции и делаем небольшой шаг в обратном,
антиградиентном направлении. В результате приходим в точку, в которой
значение функции будет меньше первоначального. В новой точке повторяем
процедуру: снова вычисляем градиент функции и делаем шаг в обратном направлении. Продолжая этот процесс, мы будем двигаться в сторону убывания
функции. Можно представить это как движение вниз по холму – сделав шаг
вниз, текущая позиция будет ниже, чем предыдущая. Таким образом, на каждом следующем шаге высота будет как минимум не увеличиваться. Поэтому
этот метод и называется спуском. Важно, чтобы наша функция была выпуклой
и гладкой. Гладкой или непрерывно дифференцируемой функцией называют
функцию, имеющую непрерывную производную на всем множестве определения. Выпук­лой (или выпуклой вниз) функцией называют функцию, для которой отрезок между любыми двумя точками ее графика в векторном пространстве лежит не ниже соответствующей дуги графика. Выпуклость гарантирует
существование лишь одного минимума, а гладкость – существование вектора
градиента в каждой точке.

Рис. 5 Градиентный спуск

Допустим, необходимо минимизировать функцию вида y = (θ – 5)2, т. е. нам
надо найти, при каком значении θ наша функция принимает минимальное
значение. Нужно выполнить следующие действия:
1) необходима производная по θ:

= 2(θ – 5) = 2θ – 10;

2) установим начальное значение θ = 0;
3) установим скорость обучения γ равной 0,2;
4) 20 раз подряд применим формулу θt = θt–1 – γ
вестный параметр, поэтому будет одна формула.
n_iter = 20 # количество итераций
learning_rate = 0.2 # скорость обучения
def func(x):
return (x - 5) ** 2
def func_derivative(x):
return 2 * (x - 5)
previous_x, current_x = 0, 0

. У нас только один неиз-

20



Немного математики

for i in range(n_iter):
current_x = previous_x - learning_rate * func_derivative(previous_x)
previous_x = current_x
print("theta", format(current_x, ".6f"),
"function value=", format(func(current_x), ".6f"),
"derivative=", format(func_derivative(current_x), ".6f"))
theta
theta
theta
theta
theta
theta
theta
theta
theta
theta
theta
theta
theta
theta
theta
theta
theta
theta
theta
theta

2.000000
3.200000
3.920000
4.352000
4.611200
4.766720
4.860032
4.916019
4.949612
4.969767
4.981860
4.989116
4.993470
4.996082
4.997649
4.998589
4.999154
4.999492
4.999695
4.999817

function
function
function
function
function
function
function
function
function
function
function
function
function
function
function
function
function
function
function
function

value=
value=
value=
value=
value=
value=
value=
value=
value=
value=
value=
value=
value=
value=
value=
value=
value=
value=
value=
value=

9.000000
3.240000
1.166400
0.419904
0.151165
0.054420
0.019591
0.007053
0.002539
0.000914
0.000329
0.000118
0.000043
0.000015
0.000006
0.000002
0.000001
0.000000
0.000000
0.000000

derivative=
derivative=
derivative=
derivative=
derivative=
derivative=
derivative=
derivative=
derivative=
derivative=
derivative=
derivative=
derivative=
derivative=
derivative=
derivative=
derivative=
derivative=
derivative=
derivative=

-6.000000
-3.600000
-2.160000
-1.296000
-0.777600
-0.466560
-0.279936
-0.167962
-0.100777
-0.060466
-0.036280
-0.021768
-0.013061
-0.007836
-0.004702
-0.002821
-0.001693
-0.001016
-0.000609
-0.000366

В итоге находим значение параметра θ, при котором функция y = (θ – 5)2
принимает минимальное значение.
Очень важно при использовании метода градиентного спуска правильно
подбирать шаг. Каких-либо конкретных правил подбора шага не существует,
выбор шага – это искусство, но существует несколько полезных закономерностей. Если длина шага слишком мала, то метод будет не спеша, но верно шагать
в сторону минимума. Если же взять размер шага очень большим, появляется
риск, что метод будет перепрыгивать через минимум. Более того, есть риск
того, что градиентный спуск не сойдется.

Рис. 6 Большой и маленький темпы обучения

1.6. Функция потерь и градиентный спуск  21
В методе обычного градиентного спуска градиент вычисляется по всем наблюдениям обучающей выборки, формулу обновления для обычного градиентного спуска еще можно записать так:
wt = wt–1 – η∇Q(wt–1, X),
где X – обучающая выборка.
В этом и состоит основной недостаток метода градиентного спуска – в случае большой выборки даже одна итерация метода градиентного спуска будет
осуществляться долго.
В методе стохастического градиентного спуска градиент функции качества
вычисляется только на одном случайно выбранном объекте обучающей выборки:
wt = wt–1 – η∇Q(wt–1, {xi}),
где {xi} – случайно отобранное наблюдение обучающей выборки.
Это позволяет обойти вышеупомянутый недостаток обычного градиентного
спуска.
Показательно посмотреть на графики сходимости градиентного спуска
и стохастического градиентного спуска. В обычном градиентном спуске на
каждом шаге уменьшается суммарная ошибка на всех элементах обучающей
выборки. График в таком случае обычно получается монотонным. В стохастическом градиентном спуске параметры меняются таким образом, чтобы максимально уменьшить ошибку для одного случайно выбранного объекта. Это
приводит к тому, что график выглядит пилообразным, то есть на каждой конкретной итерации полная ошибка может как увеличиваться, так и уменьшаться. Но в итоге с ростом номера итерации значение функции уменьшается.

Рис. 7 Зависимость функции потерь от номера итерации в обычном градиентном спуске
и стохастическом градиентном спуске

22



Немного математики

Рис. 8 Процесс обучения в обычном градиентном спуске и стохастическом градиентном
спуске

Задача с собеседования (математика)
1. Идут три охотника на охоту. У одного – 3 стакана крупы, у другого –
5 стаканов крупы, у третьего – 8 патронов. Сварили кашу, и все поели поровну. Третий охотник решил отблагодарить двух и отдать им все патроны. Как
поделить патроны справедливо?

Часть 2

Инструменты
1. ВВЕДЕНИЕ
Python стал одним из самых популярных языков, применяемых в машинном обучении для выполнения научных и коммерческих проектов. На момент написания
книги экосистема Python предлагает широчайшие возможности для предварительной обработки данных и построения моделей машинного обучения.
Давайте вспомним основные структуры данных в Python. Нам понадобятся
библиотека NumPy и модуль collections.
# импортируем библиотеку NumPy
# и модуль collections
import numpy as np
import collections

1.1. Структуры данных
1.1.1. Кортеж (tuple)
Кортеж – это неизменяемая последовательность с упорядоченными элементами. Мы можем создать кортеж, записав последовательность значений через
запятую.
# создаем кортеж
tup = 4, 5, 6
# смотрим кортеж
tup
(4, 5, 6)

Мы можем создать кортеж кортежей.
# создаем кортеж кортежей
nested_tup = (4, 5, 6), (7, 8)
# смотрим кортеж кортежей
nested_tup
((4, 5, 6), (7, 8))

24



Инструменты

Любую последовательность или итератор можно преобразовать в кортеж
с помощью функции tuple().
# получаем кортеж
tup = tuple([3, 5, 2])
tup
(3, 5, 2)
# создаем кортеж
tup = tuple('кортеж')
tup
('к', 'о', 'р', 'т', 'е', 'ж')

К элементам кортежа, как и к элементам большинства других последовательностей Python, можно обращаться с помощью квадратных скобок []. Нумерация элементов последовательностей в Python начинается с нуля.
# обращаемся к первому элементу кортежа
tup[0]
'к'

1.1.2. Список (list)
Список – это изменяемая последовательность с упорядоченными элементами, т. е. в отличие от кортежей содержимое списков можно модифицировать.
Список определяется с помощью квадратных скобок [] или функции list().
# создаем список
lst = [3, 4, 5]
lst
[3, 4, 5]
# создаем список
lst = list('список')
lst
['с', 'п', 'и', 'с', 'о', 'к']

Для добавления элемента в конец списка служит метод .append().
# создаем список
lst = ['a', 'b', 'c']
# добавляем элемент в конец списка
lst.append('d')
lst
['a', 'b', 'c', 'd']

1.1. Структуры данных  25
Метод .insert() позволяет вставить элемент в указанную позицию списка.
Мы должны указать позицию в списке и собственно сам элемент.
# создаем список
lst = [2, 4, 6, 9]
# вставляем 7 третьим элементом (нумерация с 0)
lst.insert(2, 7)
lst
[2, 4, 7, 6, 9]

Методом, обратным к .insert(), является .pop(), он удаляет из списка элемент,
находившийся в указанной позиции, и возвращает его.
# удаляем третий элемент из списка
lst.pop(2)
7
# смотрим результат
lst
[2, 4, 6, 9]

Метод .remove() находит и удаляет из списка первый элемент с указанным
значением.
# создаем список
lst = ['house', 'pop', 'techno', 'pop', 'trance']
# удаляем первый элемент с указанным значением
lst.remove('pop')
lst
['house', 'techno', 'pop', 'trance']

Для проверки, содержит ли список некоторое значение, используется оператор in.
# проверим, входит ли 'hardcore' в список
'hardcore' in lst
False

Проверка вхождения значения в случае списка занимает гораздо больше
времени, чем в случае словаря или множества, потому что Python должен просматривать список от начала до конца, а это требует линейного времени, тогда
как поиск в других структурах занимает постоянное время.
Операция сложения списков конкатенирует списки (то же самое будет с кортежами).
# складываем списки
['a', 'b'] + ['c', 'd']

# складываем кортежи
('a', 'b') + ('c', 'd')

['a', 'b', 'c', 'd']

('a', 'b', 'c', 'd')

26



Инструменты

Для добавления в конец имеющегося списка нескольких элементов можно
использовать метод .extend().
# создаем список
lst = ['house', 'pop', 'techno', 'pop', 'trance']
# добавляем в конец списка несколько элементов
lst.extend(['hardcore', 'speedcore'])
lst
['house', 'pop', 'techno', 'pop', 'trance', 'hardcore', 'speedcore']

С помощью метода .clear() можно очистить список.
# очищаем список
lst.clear()
lst
[]

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

Метод .count() позволяет вычислить количество элементов с заданным
значением.
# создаем список
lst = ['house', 'pop', 'techno', 'pop', 'trance']
# смотрим количество элементов с заданным значением
lst.count('pop')
2

Часто возникает потребность вычислить абсолютные частоты элементов
списка. Это можно сделать с помощью функции библиотеки NumPy np.unique().
Сначала с ее помощью получаем массив уникальных значений и массив абсолютных частот, с помощью функции zip() «сшиваем» их и с помощью функции
dict() записываем результат в словарь.
# вычисляем абсолютные частоты элементов с помощью np.unique()
# получаем массив уникальных значений и массив
# абсолютных частот с помощью np.unique
unique, counts = np.unique(lst, return_counts=True)
# с помощью zip() "сшиваем" два массива, с помощью
# dict() преобразовываем в словарь
freq = dict(zip(unique, counts))

1.1. Структуры данных  27
freq
{'house': 1, 'pop': 2, 'techno': 1, 'trance': 1}

Кроме того, можно вычислить абсолютные частоты элементов с помощью
класса Counter модуля collections.
# вычисляем абсолютные частоты элементов с помощью
# класса Counter модуля collections
freq = dict(collections.Counter(lst))
freq
{'house': 1, 'pop': 2, 'techno': 1, 'trance': 1}

От абсолютных частот легко перейти к относительным.
# теперь получим относительные частоты
freq = dict(zip(unique, counts * 100 / len(lst)))
freq
{'house': 20.0, 'pop': 40.0, 'techno': 20.0, 'trance': 20.0}

Метод .reverse() разворачивает список.
# создаем список
lst = [2, 4, 6, 8]
# разворачиваем список
lst.reverse()
lst
[8, 6, 4, 2]

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

1.1.3. Словарь (dictionary)
Словарь – одна из самых важных встроенных в Python структур данных. Его
также называют хешем, отображением или ассоциативным массивом. Он представляет собой коллекцию пар ключ-значение переменного размера, в которой и ключ, и значение – объекты Python. Создать словарь можно с помощью
фигурных скобок {}, отделяя ключи от значений двоеточием.

28



Инструменты

Давайте создадим пустой словарь.
# создаем пустой словарь
empty_dict = {}

А теперь создадим словарь, в котором ключами будут имена переменных,
а значениями – значения переменных. Такие словари часто используются
в циклах, например когда по каждой переменной требуется вычислить среднее значение, положить его в словарь (ключом для значения будет соответствующая переменная), а потом извлечь среднее значение для замены пропущенного значения в соответствующей переменной.
# создаем еще один словарь
d = {'age': 25, 'income': 20000}

Словарь также можно создать с помощью функции dict(). Вместо двоеточия
уже используется знак равенства =, названия ключей берутся без кавычек.
# создаем словарь с помощью функции dict()
d = dict(age=25, income=20000)
d
{'age': 25, 'income': 20000}

Словарь можно создать с помощью метода .fromkeys().
# создаем словарь с помощью метода .fromkeys()
d = dict.fromkeys(['a', 'b'], 25)
d
{'a': 25, 'b': 25}

А теперь создадим словарь, в котором ключами будут строковые значения,
а значения будут представлять собой списки. Такие словари используются для
поиска оптимальных значений гиперпараметров моделей предварительной
подготовки и моделей машинного обучения.
# создаем словарь, где значениями являются списки, используется
# для поиска оптимальных значений гиперпараметров
d = {'max_depth': [2, 4, 6], 'max_features': [3, 6, 9]}

Словарь можно создать на основе двух последовательностей, рассматриваемых как ключи и значения.
# создаем список ключей
key_list = ['a', 'b', 'c']
# создаем список значений
value_list = [10, 20, 30]
# создаем словарь на основе двух списков
mapping = {}
for key, value in zip(key_list, value_list):

1.1. Структуры данных  29
mapping[key] = value
# смотрим словарь
mapping
{'a': 10, 'b': 20, 'c': 30}
# или так
dict(zip(key_list, value_list))
{'a': 10, 'b': 20, 'c': 30}

Для доступа к элементам, вставки и присваивания применяется такой же
синтаксис, как в случае списка или кортежа.
# выполняем присваивание значения
d = {'a': 55, 'b': 35}
d['b'] = 30
d
{'a': 55, 'b': 30}
# вставляем новую пару ключ-значение
d['c'] = 10
d
{'a': 55, 'b': 30, 'c': 10}

Проверка наличия ключа в словаре тоже производится, как для кортежа или
списка.
# проверяем наличие ключа 'c' в словаре
'c' in d
True

Методы .keys() и .values() возвращают соответственно ключи и значения.
Хотя точный порядок пар ключ-значение не определен, эти методы возвращают ключи и значения в одном и том же порядке.
# возвращаем ключи с помощью метода .keys()
d.keys()
dict_keys(['a', 'b', 'c'])
# возвращаем значения с помощью метода .values()
d.values()
dict_values([55, 30, 10])

Обратите внимание, что возвращаемые объекты не являются обычными
списками. Речь идет о динамическом представлении элементов словаря. Чтобы получить списки ключей или списки значений, нужно воспользоваться генераторами списков [k for k in d.keys()] или [k for k in d.values()].

30



Инструменты

# получаем список из ключей словаря
[k for k in d.keys()]
['a', 'b', 'c']
# получаем список из значений словаря
[k for k in d.values()]
[55, 30, 10]

Метод .items() возвращает пары ключ-значение.
# возвращаем пары ключ-значение
d.items()
dict_items([('a', 55), ('b', 30), ('c', 10)])

Давайте извлечем первую и последнюю пары.
# извлечем первую пару
list(d.items())[0]
('a', 55)
# извлечем последнюю пару
list(d.items())[-1]
('c', 10)

Два словаря можно объединить в один с помощью метода .update().
# объединяем два словаря в один
d.update({'c': 10, 'd': 12})
d
{'a': 55, 'b': 30, 'c': 10, 'd': 12}

Для удаления элемента по ключу можно использовать и ключевое слово del.
# удаляем элемент по ключу с помощью del
del d['a']
d
{'b': 30, 'c': 10, 'd': 12}

Метод .pop() удаляет элемент по ключу и возвращает соответствующее ключу значение.
# удаляем элемент по ключу с помощью .pop()
print(d.pop('b'))
print(d)
30
{'c': 10, 'd': 12}

1.1. Структуры данных  31
Метод .popitem() удаляет последний элемент словаря и возвращает удаленный ключ и значение.
# удаляем последний элемент с помощью .popitem()
print(d.popitem())
print(d)
{'d', 12}
{'с': 10}

Метод .get() возвращает значение словаря по ключу.
# с помощью .get() возвращаем значение по ключу
d.get('c')
10

1.1.4. Множество (set)
Множество – это неупорядоченный набор уникальных элементов. Множества
не упорядочены, они не хранят ни позицию элемента, ни порядок вставки. Поэтому наборы не поддерживают ни обращение к элементам по индексам, ни
срезы, ни какое-либо другое поведение, присущее последовательностям. Множества создаются в абсолютно случайном порядке каждый раз. Вы можете разместить элементы как вам будет угодно, но они все равно будут расположены
впоследствии в случайном порядке. Во-вторых, множества не могут иметь повторяющихся элементов, поэтому все элементы, которые будут одинаковыми,
не будут выведены повторно.
Их очень удобно использовать, если вы хотите удалить повторяющиеся элементы из списка.
Множество можно создать с помощью функции set() или фигурных скобок {}
(однако создать пустое множество с помощью фигурных скобок нельзя, вместо
пустого множества вы создадите пустой словарь).
# создаем множество с помощью функции set()
a = set([2, 2, 2, 1, 3, 3])
a
{1, 2, 3}
# создаем множество с помощью фигурных скобок {}
a = {2, 2, 2, 1, 3, 3}
a
{1, 2, 3}

С помощью метода .add() можно добавить элемент в множество.
# добавляем элемент в множество с помощью метода .add()
a.add(4)
a
{1, 2, 3, 4}

32



Инструменты

С помощью метода .remove() можно удалить элемент из множества.
# удаляем элемент из множества с помощью метода .remove()
a.remove(1)
a
{2, 3, 4}

Для проверки, содержит ли множество интересующий элемент, используется оператор in.
# проверяем, содержит ли множество элемент 5
5 in a
False

Метод .clear() очищает множество.
# с помощью метода .clear() очищаем множество
a.clear()
a
set()

С помощью метода .union() можно найти все уникальные элементы, входящие либо в первое, либо во второе множества.
# создаем множества a и b
a = set([1, 2, 3, 6])
b = set([4, 6, 2, 8])
# найдем все уникальные элементы,
# входящие либо в a, либо в b
a.union(b)
{1, 2, 3, 4, 6, 8}
# это эквивалентно синтаксису
a | b
{1, 2, 3, 4, 6, 8}

С помощью метода .intersection() можно найти все элементы, входящие и в
первое, и во второе множества.
# находим все элементы, входящие и в a, и в b
a.intersection(b)
{2, 6}
# это эквивалентно синтаксису
a & b
{2, 6}

1.1. Структуры данных  33
С помощью метода .difference() можно найти все элементы, входящие в первое множество, но не входящие во второе.
# находим все элементы, входящие в a, но не входящие в b
a.difference(b)
{1, 3}
# это эквивалентно синтаксису
a - b
{1, 3}

С помощью метода .symmetric_difference() можно найти элементы, входящие
либо в первое множество, либо во второе множество, но не в первое и второе
множества одновременно.
# находим элементы, входящие либо в a, либо в b,
# но не в a и b одновременно
a.symmetric_difference(b)
{1, 3, 4, 8}
# это эквивалентно синтаксису
a ^ b
{1, 3, 4, 8}

С помощью метода .update() можно добавить в первое множество все элементы из второго множества.
# добавляем в множество a все элементы из множества b
a.update(b)
a
{1, 2, 3, 4, 6, 8}

Можно также проверить, является ли множество подмножеством или над­
множеством другого множества.
# создаем множества
a = {1, 2, 3, 4, 5}
b = {1, 2, 3}
# проверяем, является ли множество b
# подмножеством множества a
b.issubset(a)
True
# проверяем, является ли множество a надмножеством множества b
# (мы проверяем, содержит ли множество a в себе множество b)
a.issuperset(b)
True

34



Инструменты

Теперь от структур данных перейдем к функциям и классам.

Задача с собеседования (SQL)
1. Создайте с помощью языка SQL таблицу employees.
id

name

age

department

1

David

22

B

2

Paul

33

B

3

Jeremy

26

B

4

Jack

21

C

5

John

36

A

6

David

45

A

1.2. Функция
Функция в Python – это объект, выполняющий определенное действие. Функции могут принимать параметры, т. е. некоторые значения, передаваемые
функции для того, чтобы она что-либо сделала с ними.
Параметры указываются как имена в скобках при объявлении функции
и разделяются запятыми. Аналогично мы передаем значения при вызове
функции. Обратите внимание на терминологию: имена, указанные в объявлении функции, называются параметрами, тогда как значения, которые вы
передаёте в функцию при её вызове, – аргументами. Обычно функция определяется с помощью инструкции def.
Инструкция return говорит, что нужно вернуть значение.
Например, мы можем написать функцию square(), она принимает параметры
width и height.
# выясним, что является параметрами, а что – аргументами
# здесь имена width и height, указанные в скобках
# при объявлении функции square(), – это параметры
def square(width, height):
return width * height

Когда функция square() вызывается, то ей передаются аргументы. В примере ниже мы задали глобальные переменные w и h и передали в функцию
square().
Однако на самом деле передаются не эти переменные, а их значения. В данном случае числа 10 и 20. Другими словами, мы могли бы писать square(10, 20).
Разницы не было бы.
#
#
w
h

значения, которые передаем при вызове
функции square(), – аргументы
= 10
= 20

1.3. Полезные встроенные функции  35
square(w, h)
200
# а еще можно было так
square(10, 20)
200

Задача с собеседования (математика)
1. Является ли число 3599 простым?

1.3. Полезные встроенные функции
1.3.1. Функция enumerate()
Функция enumerate() применяется для итерируемых коллекций (строки, списки,
словари и др.) и возвращает кортежи, состоящие из двух элементов – индекса
элемента и самого элемента. Она используется для упрощения прохода по коллекциям в цикле, когда кроме самих элементов требуется их индекс. У нее – два
параметра. Параметр iterable задает объект, элементы которого будем перебирать (список, множество, словарь). Параметр start задает начальное значение
индекса. По умолчанию начальное значение равно 0.
# создаем список
a = [10, 20, 30, 40]
# с помощью цикла for и функции enumerate() возвращаем
# кортеж (индекс значения, само значение)
for i in enumerate(a):
print(i)
(0,
(1,
(2,
(3,

10)
20)
30)
40)

Здесь мы с помощью enumerate() перебираем весь список a. При переборе мы
каждый раз получаем кортеж, состоящий из двух элементов: индекс (порядковый номер) элемента в списке и значение элемента.
# а еще можно так
for num, val in enumerate(a):
print(num, val)
0 10
1 20
2 30
3 40

Выполняем аналогичную операцию. В переменной num будет индекс элемента в списке, в переменной val – значение элемента.
Приведем еще один пример.

36



Инструменты

# еще один пример
data = [2, 5, 3, 4, 1, 5]
for num, val in enumerate(data, 1):
print(str(num) + '-е значение равно ' + str(val))
1-е значение равно 2
2-е значение равно 5
3-е значение равно 3
4-е значение равно 4
5-е значение равно 1
6-е значение равно 5

С помощью enumerate() мы перебираем весь список data. При переборе мы
опять получали кортеж, состоящий из двух элементов. В переменной num будет
индекс элемента в списке. В переменной val – значение элемента. В качестве
второго аргумента в функции enumerate() мы использовали цифру 1, чтобы индексы начинались с единицы, а не с нуля.

1.3.2. Функция sorted()
Функция sorted() возвращает новый отсортированный список, построенный из
элементов произвольной последовательности.
# получаем отсортированный массив
sorted([7, 1, 2, 6, 0, 3, 2])
[0, 1, 2, 2, 3, 6, 7]

1.3.3. Функция zip()
Функция zip() «сшивает» (от англ. zip – застегивать на молнию) элементы нескольких списков, кортежей или других последовательностей в пары, создавая
список кортежей.
# создаем два списка
a = [10, 20, 30, 40]
b = ['a', 'b', 'c', 'd', 'e']
# сшиваем элементы списков в кортежи
zipped = list(zip(a, b))
zipped
[(10, 'a'), (20, 'b'), (30, 'c'), (40, 'd')]
# "сшиваем" с помощью цикла for и функции zip()
for i, j in zip(a, b):
print(i, j)
10
20
30
40

a
b
c
d

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

1.3. Полезные встроенные функции  37
zip() применяют в связке с функцией enumerate(), чтобы дополнительно получить индексы.
# "сшиваем" с помощью цикла for,
# функций zip() и enumerate()
for i, (x, y) in enumerate(zip(a, b)):
print(i, x, y)
0
1
2
3

10
20
30
40

a
b
c
d

Мы можем сразу «сшить» два списка и отсортировать элементы с помощью
функций zip() и sorted().
# создаем списки
letters = ['b', 'a', 'd', 'c']
numbers = [2, 4, 3, 1]
# "сшиваем" списки и сортируем элементы
data = sorted(zip(letters, numbers))
data
[('a', 4), ('b', 2), ('c', 1), ('d', 3)]

Функцию zip() можно использовать для выполнения арифметических операций.
# создаем списки – продажи и затраты
total_sales = [52000.00, 51000.00, 48000.00]
prod_cost = [46800.00, 45900.00, 43200.00]
# вычисляем и печатаем значения прибыли
for sales, costs in zip(total_sales, prod_cost):
profit = sales - costs
print(f"Total profit: {profit}")
Total profit: 5200.0
Total profit: 5100.0
Total profit: 4800.0

Если у вас уже есть список кортежей и вы хотите выделить элементы каждого кортежа в отдельные последовательности (то есть хотите «распороть»
имеющуюся последовательность), вы можете воспользоваться функцией zip()
в сочетании с оператором распаковки *.
# создаем список кортежей
pairs = [(1, 'a'), (2, 'b'), (3, 'c'), (4, 'd')]
# "распарываем" на два списка с помощью функции zip()
# и оператора распаковки *
numbers, letters = zip(*pairs)
numbers

38



Инструменты

(1, 2, 3, 4)
letters
('a', 'b', 'c', 'd')

Задача с собеседования (математика)
1. Что больше, log2(3) или log3(5)?

1.4. Класс
Класс – тип, описывающий устройство объектов. Создание класса в Python начинается с инструкции class.
Вот так будет выглядеть минимальный класс.
# создаем класс
class C:
pass

Класс состоит из объявления (инструкция class), имени класса (в нашем случае это имя C, в Python конвенция указывает на то, что название класса должно
начинаться с заглавной буквы и иметь мужской род) и тела класса, которое
содержит атрибуты и методы. Обратите внимание, что в нашем минимальном
классе есть только одна инструкция pass. Она представляет собой пустую операцию (и она ничего не выполняет). Вы можете использовать pass в любом мес­
те, где интерпретатор ожидает встретить настоящую инструкцию. Сейчас мы
не готовы определить все детали класса С, поэтому использовали pass, чтобы
избежать любых синтаксических ошибок, которые появились бы при попытке
создать класс без какого-либо кода в его теле.
Класс может содержать атрибуты. Ниже представлен класс, содержащий
атрибуты color (цвет), width (ширина), height (высота). Атрибут – это переменная,
содержащая определенную характеристику объекта.
# у класса есть атрибуты
class Rectangle:
color = 'green'
width = 100
height = 100

Чтобы воспользоваться возможностями класса, нужно создать объект, и эта
операция называется созданием экземпляра или инстанса класса (инстатированием).
# создаем экземпляр класса
rect1 = Rectangle()

Доступ к атрибуту класса можно получить следующим образом: имя_объекта.

атрибут.

1.4. Класс  39
# получаем доступ к атрибуту
print(rect1.color)
green

Добавим к нашему классу метод. Метод – это функция, находящаяся внутри
класса (или, говорят, «функция, принадлежащая объекту», этим объектом может быть экземпляр класса). Он выполняет определенное действие, которое
чаще всего предполагает доступ к атрибутам созданного объекта. Метод объекта вызываем так: имя_объекта.имя_метода(). Метод часто обозначают так: .имя_метода(), например .fit().
Скобки говорят о том, что мы имеем дело с функцией, а точка говорит о принадлежности функции к определенному объекту.
Например, в наш класс Rectangle можно добавить метод, вычисляющий площадь прямоугольника. Для того чтобы метод в классе знал, с каким объектом
он работает (это нужно для того, чтобы получить доступ к атрибутам: ширине
(width) и высоте (height)), первым аргументом ему следует передать параметр
self, через который он может получить доступ к своим данным. Параметр self –
это ссылка на конкретный экземпляр класса. С помощью него мы указываем,
что нас интересует не ширина и высота вообще, а ширина и высота конкретного объекта – созданного нами экземпляра класса. Параметр – это наш внешний способ обратиться к атрибуту, находящемуся внутри класса. Вспомним,
что параметры указываются как имена в скобках при объявлении функции
и разделяются запятыми. Также вспомним, что функцию мы определяем с помощью инструкции def.
# добавляем в класс метод square
class Rectangle:
color = 'green'
атрибуты width = 100
height = 100
метод def square(self):

return self.width * self.height

Обратите внимание, не все атрибуты могут быть доступными извне или
публичными (public), они бывают защищенными (protected) и обозначаются
через символ нижнего подчеркивания _, они могут быть частными (private)
и обозначаются через двойной символ нижнего подчеркивания __. Методы
также могут быть публичными, защищенными и частными.
Однако, строго говоря, в Python, в отличие от Java, C++, нет механизма, который эффективно ограничивал бы доступ к любой переменной или методу
экземпляра. В Python просто принято соглашение о префиксе имени переменной/метода с одним или двойным символом подчеркивания для эмуляции поведения спецификаторов защищенного и частного доступа. Зачем же
тогда нужны подчеркивания? Культура программирования Python заключается в том, что имена, начинающиеся с подчеркивания, означают «не трогайте данный атрибут/метод, если вы действительно не знаете, какую задачу он
выполняет». Например, если вы видите частный метод __loss, то вы должны

40



Инструменты

понять, что, скорее всего, он нужен для правильной внутренней работы класса,
используется внутри класса, например для того, чтобы вычислять логистическую функцию потерь на основе обновленных вероятностей положительного
класса, получаемых с помощью обновленных весов в ходе градиентного спус­
ка. Поэтому, в отличие от публичного метода .fit(), вам совершенно не нужно
вызывать его отдельно.
Вернемся к нашему классу Rectangle. Давайте поработаем с атрибутами и методами класса.
# создаем экземпляр класса
rect2 = Rectangle()
# взглянем на значение атрибута color
print(rect2.color)
green
# вычислим площадь с помощью метода .square()
print(rect2.square())
10000
# заново создаем экземпляр класса
rect3 = Rectangle()
# зададим новое значение атрибута width
rect3.width = 200
# зададим новое значение атрибута color
rect3.color = 'brown'
# взглянем на значение атрибута color
print(rect3.color)
brown
# заново вычислим площадь с помощью метода .square()
print(rect3.square())
20000

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

1.4. Класс  41

Давайте воспользуемся новым классом.
# заново создаем экземпляр класса
rect4 = Rectangle()
# взглянем на значение атрибута color
print(rect4.color)
green
# заново вычислим площадь
print(rect4.square())
10000
# заново создаем экземпляр класса, передав
# конкретные значения параметров
rect5 = Rectangle('yellow', 23, 34)
# взглянем на значение атрибута color
print(rect5.color)
yellow
# заново вычислим площадь
print(rect5.square())
782

Когда мы хотим, чтобы наш класс использовал функционал родительского
класса, необходимо наследование. В организации наследования участвуют как
минимум два класса: класс-родитель и класс-потомок. При этом возможно
множественное наследование, в этом случае у класса-потомка есть несколько
родителей. Родительский класс помещается в скобки после имени класса.
# создаем родительский класс
class Figure:
def __init__(self, color):
self.color = color
def get_color(self):
return self.color
# создаем дочерний класс
class Rectangle(Figure):
def __init__(self, color, width=100, height=100):
super().__init__(color)
self.width = width
self.height = height

42



Инструменты

def square(self):
return self.width * self.height

В данном случае с помощью функции super() мы вызовем родительский конструктор. В противном случае мы не сможем воспользоваться атрибутом color.
Функция super() используется, когда нужно обратиться к атрибутам и методам
родительского класса.
# заново создаем экземпляр класса,
# задав значение параметра
rect6 = Rectangle('blue')
# получим значение color
print(rect6.get_color())
blue
# вычислим площадь
print(rect6.square())
10000
# заново создаем экземпляр класса,
# задав значения параметров
rect7 = Rectangle('red', 25, 70)
# получим значение color
print(rect7.get_color())
red
# вычислим площадь
print(rect7.square())
1750

Задачи с собеседований (Python)
1. Расскажите об операторах принадлежности in и not in.
2. Расскажите про операторы тождественности is и is not.
3. Дано a = [1, 2, 3] и b = [1, 2, 3]:
 какое значение получим, применив оператор a == b?
 какое значение получим, применив оператор a is b?
Необходимо пояснить причину получения данных значений.
4. Какое значение получим, применив оператор a is b:
 если a = 123 и b = 123?
 если a = 100500 и b = 100500?
Необходимо пояснить причину получения данных значений.
5. Преобразуйте список ['one', 'two', 'three', 'four'] в строку.
6. У нас есть список менеджеров, и по каждому менеджеру известен результат его работы – объем продаж, нужно получить реестр вида:

1.5. Знакомство с Anaconda  43
Petrov 200000
Sidorov 100000
Ivanov 50000

7. Как в Python узнать, в каком каталоге мы сейчас находимся?
8. Создайте список [1, 3, 5, 7, 9] с помощью генератора списков (нужно воспользоваться одной строкой программного кода).
9. Обратите порядок элементов в списке [1, 3, 5, 7, 9].
10. У нас есть список под названием a вида [9, 7, 5, 3, 1], какой элемент вернет программный код a[-2]?
11. У нас есть два списка ['Petrov', 'Sidorov', 'Ivanov'] и [200000, 100000, 50000].
Нужно превратить их в словарь вида {'Petrov': 200000, 'Sidorov': 100000, 'Ivanov':
50000}.
12. Как убрать дубликаты из списка [1, 2, 1, 3, 4, 2]?
13. Как работает отрицательный индекс?
14. Необходимо получить индекс по каждому элементу списка ['Petrov',
'Ivanov', 'Sidorov'] в следующем виде:
(0, 'Petrov')
(1, 'Ivanov')
(2, 'Sidorov')

15. Дан список признаков ['age', 'credit', 'debt'] и список регрессионных коэффициентов [0.4, -0.2, -0.13]. Нужно получить список кортежей вида [('age',
0.4), ('credit', -0.2), ('debt', -0.13)].
16. У нас есть набор данных, записанный в файле alfa_python_test.csv.
a) Для каждого уникального id оставьте только одну строку с самой поздней
датой.
b) Cколько теперь в каждом городе находится уникальных id?
с) Сколько теперь id содержится в каждом множестве городов?

1.5. Знакомство с Anaconda
Для предварительной подготовки данных и построения моделей в Python нам
потребуется ряд библиотек: NumPy, SciPy, matplotlib, pandas, IPython и scikitlearn. Настоятельно рекомендуем воспользоваться дистрибутивом Anaconda,
который уже включает все необходимые библиотеки. Есть версии для Mac OS,
Windows и Linux.
Anaconda Distribution можно загрузить с веб-сайта Continuum Analytics по
адресу https://www.anaconda.com/download/. Веб-сервер определит операционную систему вашего браузера и предоставит вам соответствующий вашей системе файл загрузки.
При открытии этого URL-адреса в вашем браузере вы увидите страницу
примерно следующего вида:

44



Инструменты

Риc. 1 Стартовая страница Anaconda Distribution

Загрузите инсталлятор для Python 3.9. Запустите инсталлятор и установите
Anaconda Distribution.
Теперь, когда у нас установлено все необходимое, давайте перейдем к использованию IPython и Jupyter Notebook.

2. IPYTHON И JUPYTER NOTEBOOK
IPython – это альтернативная оболочка для интерактивной работы с Python.
Она предлагает несколько усовершенствований для REPL, поставляемой по
умолчанию. REPL – это форма организации простой интерактивной среды
программирования в рамках средств интерфейса командной строки (REPL, от
англ. read-eval-print loop – цикл «чтение–вычисление–вывод»), которая поставляется вместе с Python.
Чтобы запустить IPython, просто выполните команду ipython из командной
строки/терминала.

Риc. 2 Командная строка

2. IPython и Jupyter Notebook  45
Командная строка ввода показывает In[1]:. Каждый раз, когда вы будете вводить инструкцию в REPL IPython, число в командной строке будет увеличиваться.
Аналогично вывод для какой-либо конкретной записи будет предваряться
Out[x]:, где x соответствует номеру In[x]:.
Данная нумерация операций ввода и вывода будет важна для примеров, поскольку все примеры будут предваряться In[x]: и Out[x], и таким образом можно
будет отследить последовательность выполнения операций.
Обратите внимание, что эти числа являются строго последовательными.
Если вы запускаете программный код и при вводе возникают ошибки, или вы
вводите дополнительные инструкции, нумерация перестанет быть последовательной (ее можно сбросить, выйдя и перезапустив IPython).
Jupyter Notebook – это веб-оболочка для Ipython (ранее называлась IPython
Notebook). Это веб-приложение с открытым исходным кодом, которое позволяет создавать и обмениваться документами, содержащими живой код, уравнения, визуализацию и разметку.
Первоначально IPython Notebook ограничивался лишь Python в качестве
единственного языка. Jupyter Notebook позволил использовать многие языки
программирования, включая Python, R, Julia, Scala и F#. Если вы хотите глубже
познакомиться с Jupyter Notebook, перейдите на http://jupyter.org/, где вы увидите страницу следующего вида:

Риc. 3 Стартовая страница сайта https://jupyter.org

Jupyter Notebook можно скачать и использовать независимо от Python.
Anaconda устанавливает его по умолчанию. Чтобы запустить Jupyter Notebook,
введите в Anaconda Prompt следующую команду:
jupyter notebook

46



Инструменты

Риc. 4 Ввод команды jupiter notebook

Откроется страница браузера, отображающая домашнюю страницу Jupyter
Notebook (http://localhost:8888/tree). Если щелкнуть по файлу с расширением
.ipynb, откроется страница с тетрадкой (блокнотом).

Риc. 5 Тетрадка Jupiter

Отображаемая тетрадка представляет собой HTML-документ, который был
создан Jupyter и IPython. Он состоит из нескольких ячеек, которые могут быть
одного из трех типов: Сode (активный программный код), Markdown (текст,
поясняющий код, более развернутый, чем комментарий), Raw NBConvert
(пассивный программный код).

2. IPython и Jupyter Notebook  47
Jupyter запускает ядро IPython для каждой тетрадки. Ячейки, содержащие
код Python, выполняются внутри этого ядра, и результаты добавляются в тет­
радку в формате HTML. Двойной щелчок по любой из этой ячеек позволит
отредактировать ее. По завершении редактирования содержимого ячейки
нажмите Shift+Enter, после чего Jupyter/IPython проанализирует содержимое
и отобразит результаты.

Риc. 6 Панель инструментов в Jupiter

Панель инструментов в верхней части браузера предоставляет ряд возможностей по работе с тетрадкой. К ним относятся добавление, удаление и перемещение ячеек вверх и вниз в тетрадке. Также доступны команды для запуска
ячеек, перезапуска ячеек и перезапуска основного ядра IPython.
Чтобы создать новую тетрадку, перейдите в меню File | New Notebook |
Python 3:

Риc. 7 Создание новой тетрадки в Jupiter

Страница новой тетрадки будет создана в новой вкладке браузера. Ее имя по
умолчанию будет Untitled.

Риc. 8 Новая тетрадка в Jupiter

Тетрадка состоит из одной ячейки Code, которая готова к вводу программного кода Python. Введите 1 + 1 в ячейку и нажмите Shift+Enter для выполнения.

Риc. 9 Операции с ячейками в Jupiter

48



Инструменты

Ячейка выполнена, и результат показан как Out[1]:. Jupyter также создал новую ячейку, чтобы вы могли снова ввести код или разметку.
В ячейке Markdown мы можем вводить и форматировать текст.

Риc. 10 Редактирование текста в Jupiter

Полезно знать магические функции Jupiter Notebook. Все magic-функции (их
еще называют magic-командами) начинаются со знака %, если функция применяется к одной строке, и %%, если применяется ко всей ячейке Jupyter.
Чтобы получить представление о времени, которое потребуется для выполнения функции, приведенной выше, мы можем воспользоваться magic-функциями %timeit, %%timeit, %time, %%time, созданными специально для работы с тет­
радками Jupyter. %time однократно запускает строку кода. %timeit выполняет
строку кода несколько раз, а затем выдает среднее значение. %%time однократно
запускает блок кода в ячейке. %%timeit выполняет блок кода в ячейке несколько
раз, а затем выдает среднее значение.
%time sum(range(100))
CPU times: user 4 µs, sys: 0 ns, total: 4 µs
Wall time: 5.96 µs
4950
%timeit sum(range(100))
753 ns ± 22.5 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
# импортируем необходимые библиотеки
import numpy as np
# создаем массивы значений
x = np.random.randn(10000000)
y = np.random.randn(10000000)
%%time

2. IPython и Jupyter Notebook  49
nx = len(x)
result = 0.0
count = 0
for i in range(nx):
result += x[i] - y[i] count += 1
result / count
CPU times: user 3.37 s, sys: 6.55 ms, total: 3.38 s
Wall time: 3.38 s
-0.00017262501625817198
%%timeit
nx = len(x)
result = 0.0
count = 0
for i in range(nx):
result += x[i] - y[i] count += 1
result / count
2.8 s ± 75.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%matplotlib notebook и %matplotlib inline позволяют выводить графики непосредственно в тетрадке.
На экранах с высоким разрешением типа Retina графики в тетрадках Jupiter
по умолчанию выглядят размытыми, поэтому для улучшения резкости используйте %config InlineBackend.figure_format = 'retina' после %matplotlib inline.
# импортируем необходимые библиотеки
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
# загружаем данные
data = pd.read_csv('Data/Visualizations.csv',
encoding='cp1251', sep=';')
# строим состыкованную столбиковую диаграмму
pd.crosstab(data['образование'], data['дефолт']).plot.barh(
stacked=True);

50



Инструменты

Видим, что наш график выглядит размытым.
# включаем режим 'retina', если у вас экран Retina
%config InlineBackend.figure_format = 'retina'
# снова строим состыкованную столбиковую диаграмму
pd.crosstab(data['образование'], data['дефолт']).plot.barh(
stacked=True);

Задача с собеседования (теория вероятности)
1. В урне находится 15 белых, 5 красных и 10 черных шаров. Наугад извлекается 1 шар. Найти вероятность того, что он будет: а) белым; б) красным; в) черным.

3. NumPy
3.1. Создание массивов NumPy
NumPy (произносится как нампай) – это один из основных пакетов для вычислений в Python. Он содержит функциональные возможности для работы
с многомерными массивами и различными математическими функциями.
Основа NumPy – это объект ndarray, n-мерный массив. В Python массив
NumPy – это базовая структура данных. Библиотека scikit-learn, с помощью которой мы будем строить модели, требует, чтобы данные были записаны в виде
массивов NumPy. Датафреймы pandas, с которыми мы познакомимся позднее, также будут внутренне преобразованы библиотекой scikit-learn в массивы
NumPy. Массивы похожи на списки Python, за исключением того, что элементы массива должны иметь одинаковый тип данных, как float и int. С массивами можно проводить числовые операции с большим объемом информации
в разы быстрее и, главное, намного эффективнее, чем со списками. При работе
с массивами NumPy полезно помнить, что индексация элементов начинается
с 0. Массив NumPy можно создать с помощью функции array(). Она имеет общий вид:

3.1. Создание массивов NumPy  51
numpy.array(object, dtype=None, copy=True, order='K', ndmin=0)
Параметр

Предназначение

object

Задает объект, подобный массиву. Список или кортеж, а также любая функция или метод объекта, возвращающие список или кортеж

dtype

Определяет тип данных выходного массива

copy

Задает копирование объекта

order

Определяет, в каком порядке массивы должны храниться в памяти: строчном C-стиле или столбчатом Fortran-стиле. Если объект не является массивом NumPy, то созданный массив будет находиться в памяти в строковом
С-порядке; если указать значение 'F', то будет храниться в столбчатом
Fortran-порядке. Если объект – это массив NumPy, то в зависимости от флага
происходит следующее:
Порядок

нет копирования

copy=True

'K'

сохраняет порядок
исходного массива

сохраняет C-порядок или F-порядок,
либо устанавливает самый близкий по
структуре

'A'

сохраняет порядок
исходного массива

установит F-порядок, если массив по
стилю похож на столбчатый Fortranстиль, в противном случае будет задан
C-порядок

'C'

C-порядок

C-порядок

'F'

F-порядок

F-порядок

По умолчанию задано значение 'K'
ndmin

Определяет минимальное количество измерений результирующего массива

Рис. 11 Порядок хранения массивов в памяти

Импорт всех имен из большого пакета, каким является NumPy (from numpy
import *), считается среди разработчиков на Python дурным тоном, поэтому

52



Инструменты

с помощью np.array() мы указываем, что нас интересует функция array()
библиотеки NumPy, которая у нас импортирована как np. Далее все функции
NumPy мы будем обозначать np.имя_функции.
# импортируем библиотеку numpy
import numpy as np
# создаем массив NumPy
a = np.array([1, 4, 5, 8], float)
– эквивалентно → a = np.array(object=[1, 4, 5, 8],
print("массив NumPy:\n{}".format(a))
dtype=float)
массив NumPy:
[1. 4. 5. 8.]

Первый элемент будет иметь индекс 0

С помощью функции type() убедимся, что перед нами – массив NumPy. Функция type() определяет тип переданного аргумента.
# выясняем тип объекта
type(a)
numpy.ndarray

Поскольку NumPy ориентирован прежде всего на численные расчеты, тип
данных, если он не указан явно, во многих случаях предполагается float64.
float64 – это числовой тип данных в NumPy, который используется для хранения
чисел c плавающей точкой двойной точности. Стандартное значение с плавающей точкой двойной точности (хранящееся во внутреннем представлении
объекта Python типа float) занимает 8 байт, или 64 бита. Поэтому соответствующий тип в NumPy называется float64.
Свойство dtype как раз возвращает тип значений, хранящихся в массиве.
# смотрим тип значений, хранящихся в массиве
a.dtype
dtype('float64')
Таблица 1 Cписок некоторых поддерживаемых NumPy типов данных
Функция

Код типа

Описание

int8, uint8

i1, u1

Знаковое и беззнаковое 8-разрядное (1 байт) целое

int16, uint16

i2, u2

Знаковое и беззнаковое 16-разрядное (2 байта) целое

int32, uint32

i4, u4

Знаковое и беззнаковое 32-разрядное (4 байта) целое

int64, uint64

i8, u8

Знаковое и беззнаковое 64-разрядное (8 байта) целое

float16

f2

С плавающей точкой половинной точности

float32

f4

Стандартный тип с плавающей точкой одинарной точности. Совместим с типом C float

3.1. Создание массивов NumPy  53
Функция

Код типа

Описание

float64

f8 или d

Стандартный тип с плавающей точкой двойной точности.
Совместим с типом C double и с типом Python float

float128

f16

С плавающей точкой расширенной точности

complex64,
complex128,
complex256

c8, c16, c32

Комплексные числа, вещественная и мнимая части которых представлены соответственно типами float32, float64
и float128

bool

?

Булев тип, способный хранить значения True и False

object

O

Тип объекта Python

string_

S

Тип строки фиксированной длины (1 байт на символ).
Например, строка длиной 10 имеет тип S10

unicode_

U

Тип Unicode-строки фиксированной длины (количество
байтов на символ зависит от платформы). Семантика такая
же, как у типа string_ (например, U10)

На рисунке ниже показана иерархия класса dtype в Python.

Рис. 12 Иерархия класса dtype в NumPy

Ранее мы создали массив NumPy с помощью функции np.array(). Но мы можем использовать и другие функции для создания массивов NumPy, например
np.asarray(). Она преобразует входные данные в ndarray, но не копирует, если на
вход уже подан ndarray.
# воспользуемся функцией asarray()
a = np.asarray([1, 4, 5, 8], float)
a

array([1., 4., 5., 8.])

54



Инструменты

Функция np.zeros() генерирует массив, состоящий из одних нулей, форма
массива и тип значений определяются параметрами shape и dtype.
# создаем одномерный массив, состоящий из 5 нулей
Z = np.zeros(5)
Z
array([0., 0., 0., 0., 0.])

Функция np.ones() генерирует массив, состоящий из одних единиц, форма
массива и тип значений определяются параметрами shape и dtype.
# создаем одномерный массив, состоящий из 5 единиц
O = np.ones(5)
O
array([1., 1., 1., 1., 1.])

Функции np.eye() и np.identity() создают единичную квадратную матрицу
N×N (элементы на главной диагонали равны 1, все остальные – 0).
# создаем единичную квадратную матрицу 3×3 (элементы
# на главной диагонали равны 1, все остальные – 0)
E = np.eye(3)
E
array([[1., 0., 0.],
[0., 1., 0.],
[0., 0., 1.]])
# создаем единичную квадратную матрицу 3x3 (элементы
# на главной диагонали равны 1, все остальные – 0)
I = np.identity(3)
I
array([[1., 0., 0.],
[0., 1., 0.],
[0., 0., 1.]])

С помощью метода .fill() мы можем заполнить массив одинаковым значением. Операция выполняется на месте.
# заполняем значения нулями
I.fill(0)
I
array([[0., 0., 0.],
[0., 0., 0.],
[0., 0., 0.]])

Функция np.empty() создает новые массивы, выделяя под них память, но, в отличие от np.ones() и np.zeros(), не инициализирует элементы. Исходное содержимое случайно и зависит от состояния памяти на момент создания массива (то
есть от того мусора, что в ней хранится).

3.2. Обращение к элементам массива  55
# np.empty() не инициализирует элементы
np.empty(2)
array([-5.73021895e-300, 8.04338871e-320])

3.2. Обращение к элементам массива
Вернемся к нашему массиву a.
# вернемся к массиву a
a
array([1., 4., 5., 8.])

К элементам массива можно получить доступ, создавать срезы и обращаться
с ними так же, как со списками.
# смотрим первые 2 элемента
a[:2]
array([1., 4.])
# смотрим элемент с индексом 3
# (индексация с 0)
a[3]
8.0
# присваиваем первому элементу (элементу
# с индексом 0) значение 5.
a[0] = 5.
# смотрим результат
a
array([5., 4., 5., 8.])

Массивы могут быть многомерными. В отличие от списков, доступ к различным осям можно получить с помощью запятых внутри квадратных скобок.
Здесь у нас пример двумерного массива (т. е. матрицы):
# создаем двумерный массив
a = np.array([[1, 2, 3],
[4, 5, 6]],
float)
a
array([[1., 2., 3.],
[4., 5., 6.]])

Давайте обратимся к элементу c индексом 0 по оси строк и оси столбцов.

56



Инструменты

# обратимся к элементу c индексом 0
# по оси строк и оси столбцов
a[0, 0]
1.0

Давайте обратимся к элементу c индексом 0 по оси строк и индексом 1 по
оси столбцов.
# обратимся к элементу c индексом 0
# по оси строк и оси столбцов
a[0, 1]
2.0

Срезы многомерных массивов можно создавать точно так же, как срезы
одномерных массивов. Спецификация среза работает как фильтр для заданного измерения. Одиночный символ : для измерения задает использование
всех элементов в этом измерении. Индекс с запятой перед символом : будет
означать отбор строки с соответствующим индексом. Индекс с запятой после
символа : будет означать отбор столбца с соответствующим индексом. Отрицательный знак перед индексом будет обозначать позицию с конца (–1 будет
обозначать первую с конца, т. е. последнюю строку/столбец).
Давайте обратимся ко второй строке, т. е. строке с индексом 1.
# обращаемся ко второй строке
a[1, :]
array([4., 5., 6.])

Теперь обратимся к третьему столбцу, т. е. столбцу с индексом 2.
# обращаемся к третьему столбцу
a[:, 2]
array([3., 6.])

А теперь сначала отберем последнюю строку и в ней отберем значения по
двум последним столбцам.
# сначала отбираем последнюю строку
# и в ней отбираем значения по двум
# последним столбцам
a[-1:, -2:]
array([[5., 6.]])

3.3. Получение краткой информации о массиве  57
Происходящее «под капотом» можно визуализировать следующим образом.

Подход Python к использованию именованных переменных работает и для
массивов. Метод .copy() используется для создания новой отдельной копии
массива в памяти.
# создаем массив a
a = np.array([1, 2, 3], float)
# присваиваем массиву a имя b
b = a
# создаем c – копию массива a
c = a.copy()
# первому элементу массива a
# присваиваем значение 0
a[0] = 0
# смотрим массив b
b
array([0., 2., 3.])
# смотрим массив c
c
array([1., 2., 3.])

3.3. Получение краткой информации о массиве
Свойство shape возвращает двухэлементный кортеж с количеством строк
и столбцов массива.
# смотрим информацию о количестве
# строк и столбцов в массиве
a.shape
(2, 3)

Свойство size возвращает количество элементов массива.
# смотрим информацию о количестве
# элементов массива
a.size
6

Свойство ndim возвращает размерность массива.

58



Инструменты

# смотрим размерность массива
a.ndim
1

Функция len() возвращает длину первой оси.
# смотрим длину первой оси
len(a)
2

Оператор in используется для проверки на наличие элемента в массиве.
# проверяем, есть ли значение 2 в массиве
2 in a
True
# проверяем, есть ли значение 0 в массиве
0 in a
False

3.4. Изменение формы массива
Можно изменить форму массива с помощью кортежей, задающих новые размерности. В следующем примере мы превратим одномерный массив с 10 элементами в двумерный массив, у которого первая ось будет содержать 5 элементов, а вторая ось – 2 элемента.
# создаем одномерный массив с 10 элементами
a = np.array(range(10), float)
a
array([0., 1., 2., 3., 4., 5., 6., 7., 8., 9.])

Обратите внимание, что здесь мы воспользовались универсальной функцией range(), которая используется для создания списков, содержащих арифметическую прогрессию. Вы ее еще часто увидите в циклах for.
Итак, выполняем преобразование. В этом нам поможет функция np.reshape().
Она принимает два аргумента: массив, который нужно преобразовать в массив нужной формы, и кортеж с двумя целочисленными значениями, определяющий форму (соответственно параметры a и newshape). Если передан кортеж
с одним целочисленным значением, то результатом будет одномерный массив
данной длины. Если значением будет –1, то значение определяется длиной
массива и оставшимися размерностями.
# превратим одномерный массив с 10 элементами
# в двумерный массив, первая ось будет содержать
# 5 элементов, а вторая ось – 2 элемента

3.4. Изменение формы массива  59
a = np.reshape(a, (5, 2))
a


двухэлементный кортеж (количество строк, количество
столбцов)

array([[0.,
[2.,
[4.,
[6.,
[8.,

1.],
3.],
5.],
7.],
9.]])

Теперь выполним обратное преобразование.
# преобразовываем обратно
a = np.reshape(a, 10)
a
array([0., 1., 2., 3., 4., 5., 6., 7., 8., 9.])

Эквивалентом функции np.reshape() будет метод .reshape().
#
#
#
#
a
a

снова превратим одномерный массив с 10 элементами
в двумерный массив, первая ось будет содержать
5 элементов, а вторая ось – 2 элемента,
только уже с помощью метода .reshape()
= a.reshape((5, 2))

array([[0.,
[2.,
[4.,
[6.,
[8.,

1.],
3.],
5.],
7.],
9.]])

Из массивов можно создавать списки с помощью метода .tolist() и функции

list().

# создаем массив
a = np.array([1, 2, 3], float)
# преобразовываем в список
# с помощью метода .tolist()
a.tolist()
[1.0, 2.0, 3.0]
# преобразовываем в список
# с помощью функции list()
list(a)
[1.0, 2.0, 3.0]

Можно преобразовать массив с сырыми данными в бинарную строку (т. е.
в машиночитаемый формат), используя метод .tobytes() (он пришел насме-

60



Инструменты

ну методу .tostring()). Функция np.frombuffer() работает для обратного преобразования. Раньше использовалась функция np.fromstring(), но в ряде случаев
она вела себя непредсказуемо. Эти операции иногда полезны для сохранения
большого объема данных в файлах, которые могут быть считаны в будущем.
#
a
#
s
#
s

создаем массив
= np.array([1, 2, 3], float)
переводим массив в бинарную строку
= a.tobytes()
смотрим результат

b'\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x08@'
# выполняем обратное преобразование
np.frombuffer(s)
array([1., 2., 3.])

Транспонирование массивов также возможно, при этом создается новый
массив с двумя измененными осями.
# создаем массив из 6 элементов,
# 2 строки и 3 столбца
a = np.array(range(6), float).reshape((2, 3))
a
array([[0., 1., 2.],
[3., 4., 5.]])
# выполняем транспонирование,
# получаем 3 строки и 2 столбца
a_t = a.transpose()
# смотрим транспонированный массив
a_t
array([[0., 3.],
[1., 4.],
[2., 5.]])
# можно покороче
a_T = a.T a_T
array([[0., 3.],
[1., 4.],
[2., 5.]])

Многомерный массив можно преобразовать в одномерный с помощью методов .flatten() и .ravel(). Однако между ними есть различия. Метод .ravel() возвращает ссылку/представление исходного массива. Если вы модифицируете выходной массив, то и исходный массив изменяется. Метод .flatten() возвращает
копию исходного массива. Если вы меняете значение выходного массива, исходный массив остается неизменным. Метод .ravel() быстрее, чем метод .flatten().

3.5. Конкатенация массивов  61
# создаем многомерный массив
a = np.array([[1, 2, 3],
[4, 5, 6]],
float)
a
array([[1., 2., 3.],
[4., 5., 6.]])
# преобразовываем многомерный массив в одномерный
a_f = a.flatten()
a_f
array([1., 2., 3., 4., 5., 6.])
# модифицируем выходной массив
a_f[0] = 0
# смотрим выходной и исходный массивы
print(a_f)
print("")
print(a)
[0. 2. 3. 4. 5. 6.]
[[1. 2. 3.]
[4. 5. 6.]]
# преобразовываем многомерный массив в одномерный
a_r = np.ravel(a)
a_r
array([1., 2., 3., 4., 5., 6.])
# модифицируем выходной массив
a_r[0] = 0
# смотрим выходной и исходный массивы
print(a_r)
print("")
print(a)
[0. 2. 3. 4. 5. 6.]
[[0. 2. 3.]
[4. 5. 6.]]

3.5. Конкатенация массивов
Два или больше массивов можно сконкатенировать (соединить) при помощи
функции np.concatenate(). Мы передаем в функцию кортеж конкатенируемых
массивов.
# создаем массивы для конкатенации
a = np.array([1, 2], float)

62



Инструменты

b = np.array([3, 4, 5, 6], float)
c = np.array([7, 8, 9], float)
# выполняем конкатенацию массивов,
# создавая новый массив
d = np.concatenate((a, b, c))
# смотрим получившийся массив
d
array([1., 2., 3., 4., 5., 6., 7., 8., 9.])

Если массив не является одномерным, можно задать ось, по которой будет
происходить конкатенация. По умолчанию (если не задавать значения оси)
конкатенация будет происходить по оси строк.
#
a
b
#
#
c

создаем массивы для конкатенации
= np.array([[1, 2], [3, 4]], float)
= np.array([[5, 6], [7, 8]], float)
выполняем конкатенацию массивов, создавая новый массив
(по умолчанию выполняется конкатенация по оси строк)
= np.concatenate((a,b))

# смотрим получившийся массив
c
array([[1.,
[3.,
[5.,
[7.,


2.],
4.],
6.],
8.]])

Это эквивалентно варианту конкатенации по оси строк, когда мы явно указали axis=0.
#
#
c
#
c

выполняем конкатенацию массивов
по оси строк, указав axis=0
= np.concatenate((a, b), axis=0)
смотрим получившийся массив

array([[1.,
[3.,
[5.,
[7.,

2.],
4.],
6.],
8.]])

Теперь выполним конкатенацию по оси столбцов, указав axis=1.
#
#
d
#
d

выполняем конкатенацию массивов
по оси столбцов, указав axis=1
= np.concatenate((a, b), axis=1)
смотрим получившийся массив

array([[1., 2., 5., 6.],
[3., 4., 7., 8.]])


3.5. Конкатенация массивов  63
Существуют и другие функции, выполняющие конкатенацию.
Функция np.vstack() конкатенирует массивы вертикально (по оси строк). Она
эквивалентна конкатенации вдоль первой оси (оси строк). Одномерные массивы соединяются построчно в двумерные массивы.
#
#
a
b
c
c

выполняем конкатенацию одномерных
массивов с помощью np.vstack()
= np.array([1, 2, 3])
= np.array([2, 3, 4])
= np.vstack((a, b))

array([[1, 2, 3],
[2, 3, 4]])
# конкатенируем двумерные массивы
# с помощью np.vstack()
a = np.array([[1, 2, 3],
[3, 4, 6]])
b = np.array([[2, 3, 4],
[5, 7, 1]])
c = np.vstack((a, b))
c
array([[1,
[3,
[2,
[5,


2,
4,
3,
7,

3],
6],
4],
1]])

Аналогом функции np.vstack() является np.row_stack().
#
#
a
b
c
c

выполняем конкатенацию одномерных массивов
с помощью np.row_stack()
= np.array([1, 2, 3])
= np.array([2, 3, 4])
= np.row_stack((a, b))

array([[1, 2, 3],
[2, 3, 4]])
# конкатенируем двумерные массивы
# с помощью np.row_stack()
a = np.array([[1, 2, 3],
[3, 4, 6]])
b = np.array([[2, 3, 4],
[5, 7, 1]])
c = np.row_stack((a, b))
c
array([[1,
[3,
[2,
[5,

2,
4,
3,
7,

3],
6],
4],
1]])

64



Инструменты

Функция np.hstack() конкатенирует массивы горизонтально (по оси столбцов). Она эквивалентна конкатенации вдоль второй оси (оси столбцов). Одномерные массивы просто соединяются.
#
#
a
b
c
c

выполняем конкатенацию одномерных
массивов с помощью np.hstack()
= np.array([1, 2, 3])
= np.array([2, 3, 4])
= np.hstack((a, b))

array([1, 2, 3, 2, 3, 4])
# конкатенируем двумерные массивы
# с помощью np.hstack()
a = np.array([[1, 2, 3],
[3, 4, 6]])
b = np.array([[2, 3, 4],
[5, 7, 1]])
c = np.hstack((a, b))
c
array([[1, 2, 3, 2, 3, 4],
[3, 4, 6, 5, 7, 1]])


Функция np.column_stack() берет одномерные массивы, превращает их в столбцы двумерного массива и соединяет в двумерный массив. Двумерные массивы
соединяются так же, как при использовании np.hstack().
#
#
a
b
c
c

выполняем конкатенацию одномерных
массивов с помощью np.column_stack()
= np.array([1, 2, 3])
= np.array([2, 3, 4])
= np.column_stack((a, b))

array([[1, 2],
[2, 3],
[3, 4]])
# конкатенируем двумерные массивы
# с помощью np.column_stack()
a = np.array([[1, 2, 3],
[3, 4, 6]])
b = np.array([[2, 3, 4],
[5, 7, 1]])
c = np.column_stack((a, b))
c
array([[1, 2, 3, 2, 3, 4],
[3, 4, 6, 5, 7, 1]])


3.6. Функции математических операций, знакомство с правилами...  65

3.6. Функции математических операций, знакомство
с правилами транслирования
При использовании стандартных математических операций с массивами выполняется поэлементный принцип: арифметические операции, в которых
участвует скаляр, применяются к каждому элементу массива. В свою очередь,
это означает, что массивы должны быть одинакового размера во время сложения, вычитания и тому подобных операций.
# создаем массивы для выполнения арифметических операций
a = np.array([1, 2, 3], float)
b = np.array([5, 2, 6], float)
# выполняем сложение
a + b

# выполняем вычитание
a - b

# выполняем умножение
a * b

array([6., 4., 9.])

array([-4., 0., -3.])

array([ 5., 4., 18.])

# выполняем деление
a / b

# выполняем возведение в степень
b ** a

array([0.2, 1. , 0.5])

array([ 5., 4., 216.])

Для двумерных массивов умножение остается поэлементным и не соответствует умножению матриц. Для этого существуют специальные функции, которые будут рассмотрены позже.
# создаем массивы для выполнения арифметических операций
a = np.array([[1, 2],
[3, 4]],
float)
b = np.array([[2, 0],
[1, 3]],
float)
# выполняем умножение
a * b
array([[ 2., 0.],
[ 3., 12.]])

Однако если массивы не совпадают по размерности (количеству осей), к ним
будет применено укладывание, или транслирование (broadcasting), для выполнения математических операций. Транслирование в библиотеке NumPy следует строгому набору правил, определяющему взаимодействие двух массивов.
Если в каком-либо измерении размеры массивов различаются и ни один не
равен 1, генерируется ошибка.
Если размерности двух массивов отличаются, форма массива с меньшей
размерностью дополняется единицами с ведущей (левой) стороны.
Если форма двух массивов не совпадает в каком-то измерении, массив
с формой, равной 1 в данном измерении, растягивается вплоть до соответствия форме другого массива.

66



Инструменты

Создаем массивы, отличающиеся по размеру: в одном – 3 элемента, в другом – 2 элемента. При этом ни один не равен 1.
#
#
a
b
a

при несоответствии размеров массивов
выбрасываются ошибки
= np.array([1, 2, 3], float)
= np.array([4, 5], float)
+ b

--------------------------------------------------------------------------ValueError
Traceback (most recent call last)
in
2 a = np.array([1, 2, 3], float)
3 b = np.array([4, 5], float)
----> 4 a + b
ValueError: operands could not be broadcast together with shapes (3,) (2,)

Создаем массивы, отличающиеся по размерности: один массив является
двумерным, а второй – одномерным.
# создаем массивы, различающиеся по размерности
a = np.array([[1, 2], [3, 4], [5, 6]], float)
b = np.array([-1, 3], float)
# выполняем
a + b
array([[0.,
[2.,
[4.,

сложение
5.],
7.],
9.]])

К массиву b применяется транслирование, для того чтобы мы могли выполнить математическую операцию. В нашем случае меньший массив b будет использован несколько раз (продублирован) для каждой строки массива a. В итоге одномерный массив b был транслирован в двумерный так, чтобы он соответствовал форме массива a.

Библиотека NumPy предлагает большой набор функций для выполнения стандартных математических операций над массивами (функции np.abs,
np.sign, np.sqrt, np.log, np.log10, np.exp, np.sin, np.cos, np.tan, np.arcsin, np.arccos,
np.arctan, np.sinh, np.cosh, np.tanh, np.arcsinh, np.arccosh и np.arctanh).
# создаем массив
a = np.array([1, 4, 9], float)

3.6. Функции математических операций, знакомство с правилами...  67
# берем квадратный корень
np.sqrt(a)
array([1., 2., 3.])

Функция np.rint() округляет до ближайшего целого числа по общепринятым
правилам, функция np.ceil() – до ближайшего целого, но только всегда в большую сторону, а функция np.floor() – до ближайшего целого, но только всегда
в меньшую сторону.
# создаем массив
a = np.array([1.1, 1.5, 1.9], float)
# функция rint() округляет до ближайшего целого
# числа по общепринятым правилам
np.rint(a)
array([1., 2., 2.])
# функция ceil() округляет до ближайшего целого,
# но только всегда в большую сторону
np.ceil(a)
array([2., 2., 2.])
# функция floor() округляет до ближайшего целого,
# но только всегда в меньшую сторону
np.floor(a)
array([1., 1., 1.])

В NumPy включены две важные математические константы.
# выведем число пи и e – основание натурального логарифма
print(np.pi)
print(np.e)
3.141592653589793
2.718281828459045

Подробнее поговорим о функциях и методах, выполняющих базовые операции над массивами.
# создаем массив
a = np.array([2, 4, 3], float)

Например, мы можем просуммировать или перемножить элементы массива
с помощью .sum()/np.sum() и .prod()/np.prod() соответственно.
# суммируем
print(a.sum())
print(np.sum(a))

68



Инструменты

9.0
9.0
# перемножаем
print(a.prod())
print(np.prod(a))
24.0
24.0

C помощью .mean()/mean(), .std()/np.std() и .var()/np.var() мы можем вычислить
среднее, стандартное отклонение и дисперсию – квадрат стандартного отклонения.
# создаем массив
a = np.array([2, 1, 9], float)
# вычисляем среднее
print(a.mean())
print(np.mean(a))
4.0
4.0
# вычисляем стандартное отклонение
print(a.std())
print(np.std(a))
3.559026084010437
3.559026084010437
# вычисляем дисперсию
print(a.var())
print(np.var(a))
12.666666666666666
12.666666666666666

Мы можем найти минимум и максимум в массиве.
# находим минимум
print(a.min())
print(np.min(a))
1.0
1.0
# находим максимум
print(a.max())
print(np.max(a))
9.0
9.0

3.6. Функции математических операций, знакомство с правилами...  69
Функции np.argmin() и np.argmax() возвращают индекс минимального или максимального элемента.
# находим индекс минимального элемента
np.argmin(a)
1
# находим индекс максимального элемента
np.argmax(a)
2

При работе с многомерными массивами у функции будет дополнительный
параметр axis (по умолчанию задано значение None – вычисляется среднее по
всем элементам массива, как если бы он был плоским массивом). В зависимости от его значения функция выполнит операцию по заданной оси и поместит
результаты выполнения в возвращаемом массиве. Можно сказать, что операции по оси строк – это операции, выполняемые по вертикали, операции по оси
столбцов – это операции, выполняемые по горизонтали.
Посмотрим это на примере функции np.mean().
# создаем двумерный массив
a = np.array([[0, 2],
[3, -1],
[3, 5]],
float)
a
array([[ 0., 2.],
[ 3., -1.],
[ 3., 5.]])
# вычисляем среднее по оси строк (оси 0)
np.mean(a, axis=0)
array([2., 2.])


# вычисляем среднее по оси столбцов (оси 1)
np.mean(a, axis=1)
array([1., 1., 4.])



70



Инструменты

# вычисляем среднее по умолчанию
# эквивалентно np.mean(a, axis=None)
np.mean(a)

2.0

3.7. Обработка пропусков
Теперь мы создадим двумерный массив с пропущенными значениями.
# создаем двумерный массив
# c пропущенными значениями
a = np.array([[np.nan, 2, 4],
[9, 2, np.nan]],
float)
a
array([[nan, 2., 4.],
[9., 2., nan]])

Для вычисления среднего при наличии пропусков можно воспользоваться
функцией np.nanmean(). Она позволяет вычислить среднее, игнорируя пропуски.
# вычисляем среднее по оси строк (оси 0)
np.nanmean(a, axis=0)
array([9., 2., 4.])
# вычисляем среднее по оси столбцов (оси 1)
np.nanmean(a, axis=1)
array([3. , 5.5])
# вычисляем среднее по умолчанию
# эквивалентно np.nanmean(a, axis=None)
np.nanmean(a)

4.25

С помощью функции np.nan_to_num() можно заменить пропуски определенным значением. Параметр nan позволяет задать значение для замены. Сейчас
мы заменим все пропуски в массиве значением 5.
# создаем двумерный массив
# c пропущенными значениями
a = np.array([[np.nan, 2, 4],
[9, 2, np.nan]],
float)
# заменяем все пропуски значением 5
a = np.nan_to_num(a, nan=5)
a

3.7. Обработка пропусков  71
array([[5., 2., 4.],
[9., 2., 5.]])

Сейчас с помощью функции np.nan_to_num() мы заменим пропуски в отдельном столбце – столбце с индексом 0.
# заменяем пропуски в столбце с индексом 0
a = np.array([[np.nan, 2, 4],
[9, 2, np.nan]],
float)
a[:, 0] = np.nan_to_num(a[:, 0], nan=5)
a
array([[ 5., 2., 4.],
[ 9., 2., nan]])

Теперь с помощью функции np.nan_to_num() мы заменим пропуски в отдельной строке – строке с индексом 1.
# заменяем пропуски в строке с индексом 1
a = np.array([[np.nan, 2, 4],
[9, 2, np.nan]],
float)
a[1, :] = np.nan_to_num(a[1, :], nan=5)
a
array([[nan, 2., 4.],
[ 9., 2., 5.]])

Теперь выполним замену пропусков по столбцам, при этом у каждого столбца будет свое заменяющее значение. Мы создадим двумерный массив с пропусками, у нас будет две строки и три столбца. Затем создадим словарь, в котором будет три пары «ключ-значение», ключом будет индекс столбца, а значением – значение, на которое нужно заменить пропуск в данном столбце. Для
итерирования по столбцам часто применяют цикл for в сочетании с функцией
range(), аргументом будет количество столбцов массива (для этого можно взять
второй элемент кортежа, возвращаемого свойством shape, например a.shape[1]).
# создаем массив с пропусками
a = np.array([[np.nan, 2, 4],
[9, np.nan, np.nan]],
float)
a
array([[nan, 2., 4.],
[9., nan, nan]])
# создаем словарь со значениями для замены
enc_dict = {0: 5, 1: 6, 2: 7}
# по каждому столбцу массива заменяем пропуски
# значением из словаря
for col in range(a.shape[1]):

72



Инструменты

a[:, col] = np.nan_to_num(a[:, col],
nan=enc_dict[col])
a
array([[5., 2., 4.],
[9., 6., 7.]])

А сейчас выполним замену пропусков по строкам, у каждой строки будет
свое заменяющее значение. Вновь создадим точно такой же двумерный массив с пропусками, у нас опять будет две строки и три столбца. Затем создадим
словарь, в котором будет две пары «ключ-значение», ключом будет индекс
строки, а значением – значение, на которое нужно заменить пропуск в данной
строке. Для итерирования по столбцам опять применяем цикл for в сочетании
с функцией range(), аргументом уже будет количество строк массива (для этого
можно взять первый элемент кортежа, возвращаемого свойством shape, например a.shape[0]).
# создаем массив с пропусками
a = np.array([[np.nan, 2, 4],
[9, np.nan, np.nan]],
float)
# создаем словарь со значениями для замены
enc_dict = {0: 5, 1: 10}
# по каждой строке массива заменяем пропуски
# значением из словаря
for col in range(a.shape[0]):
a[col, :] = np.nan_to_num(a[col, :],
nan=enc_dict[col])
a
array([[ 5., 2., 4.],
[ 9., 10., 10.]])

Приведем еще ряд полезных функций NumPy.

3.8. Функция np.linspace()
Функция np.linspace() возвращает одномерный массив из указанного количест­
ва элементов (по умолчанию 50), значения которых равномерно распределены
внутри заданного интервала. Она имеет вид:
np.linspace(start, stop, num=50, endpoint=True)

3.8. Функция np.linspace()  73
Параметр

Предназначение

start
(вещественное число)

Задает начальное значение последовательности

stop
(вещественное число)

Задает последнее значение последовательности, если для
парамет­ра endpoint задано значение True. Если endpoint=False,
то данное значение не включается в интервал, при этом значение шага между элементами последовательности изменяется

num
(целое положительное
число, необязательный)

Определяет количество элементов последовательности (по
умолчанию 50)

endpoint
(True или False, необязательный)

Определяет включение последнего значения последовательности (stop) в интервал. Если endpoint=True, то значение stop
включается в интервал и является последним. В противном
случае stop не входит в интервал. По умолчанию endpoint=True

Возвращает
ndarray
(массив Numpy)

Одномерный массив из указанного количества равномерно
распределенных элементов

Сейчас мы создадим одномерный массив из 50 элементов, значения которых равномерно распределены внутри интервала от 0 до 1.
# создаем одномерный массив из 50 элементов,
# значения которых равномерно распределены
# внутри интервала от 0 до 1
np.linspace(start=0, stop=1)
array([0.
,
0.10204082,
0.20408163,
0.30612245,
0.40816327,
0.51020408,
0.6122449 ,
0.71428571,
0.81632653,
0.91836735,

0.02040816,
0.12244898,
0.2244898 ,
0.32653061,
0.42857143,
0.53061224,
0.63265306,
0.73469388,
0.83673469,
0.93877551,

0.04081633,
0.14285714,
0.24489796,
0.34693878,
0.44897959,
0.55102041,
0.65306122,
0.75510204,
0.85714286,
0.95918367,

0.06122449,
0.16326531,
0.26530612,
0.36734694,
0.46938776,
0.57142857,
0.67346939,
0.7755102 ,
0.87755102,
0.97959184,

0.08163265,
0.18367347,
0.28571429,
0.3877551 ,
0.48979592,
0.59183673,
0.69387755,
0.79591837,
0.89795918,
1.
])

Теперь создадим одномерный массив из 6 элементов, значения которых
равномерно распределены внутри интервала от 1 до 6, и посмотрим, как параметр endpoint влияет на результат.
# создаем массив из 6 элементов, значения которых равномерно
# распределены внутри интервала от 1 до 6,
# включаем/отключаем параметр endpoint
print(np.linspace(start=1, stop=6, num=6, endpoint=True))
print(np.linspace(start=1, stop=6, num=6, endpoint=False))
[1. 2. 3. 4. 5. 6.]
[1. 1.83333333 2.66666667 3.5 4.33333333 5.16666667] ← Е
 сли endpoint=False,
элемент не включается

последний

74



Инструменты

3.9. Функция np.logspace()
Функция np.logspace() возвращает одномерный массив из указанного количест­
ва элементов, значения которых равномерно распределены по логарифмической шкале внутри заданного интервала. Она имеет вид:
np.logspace(start, stop, num=50, endpoint=True, base=10.0)
Параметр

Предназначение

start
(вещественное
число)

Задает начальное значение последовательности, которое равно base
** start (base в степени start)

stop
(вещественное
число)

Задает последнее значение последовательности (base ** stop), если
для параметра endpoint задано значение True. Если endpoint=False,
то данное значение не включается в интервал, при этом значение
шага между элементами последовательности изменяется

num
(целое положительное число,
необязательный)

Определяет количество элементов последовательности (по умолчанию 50)

endpoint
(True или False,
необязательный)

Определяет включение последнего значения последовательности
(base ** stop) в интервал. Если endpoint=True, то значение base ** stop
включается в интервал и является последним. В противном случае
base ** stop не входит в интервал. По умолчанию endpoint=True

base
(вещественное
число, необязательный)

Задает основание логарифмической шкалы (по умолчанию base=10)

Возвращает
ndarray
(массив Numpy)

Одномерный массив из указанного количества элементов, значения
которых равномерно распределены по логарифмической шкале

Сейчас мы создадим одномерный массив из 20 элементов, значения которых равномерно распределены по логарифмической шкале внутри интервала,
стартовым значением которого будет 10 в степени –1 (0,1), а последним значением будет 10 в степени 1 (10).
# создадим одномерный массив из 20 элементов, значения которых равномерно
# распределены по логарифмической шкале внутри интервала, стартовым
# значением которого будет 10 в степени –1 (0,1), а последним
# значением будет 10 в степени 1 (10)
np.logspace(start=-1, stop=1, num=20)
array([ 0.1
,
0.33598183,
1.12883789,
3.79269019,

0.1274275 ,
0.42813324,
1.43844989,
4.83293024,

0.16237767,
0.54555948,
1.83298071,
6.15848211,

0.20691381,
0.6951928 ,
2.33572147,
7.8475997 ,

0.26366509,
0.88586679,
2.97635144,
10.
])

3.10. Функция np.digitize()  75
На практике с помощью функций np.linspace() и np.logspace() часто создают
массивы значений гиперпараметров для моделей предварительной подготовки данных и моделей машинного обучения.

3.10. Функция np.digitize()
Функция np.digitize() возвращает индексы числовых интервалов (бинов), в которые входит каждое значение элементов массива. Она имеет вид:
np.digitize(x, bins, right=False)
Параметр

Предназначение

x
Задает входные данные. Многомерные массивы сжимаются до одной
(массив NumPy или оси
объект, подобный
массиву)
bins
Задает одномерный массив, соседние значения которого задают
(массив NumPy или границы полуоткрытых интервалов. Значения должны быть возрасобъект, подобный
тающими
массиву)
right
(True или False,
необязательный)

Определяет, какой край интервала включать в интервал (right=False
включает крайнее левое значение и не включает крайнее правое,
интервал закрыт слева и открыт справа; right=True включает крайнее
правое значение и не включает крайнее левое, интервал закрыт справа и открыт слева; поскольку либо левый, либо правый край интервала
будет открытым, мы поэтому и называем интервал полуоткрытым)

Возвращает
ndarray of ints
(массив Numpy, состоящий из целых
чисел)

Массив индексов интервалов той же формы, что и x

Давайте создадим массив вещественных чисел.
# создаем массив
a = np.array([3, 9, 15], float)
a
array([3., 9., 15.])

Теперь мы создадим список с границами и вернем индекс бина для каждого
вещественного числа массива a, когда right=False и когда right=True.
# создаем список с границами
lst_bin = [3, 9, 15]
# возвращаем индексы бинов, когда right=False
print(np.digitize(x=a, bins=lst_bin, right=False))
# возвращаем индексы бинов, когда right=True
print(np.digitize(x=a, bins=lst_bin, right=True))
[1 2 3]
[0 1 2]

76



Инструменты

Возьмем первый элемент массива со значением 3. right=False включает крайнее левое значение и не включает крайнее правое,
интервал закрыт слева и открыт справа. Круг­
right=False
лая скобка означает, что соответствующий
0 [-inf, 3) включает наименьшее зна- конец не включается (открыт), а квадратная – что включается (закрыт). Мы видим,
чение, но не включает 3
что все интервалы закрыты слева и откры1 [3, 9) включает 3, но не включает 9
2 [9, 15) включает 9, но не включает 15 ты справа (у них квадратная скобка слева и
3 [15, +inf) включает 15, но не включа- круг­лая скобка справа). Интервал с индексом
0 включает элементы со значениями меньше
ет наибольшее значение
3, поэтому элемент со значением 3 попадает
в интервал с индексом 1.
[1 2 3]
Возьмем первый элемент массива со значением 3. right=True не включает крайнее
левое значение, но включает крайнее правое.
Мы видим, что все интервалы открыты слева
right=True
и закрыты справа (у них круглая скобка слева
0 (-inf, 3] не включает наименьшее и квадратная скобка справа). Интервал с индексом 0 не включает наименьшее значение,
значение, но включает 3
но включает 3, поэтому элемент со значени1 (3, 9] не включает 3, но включает 9
2 (9, 15] не включает 9, но включает 15 ем 3 попадает в интервал с индексом 0.
3 (15, +inf] не включает 15, но включает наибольшее значение
[0 1 2]

Функция np.digitize() не раз нам пригодится для выполнения биннинга с
целью улучшения качества линейных моделей.

3.11. Функция np.searchsorted()
Функция np.searchsorted() возвращает индексы, в которые должны быть вставлены указанные элементы, чтобы порядок сортировки был сохранен. Она
имеет вид:
np.searchsorted(arr, v, side='left', sorter=None)
Параметр

Предназначение

arr
(массив NumPy или
объект, подобный
массиву)

Задает одномерный исходный массив. Предполагается, что массив
уже является отсортированным. Данный массив может быть и не отсортирован, но индексы возвращаются именно для отсортированной
версии. Если sorter равен None, он должен быть отсортирован в порядке возрастания, в противном случае sorter должен быть массивом
целочисленных индексов, который сортирует исходный массив arr

3.11. Функция np.searchsorted()  77
Параметр

Предназначение

v
Задает элементы, которые необходимо вставить в массив arr (массив
(массив NumPy или NumPy или объект, подобный массиву)
объект, подобный
массиву)
side
(строка 'left' или
'right', необязательный)

Если задано значение 'left', то возвращаем индекс для вставки элемента слева, если задано значение 'right', то возвращаем индекс для
вставки элемента справа

sorter
Задает опциональный массив целочисленных индексов, который
(массив NumPy или сортирует исходный массив arr. Такой массив может быть получен
объект, подобный
с помощью функции np.argsort()
массиву, необязательный)
Возвращает
int или array of ints
(целое число или
массив Numpy, состоящий из целых
чисел)

Массив индексов, в которые должны быть вставлены указанные элементы, той же формы, что и v, или целое число, если v – это скаляр

Сейчас мы создадим массив целочисленных чисел и найдем индекс, в который должен быть вставлен элемент 13.
# создаем массив
a = np.array([4, 10, 12, 14, 16, 16, 59, 72, 78, 86])
# находим индекс, в который должен быть вставлен элемент 13
np.searchsorted(a, 13)
3

Данная функция вернет индекс для отсортированной версии массива, даже
если он сам не отсортирован (под капотом она выполняет предварительную
сортировку массива и поддерживает работу со значениями nan).
Сейчас мы создадим неотсортированный массив целочисленных чисел
и найдем индекс, в который должен быть вставлен элемент 5.
# создаем массив
a = np.array([9, 3, 7, 8, 3, 7, 9])
# находим индекс, в который должен быть вставлен элемент 5
np.searchsorted(a, 5)
2

78



Инструменты

Функция обладает эквивалентным методом класса ndarray, т. е. вызов

np.searchsorted(a) равносилен вызову метода a.searchsorted().

3.12. Функция np.bincount()
Функция np.bincount() возвращает количество вхождений значений в массиве.
Длина выходного массива равна np.max(x) + 1. Данная функция выполняет подсчет только целых неотрицательных чисел. Если на вход подан массив с числами типа float или complex, то будет вызвано исключение TypeError. Функция
имеет вид:
np.bincount(x, weights=None, minlength=0)
Параметр

Предназначение

x
Задает одномерную последовательность целых неотрицательных
(массив NumPy или чисел. В случае недопустимых значений параметра будет вызвано
объект, подобный
исключение ValueError
массиву)
weights
Задает массив, который имеет ту же длину, что и x, и позволяет на(массив NumPy или значить весовые коэффициенты каждого его элемента
объект, подобный
массиву, необязательный)
minlength
(целое положительное число или
0, необязательный)

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

Возвращает
ndarray of ints
(массив NumPy, состоящий из целых
чисел)

Массив положительных чисел, указывающих на количество вхождений значений его индекса в исходный массив. Длина выходного
массива равна np.amax(x) + 1

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

3.13. Функция np.apply_along_axis()  79
# создаем массив
a = np.array([0, 2, 2, 1, 1, 1])
# смотрим количество вхождений каждого значения
np.bincount(a)
array([1, 3, 2])

Видим, что длина равна 3, поскольку максимальное значение в массиве равно 2 и к нему прибавляем единицу. У нас – одно вхождение значения 0, три
вхождения значения 1, два вхождения значения 2.
Приведем еще пример.
# создаем массив
a = np.array([0, 2, 5, 5])
# смотрим количество вхождений каждого значения
np.bincount(a)
array([1, 0, 1, 0, 0, 2])

Видим, что длина равна 6, поскольку максимальное значение в массиве равно 5 и к нему прибавляем единицу. У нас – одно вхождение значения 0, ноль
вхождений значения 1, одно вхождение значения 2, ноль вхождений значения 3, ноль вхождений значения 4, два вхождения значения 5.

3.13. Функция np.apply_along_axis()
Функция np.apply_along_axis() применяет заданную функцию к 1–D срезу массива вдоль указанной оси.
Давайте создадим массив.
# создаем массив NumPy
a = np.array([[0, 0, 0,
[1, 1, 1,
[0, 1, 1,
[0, 1, 1,

1],
0],
1],
0]])

Теперь с помощью функции np.apply_along_axis() применяем по оси строк
функцию np.bincount() для подсчета количества вхождений значений, а затем
функцию np.argmax() для поиска индекса максимального элемента.
# применяем по оси строк функцию подсчета количества
# вхождений значений, затем функцию поиска индекса
# максимального элемента
np.apply_along_axis(lambda x: np.argmax(np.bincount(x)),
axis=0,
arr=a)
array([0, 1, 1, 0])

80



Инструменты

3.14. Функция np.insert()
Функция np.insert() вставляет указанные элементы перед указанными индексами на указанной оси. Функция имеет вид:
np.insert(arr, obj, values, axis=None)
Параметр

Предназначение

arr
Массив NumPy или любой объект, который может быть преобразован
(массив NumPy или в массив NumPy
объект, подобный
массиву)
obj
(срез, целое число
или последовательность целых
чисел)

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

values
Значение, которое необходимо вставить в массив. При множествен(подобный массиву ной вставке необходимо использовать последовательность значений,
объект)
причем длина values должна соответствовать длине последовательности obj. Тип данных values всегда приводится к типу данных
массива arr
axis
(целое число, не­
обязательный)

Задает ось, вдоль которой нужно вставить значения. Если задано
значение None, то arr сначала становится плоским

Возвращает
ndarray
(массив NumPy)

Копия массива arr со вставленными значениями values

Самое частое применение данной функции – вставка строки или столбца
в массив. Например, получили метки кластеров с помощью кластерного анализа, и их нужно добавить в качестве столбца в массив признаков, или получили новое наблюдение, его нужно добавить в качестве строки в массив признаков. Проиллюстрируем на нескольких примерах.
# создаем массив
a = np.array([[0.1,
[0.2,
[1.3,
[2.8,

0.2,
0.7,
3.8,
1.5,

0.5],
0.9],
2.2],
1.9]])

3.15. Функция np.repeat()  81
# создаем массив, который нужно добавить как строку
row = np.array([0.4, 0.7, 0.7])
# вставляем строку в конец массива
a = np.insert(a, 4, row, axis=0)
a
array([[0.1,
[0.2,
[1.3,
[2.8,
[0.4,

0.2,
0.7,
3.8,
1.5,
0.7,

0.5],
0.9],
2.2],
1.9],
0.7]])

# создаем массив, который нужно добавить как столбец
column = np.array([0, 1, 1, 0, 0])
# вставляем столбец в конец массива
a = np.insert(a, 3, column, axis=1)
a
array([[0.1,
[0.2,
[1.3,
[2.8,
[0.4,

0.2,
0.7,
3.8,
1.5,
0.7,

0.5,
0.9,
2.2,
1.9,
0.7,

0.
1.
1.
0.
0.

],
],
],
],
]])

3.15. Функция np.repeat()
Функция np.repeat() повторяет элементы массива. Функция имеет вид:
np.repeat(arr, repeats, axis=None)
Параметр

Предназначение

arr
Массив NumPy или любой объект, который может быть преобразован
(массив NumPy или в массив NumPy
объект, подобный
массиву)
repeats
(целое число или
массив целых
чисел)

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

axis
(целое число, не­
обязательный)

Задает ось, элементы которой нужно повторять. По умолчанию
axis=None, т. е. входной массив превращается в плоский, и создается
такой же итоговый плоский массив из повторов элементов первого

Возвращает
ndarray
(массив NumPy)

Массив, который образован повторами массива arr вдоль указанной
оси и имеет ту же форму, что и arr, кроме данной оси

Самое частое применение данной функции – инициализация константных
значений.
Допустим, 4 раза повторим элемент 3.

82



Инструменты

# 4 раза повторяем элемент 3
np.repeat(3, 4)
array([3, 3, 3, 3])

Два раза повторим элементы двумерного массива. Убеждаемся, что возвращается плоский массив.
# создаем двумерный массив
x = np.array([[1,2],
[3,4]])
# два раза повторяем элементы
np.repeat(x, 2)
array([1, 1, 2, 2, 3, 3, 4, 4])

Теперь три раза повторим элементы двумерного массива по оси 0 и по оси 1.
# три раза повторим элементы
# двумерного массива по оси 0
np.repeat(x, 3, axis=0)
array([[1,
[1,
[1,
[3,
[3,
[3,

2],
2],
2],
4],
4],
4]])

# три раза повторим элементы
# двумерного массива по оси 1
np.repeat(x, 3, axis=1)
array([[1, 1, 1, 2, 2, 2],
[3, 3, 3, 4, 4, 4]])

3.16. Функция np.unique()
Функция np.unique() находит уникальные элементы массива и возвращает их
в отсортированном массиве.
В зависимости от установленных параметров данная функция может возвращать:
 индексы входного массива, которые соответствуют его уникальным элементам;
 индексы уникального массива, которые позволяют восстановить входной массив;
 количество вхождений каждого уникального элемента во входном массиве.

3.16. Функция np.unique()  83
Параметр

Предназначение

arr
(массив NumPy или
объект, подобный
массиву)

Массив NumPy или любой объект, который может быть преобразован
в массив NumPy. Если входной массив не является одномерным и не
указана ось, вдоль которой нужно искать уникальные элементы, то
данный массив будет сжат до одной оси

return_index
(True или False,
необязательный)

Если задано True, то помимо самих уникальных элементов также
будут возвращаться их индексы во входном массиве. По умолчанию
return_index=False

return_inverse
(True или False,
необязательный)

Если задано True, то помимо самих уникальных элементов также
будут возвращаться индексы уникального массива, которые можно
использовать для восстановления входного массива. По умолчанию
return_inverse=False

return_counts
(True или False,
необязательный)

Если задано True, то помимо самих уникальных элементов также будет возвращаться количество вхождений каждого из них во входном
массиве. По умолчанию return_counts=False

axis
(целое число или
None, необязательный)

Определяет ось, по которой необходимо найти уникальные элементы. Если axis=None (по умолчанию), то входной массив будет сжат до
одной оси

Возвращает
ndarray
(массив NumPy)

Массив уникальных элементов

ndarray
(массив NumPy)

Массив индексов первых вхождений уникальных элементов (если
return_index=True)

ndarray
(массив NumPy)

Массив индексов всех вхождений уникальных элементов (если
return_inverse=True)

ndarray
(массив NumPy)

Массив с количеством вхождений уникальных элементов (если
return_counts=True)

Создадим одномерный массив и извлечем из него уникальные элементы.
# извлечем уникальные элементы
np.unique([1, 2, 2, 2, 3, 3, 3, 3, 3])
array([1, 2, 3])

Теперь посмотрим встречаемость каждого уникального значения.
# создаем массив
a = np.array([1, 2, 2, 2, 3, 3, 3, 3, 3])
# выведем частоты встречаемости уникальных элементов
values, counts = np.unique(a, return_counts=True)
dict(zip(values, counts))
{1: 1, 2: 3, 3: 5}

Теперь получим массив индексов первых вхождений уникальных элементов.

84



Инструменты

# получим массив индексов первых
# вхождений уникальных элементов
_, indices = np.unique(a, return_index=True)
indices
array([0, 1, 4])

3.17. Функция np.take_along_axis()
Функция np.take_along_axis() сопоставляет одномерные массивы индексов с соответствующими полными срезами исходного массива вдоль указанной оси
и возвращает найденные элементы.
Параметр

Предназначение

arr
Исходный массив
(массив NumPy или
объект, подобный
массиву)
indices
(массив NumPy,
необязательный)

Массив индексов, который должен быть либо транслируемым по массиву arr, либо содержать столько же одномерных массивов, сколько
их в индексируемом массиве вдоль указанной в параметре axis оси

axis
(целое число или
None, необязательный)

Определяет ось, вдоль которой извлекаются элементы с указанными
в одномерных массивах индексами. По умолчанию axis=None, что
соответствует извлечению элементов из сжатого до одной оси представления массива arr

Возвращает
ndarray
(массив NumPy)

Массив элементов исходного массива, выбранных в соответствии
с индексами одномерных массивов из полного среза вдоль указанной оси исходного массива

Самый типичный пример использования этой функции – выбор топ k значений из массива.
Давайте создадим массив NumPy.
# создаем массив X
np.random.seed(0)
X = np.round(np.random.rand(10,
X
array([[0.55, 0.72, 0.6 , 0.54,
[0.79, 0.53, 0.57, 0.93,
[0.98, 0.8 , 0.46, 0.78,
[0.26, 0.77, 0.46, 0.57,
[0.36, 0.44, 0.7 , 0.06,
[0.57, 0.44, 0.99, 0.1 ,
[0.16, 0.11, 0.66, 0.14,
[0.98, 0.47, 0.98, 0.6 ,
[0.32, 0.41, 0.06, 0.69,
[0.32, 0.67, 0.13, 0.72,

10), 2)
0.42,
0.07,
0.12,
0.02,
0.67,
0.21,
0.2 ,
0.74,
0.57,
0.29,

0.65,
0.09,
0.64,
0.62,
0.67,
0.16,
0.37,
0.04,
0.27,
0.18,

0.44,
0.02,
0.14,
0.61,
0.21,
0.65,
0.82,
0.28,
0.52,
0.59,

0.89,
0.83,
0.94,
0.62,
0.13,
0.25,
0.1 ,
0.12,
0.09,
0.02,

0.96,
0.78,
0.52,
0.94,
0.32,
0.47,
0.84,
0.3 ,
0.58,
0.83,

0.38],
0.87],
0.41],
0.68],
0.36],
0.24],
0.1 ],
0.12],
0.93],
0. ]])

3.17. Функция np.take_along_axis()  85
Затем получим отсортированные индексы по каждой строке.
# получим отсортированные индексы по строке
order = X.argsort(axis=1)
order
array([[9,
[6,
[4,
[4,
[3,
[3,
[7,
[5,
[2,
[9,

4,
4,
6,
0,
7,
5,
9,
7,
7,
7,

6,
5,
9,
2,
6,
4,
1,
9,
5,
2,

3,
1,
2,
3,
8,
9,
3,
6,
0,
5,

0,
2,
8,
6,
0,
7,
0,
8,
1,
4,

2,
8,
5,
5,
9,
1,
4,
1,
6,
0,

5,
0,
3,
7,
1,
8,
5,
3,
4,
6,

1,
7,
1,
9,
4,
0,
2,
4,
8,
1,

7,
9,
7,
1,
5,
6,
6,
0,
3,
3,

8],
3],
0],
8],
2],
2],
8],
2],
9],
8]])

Находим топ 3 индексов, т. е. получаем индексы 3 наблюдений с наибольшими значениями.
# найдем топ 3 индексов
top_idx = order[:, -3:][:, ::-1]
top_idx
array([[8,
[3,
[0,
[8,
[2,
[2,
[8,
[2,
[9,
[8,

7,
9,
7,
1,
5,
6,
6,
0,
3,
3,

1],
7],
1],
9],
4],
0],
2],
4],
8],
1]])

Теперь с помощью функции np.take_along_axis() из каждой строки извлекаем
3 наибольших значения.
# из каждой строки достанем 3 наибольших значения
top_k = np.take_along_axis(X, top_idx, axis=1)
top_k
array([[0.96,
[0.93,
[0.98,
[0.94,
[0.7 ,
[0.99,
[0.84,
[0.98,
[0.93,
[0.83,

0.89,
0.87,
0.94,
0.77,
0.67,
0.65,
0.82,
0.98,
0.69,
0.72,

0.72],
0.83],
0.8 ],
0.68],
0.67],
0.57],
0.66],
0.74],
0.58],
0.67]])

86



Инструменты

3.18. Функция np.array_split()
Функция np.array_split() разбивает массив на несколько подмассивов. Чаще
всего используется в операциях, которые нужно распараллелить. Допустим,
есть сложная функция, которая применяется к массиву построчно. Чтобы
ускорить вычисления, можно использовать параллельные процессы. Поясним на примере.
# импортируем Parallel и delayed для распараллеливания
from joblib import Parallel, delayed
# пишем медленную функцию
def slow_fn(x, n=5):
"""
Медленная функция.
Параметры
---------x: np.ndarray
Массив значений.
n: int
Количество итераций
выполнения операции.
Возвращает
---------results: np.ndarray
Массив значений.
"""
for i in range(n):
np.random.seed(0)
rnd = np.random.rand(*x.shape)
x = x + rnd
return x
# пишем функцию с распараллеливанием
def parallel_map(fn, x, n_jobs=4, *args, **kwargs):
"""
Функция с распараллеливанием.
Параметры
---------fn: callable
Вызываемая функция.
x: np.ndarray
Входной массив.
n_jobs: int
Количество процессов.
args: list
Неименованные аргументы для
передачи в функцию.
kwargs: dict
Именованные аргументы для
передачи в функцию.

3.18. Функция np.array_split()  87
Возвращает
---------results: np.ndarray
Массив значений.
"""
with Parallel(n_jobs) as p:
res = p(delayed(fn)(part, *args, **kwargs)
for part in np.array_split(x, n_jobs))
return np.concatenate(res)
# создаем массив
np.random.seed(0)
X = np.random.rand(100000, 100)
%%time
slow_fn(X, n=100)
CPU times: user 7.11 s, sys: 637 ms, total: 7.74 s
Wall time: 7.75 s
array([[ 55.4301639 ,
83.72294295,
[ 68.45947022,
5.86094519,
[ 31.49138408,
98.26486839,
...,
[100.58449943,
86.8888155 ,
[ 27.53629179,
34.7257013 ,
[ 66.54489318,
46.07244422,

72.234126 , 60.87910098, ..., 2.03086216,
0.4742431 ],
27.27080529, 74.25459623, ..., 25.69000466,
43.87607918],
70.33069237, 38.15293577, ..., 87.08134326,
97.04430046],
55.26692164, 97.40958124, ..., 97.61717411,
42.39754644],
7.55992853, 92.99295104, ..., 68.78146004,
5.88385571],
25.22817557, 49.20478964, ..., 70.86996313,
14.69914449]])

%%time
parallel_map(slow_fn, X, n_jobs=4, n=100)
CPU times: user 139 ms, sys: 158 ms, total: 297 ms
Wall time: 3.36 s
array([[ 55.4301639 ,
83.72294295,
[ 68.45947022,
5.86094519,
[ 31.49138408,
98.26486839,
...,
[ 31.76407696,
99.50098923,
[ 56.78399612,
6.75348646,
[ 83.17514864,
30.0120986 ,

72.234126 , 60.87910098, ..., 2.03086216,
0.4742431 ],
27.27080529, 74.25459623, ..., 25.69000466,
43.87607918],
70.33069237, 38.15293577, ..., 87.08134326,
97.04430046],
42.2681517 , 100.35156516, ..., 41.88772581,
49.18558749],
25.69477427, 94.95402764, ..., 95.01045663,
23.41179416],
31.92025962, 57.17805087, ..., 46.36804628,
74.90787463]])

88



Инструменты

Для получения подробной информации о библиотеке NumPy рекомендуется
ознакомиться со справочным руководством по NumPy на русском языке https://
pyprog.pro/reference_manual.html.

Задача с собеседования (SQL)
1. Из таблицы employees получите с помощью SQL-запросов:
 список сотрудников с именем 'David';
 cотрудника с минимальным возрастом;
 cотрудника с максимальным возрастом;
 список сотрудников старше 30 лет;
 список сотрудников, у которых в имени содержится буква 'j';
 средний возраст сотрудника в каждом отделе;
 количество сотрудников в каждом отделе;
 cписок сотрудников моложе 27 и работающих в отделе 'B'.
id

name

age

department

1

David

22

B

2

Paul

33

B

3

Jeremy

26

B

4

Jack

21

C

5

John

36

A

6

David

45

A

4. БИБЛИОТЕКИ NUMBA, DATATABLE,
BOTTLENECK Д ЛЯ УСКОРЕНИЯ
ВЫЧИСЛЕНИЙ
4.1. Numba
Numba (произносится как намба) – библиотека с открытым исходным кодом,
которая позволяет создавать быстрые функции для массивов NumPy. Ее можно
установить с помощью pip или conda: pip install numba или conda install numba. Numba
использует JIT-компилятор (компилятор «на лету», от «just in time» – «на лету») на
основе инфраструктуры LLVM (Low-Level Virtual Machine), позволяющий с помощью аннотирования транслировать массивоориентированный и математически
нагруженный питоновский код в машинный код во время исполнения программы, то есть «на лету» (just-in-time, отсюда и название). Таким образом, получаем
код, аналогичный по производительности языкам C, C ++, при этом можно избежать переключения языков или интерпретаторов Python. Numba поддерживает
компиляцию Python в машинный код на любом CPU или GPU и предназначен для
интеграции со стеком научного программного обеспечения Python.

4.1. Numba  89
Обратите внимание, что Numba можно использовать с pandas. Установив Numba, в некоторых методах pandas вы можете задать ключевое слово
engine='numba'. С вычислительной точки зрения первый запуск функции с использованием движка Numba будет медленным, поскольку у Numba будут некоторые накладные расходы на компиляцию функции. Однако функции, скомпилированные JIT, кешируются, и последующие вызовы будут выполняться быстро.
Кроме того, можно задать свою собственную функцию Python с декоратором @jit и передать массив NumPy, созданный на основе объектов Series или
Dataframe (используйте to_numpy()), в эту функцию.
Также обратите внимание, что библиотека Numba может выполнить любую
функцию, однако ускорить она может лишь определенные функции.
Когда мы передаем функцию, использующую только те операции, которые
Numba знает, как ускорить, библиотека работает в режиме nopython, т. е. библиотека выполняет компиляцию. Если библиотеке Numba компиляция не удалась (такое возможно, если передать функцию, которая использует то, с чем
библио­тека Numba не умеет работать, например функция использует список,
содержащий элементы разного типа), она переключается в режим object. В режиме object Numba выполнит ваш программный код, но при этом значительного увеличения производительности не произойдет. Так происходит по умолчанию (по умолчанию numba передан аргумент nopython=False).
Опционно библиотека Numba может выдать ошибку, если не сможет скомпилировать функциютак, чтобы ускорить выполнение вашего программного кода. Для этого передайте Numba аргумент nopython=True (например, @numba.
jit(nopython=True), что эквивалентно @numba.njit). Для получения более подробной информации о самой библиотеке смотрите документацию по библиотеке
Numba http://numba.pydata.org/.
Numba поддерживает создание и возвращение списков из JIT-скомпилированных функций, а также всех связанных методов и операций. Списки должны
быть строго однородными: Numba отклонит любой список, содержащий объекты разных типов, даже если эти типы совместимы (например, [1, 2.5] отклоняется, так как содержит int и float). В Numba для этих целей появился numba.
typed.List (типизированный список). Numba поддерживает генераторы спис­
ков. Все методы и операции над множествами поддерживаются в JIT-скомпилированных функциях. Множества должны быть строго однородными: Numba
отклонит любой набор, содержащий объекты разных типов, даже если типы
совместимы (например, {1, 2.5} отклоняется, поскольку объект содержит int
и float). Что касается словарей, функция dict() до недавнего времени не поддерживалась, теперь ее можно использовать, но без каких-либо аргументов, такое
использование по смыслу эквивалентно {} и numba.typed.Dict(). В результате мы
имеем экземпляр numba.typed.Dict, в котором пары ключ-значение будут определены позже при использовании.
Сейчас мы напишем функцию на чистом Python, которая вычисляет среднее
расстояние между двумя значениями с помощью цикла for.
# пишем функцию, вычисляющую среднее расстояние
# между двумя значениями
def mean_distance(x, y):

90



Инструменты

nx = len(x)
result = 0.0
count = 0
for i in range(nx):
result += x[i] - y[i]
count += 1
return result / count

Создаем два массива значений.
# создаем массивы
x = np.random.randn(10000000)
y = np.random.randn(10000000)

Давайте сравним нашу функцию mean_distance() и функцию np.mean() с точки
зрения скорости вычислений.
# проверяем скорость выполнения нашей функции
%timeit mean_distance(x, y)
4.44 s ± 181 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
# проверяем скорость выполнения функции np.mean()
%timeit np.mean(x - y)
29.7 ms ± 3 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

Видим, что функция NumPy более чем в 100 раз быстрее нашей функции

mean_distance().

Теперь импортируем библиотеку numba, с помощью функции numba.jit()
превращаем нашу функцию в компилируемую функцию Numba и проверяем
ее скорость работы.
# импортируем numba
import numba as nb
# превращаем нашу функцию в компилируемую функцию
# Numba с помощью функции numba.jit()
numba_mean_distance = nb.jit(mean_distance)
# проверяем скорость выполнения функции
# numba_mean_distance()
%timeit numba_mean_distance(x, y)
12.8 ms ± 1.53 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Полученная функция работает быстрее векторизированной функции NumPy.
Кроме того, мы можем взять программный код нашей функции, приведенный
выше, и аннотировать его с помощью декоратора @jit.
# воспользуемся декоратором @jit
@nb.jit

4.1. Numba  91
def nb_mean_distance(x, y):
nx = len(x)
result = 0.0
count = 0
for i in range(nx):
result += x[i] - y[i]
count += 1
return result / count
# проверяем скорость выполнения
# функции nb_mean_distance()
%timeit nb_mean_distance(x, y)
11.7 ms ± 898 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Декоратор @vectorize позволяет питоновским функциям, принимающим скалярные аргументы, использоваться в качестве универсальных функций NumPy.
Написание универсальной функции NumPy не является самым простым процессом и включает в себя написание некоторого кода на C. Numba делает это
легко. Используя декоратор @vectorize, Numba может скомпилировать чистую
функцию Python в универсальную функцию, которая работает с массивами
NumPy так же быстро, как традиционные универсальные функции, написанные на C. Итак, следует помнить, что, используя @vectorize, вы пишете функцию,
выполняющую операцию над скалярами, а не над массивами.
Рассмотрим следующий игрушечный пример, в котором мы умножаем все
значения в массиве x на 2.
# воспользуемся декоратором @vectorize
@nb.vectorize()
def nb_square(x):
return x ** 2
# проверяем скорость выполнения функции nb_square()
%timeit nb_square(x)
15.8 ms ± 218 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Сложные последовательности математических операций можно распараллелить с помощью параметра parallel декоратора @jit, тем самым сократив время выполнения программного кода. Сравним вариант без распараллеливания
и вариант с распараллеливанием.
# генерируем ряд значений
data = np.random.randn(10000000)
# воспользуемся декоратором @jit без распараллеливания
@nb.jit()
def f(x):
return np.cos(x) ** 2 + np.sin(x) ** 2
%timeit f(data)
127 ms ± 7.25 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

92



Инструменты

# воспользуемся декоратором @jit с распараллеливанием
@nb.jit(parallel=True)
def f(x):
return np.cos(x) ** 2 + np.sin(x) ** 2
%timeit f(data)
22.6 ms ± 540 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)

C помощью декоратора @jit с включенным распараллеливанием
(parallel=True) и функцией numba.prange() вы можете распараллелить операции
в цикле. Использование же декоратора @jit с отключенным распараллеливанием (parallel=False, используется по умолчанию) и функцией numba.prange() будет эквивалентно применению функции range().
# воспользуемся декоратором @jit без
# распараллеливания и функцией prange()
@nb.jit()
def compute(x):
s = 0
for i in nb.prange(x.shape[0]):
s += x[i]
return s
%timeit compute(data)
10.7 ms ± 335 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
# воспользуемся декоратором @jit без
# распараллеливания и функцией prange()
@nb.jit(parallel=True)
def compute(x):
s = 0
for i in nb.prange(x.shape[0]):
s += x[i]
return s
%timeit compute(data)
2.97 ms ± 146 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Посмотрим, как Numba может ускорить работу методов pandas на примере вычисления скользящего среднего с шириной окна 10. Простое скользящее
среднее (simple moving average – SMA) – полезный признак для временных рядов. Его формула незатейлива:

В рамках подхода «скользящее окно» библиотека pandas вычисляет статис­
тику по «окну» данных, представляющему определенный период времени. Затем окно смещается на определенный интервал времени, и статистика постоянно вычисляется для каждого нового окна до тех пор, пока окно охватывает

4.1. Numba  93
даты временного ряда. На рисунке ниже показано вычисление скользящего
среднего с шириной окна 3.

Рис. 13 Вычисление скользящего среднего с шириной окна 3

Итак, давайте импортируем необходимые библиотеки и создадим экспериментальные данные.
# импортируем библиотеки pandas и numpy
import pandas as pd
import numpy as np
# создаем серию со 100 000 значений
series = pd.Series(np.random.randn(100000))
# задаем объект Rolling
roll = series.rolling(10)
# пишем функцию вычисления среднего
def f(x):
return np.mean(x)

Делаем вычисления с помощью методов pandas с движком Numba и без него.
# запускаем в первый раз, время компиляции
# повлияет на производительность, здесь и
# далее если raw=True, то переданная в метод .apply()
# функция вместо серии получает массив NumPy,
# -r(epeat) – сколько раз повторять таймер,
# -n(umber) – сколько раз выполнять команду
%timeit -r 1 -n 1 roll.apply(f, engine='numba', raw=True)
434 ms ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)
# функция кешируется, и производительность улучшается
%timeit roll.apply(f, engine='numba', raw=True)
14.5 ms ± 223 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

94



Инструменты

%timeit roll.apply(f, raw=True)
690 ms ± 23.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Видим, что использование движка Numba позволило сократить время вычислений.

4.2. Datatable
Datatable – это питоновская библиотека, которая аналогично pandas предназначена для предварительной подготовки данных, но в большей степени
ориентирована на обеспечение высокой скорости обработки данных и на поддержку больших наборов данных. Библиотеку с определенным допущением
можно назвать аналогом пакета R data.table. С помощью datatable можно работать как с данными, которые полностью помещаются в оперативной памяти, так и с данными, размер которых превышает объём доступной RAM. Спонсором разработки datatable является компания H2O.ai. Datatable можно легко
установить с помощью команды pip install datatable.
Давайте сравним скорость загрузки файла размером 291 Мб (набор данных
с 1 787 571 наблюдением и 34 столбцами) в pandas и datatable.
%%time
# загружаем данные с помощью pandas
dataframe = pd.read_csv('Data/train.csv', sep=',')
CPU times: user 5.88 s, sys: 1.15 s, total: 7.03 s
Wall time: 5.45 s
%%time
# загружаем данные с помощью datatable
datatable_df = dt.fread('Data/train.csv', sep=',')
CPU times: user 3.89 s, sys: 415 ms, total: 4.3 s
Wall time: 390 ms

Видим, что загрузка данных с помощью пакета datatable происходит значительно быстрее. Убедимся, что мы получили объект Frame пакета datatable.
# убедимся, что перед нами – объект
# Frame библиотеки datatable
type(datatable_df)
datatable.Frame

Итак, мы получили объект Frame пакета datatable. При желании его можно
преобразовать в объект DataFrame библиотеки pandas или массив NumPy с помощью методов .to_pandas() и .to_numpy() соответственно.
Попробуем преобразовать наш фрейм datatable в объект DataFrame библиотеки pandas и посмотрим на то, сколько это займёт времени.

4.2. Datatable  95
%%time
# преобразовываем фрейм в датафрейм pandas
datatable_pandas = datatable_df.to_pandas()
CPU times: user 14.2 s, sys: 1.35 s, total: 15.5 s
Wall time: 2.84 s

Преобразование фрейма в объект DataFrame библиотеки pandas занимает
меньше времени, чем загрузка данных в DataFrame средствами pandas.
Рассмотрим основные свойства метода объекта Frame библиотеки datatable.
С помощью свойства shape можно узнать количество наблюдений и количест­
во столбцов фрейма.
# смотрим количество строк и столбцов
datatable_df.shape
(1787571, 34)

С помощью метода .head() можно вывести первые 10 наблюдений.
# смотрим первые 10 наблюдений
datatable_df.head()

Цвета имен столбцов указывают на тип данных. Красным цветом обозначены строки, зелёным – целые числа, синим – числа с плавающей точкой.
С помощью свойств .names() и .stypes() можно вывести типы переменных.
# смотрим типы переменных
for col in range(len(datatable_df.names)):
print(datatable_df.names[col], ':', datatable_df.stypes[col])
ID : stype.int32
SK_DATE_DECISION : stype.int32
DEF : stype.bool8
NUM_SOURCE : stype.int32
CREDIT_ACTIVE : stype.int32
CREDIT_COLLATERAL : stype.bool8
CREDIT_CURRENCY : stype.str32
DTIME_CREDIT : stype.str32
CREDIT_DAY_OVERDUE : stype.int32
DTIME_CREDIT_ENDDATE : stype.str32
DTIME_CREDIT_ENDDATE_FACT : stype.str32

96



Инструменты

CREDIT_FACILITY : stype.int32
AMT_CREDIT_MAX_OVERDUE : stype.float64
CNT_CREDIT_PROLONG : stype.int32
AMT_CREDIT_SUM : stype.float64
AMT_CREDIT_SUM_DEBT : stype.float64
AMT_CREDIT_SUM_LIMIT : stype.float64
AMT_CREDIT_SUM_OVERDUE : stype.float64
CREDIT_SUM_TYPE : stype.bool8
CREDIT_TYPE : stype.int32
DTIME_CREDIT_UPDATE : stype.str32
CREDIT_DELAY30 : stype.int32
CREDIT_DELAY5 : stype.int32
CREDIT_DELAY60 : stype.int32
CREDIT_DELAY90 : stype.int32
CREDIT_DELAY_MORE : stype.int32
AMT_REQ_SOURCE_HOUR : stype.int32
AMT_REQ_SOURCE_DAY : stype.int32
AMT_REQ_SOURCE_WEEK : stype.int32
AMT_REQ_SOURCE_MON : stype.int32
AMT_REQ_SOURCE_QRT : stype.int32
AMT_REQ_SOURCE_YEAR : stype.int32
AMT_ANNUITY : stype.float64
TEXT_PAYMENT_DISCIPLINE : stype.str32

Статистики по столбцам можно получить с помощью следующих методов:
datatable_df.sum()
datatable_df.sd()
datatable_df.mode()
datatable_df.nmodal()

datatable_df.nunique()
datatable_df.max()
datatable_df.min()
datatable_df.mean()

Для отбора строк и столбцов используем квадратные скобки.
# отбираем столбец NUM_SOURCE
# и выводим первые 5 наблюдений
datatable_df[:, 'NUM_SOURCE'].head(5)

# отбираем первые 3 строки столбца NUM_SOURCE
datatable_df[:3, 'NUM_SOURCE']

4.2. Datatable  97
# отбираем первые 3 строки столбца NUM_SOURCE
# (столбца с индексом 2)
datatable_df[:3, 2]

# отбираем первые 3 строки первых двух столбцов
# (столбцов ID и SK_DATE_DECISION)
datatable_df[:3, :2]

С помощью метода .countna() можно вывести количество пропусков по каж­
дой переменной.
# выведем количество пропусков по каждой переменной
datatable_df.countna()

Теперь посмотрим, как можно вычислить групповые статистики. Сейчас мы
выведем средние суммы задолженности по первым 5 клиентам.
# выведем средние суммы задолженности по первым 5 клиентам
datatable_df[:, dt.mean(dt.f.AMT_CREDIT_SUM_DEBT), dt.by('ID')].head(5)

С помощью ключевого слова del можно удалять переменные. Например, удалим переменную ID.
# удаляем столбец ID
del datatable_df[:, 'ID']

Содержимое объекта Frame можно записать в CSV-файл, что позволяет использовать данные в будущем.

98



Инструменты

# сохраняем фрейм в CSV-файл
datatable_df.to_csv('datatable_results.csv')

Библиотека datatable работает быстрее библиотеки pandas, однако пока
главный минус datatable в сравнении с pandas – ограниченный функционал.

4.3. Bottleneck
Bottleneck – это коллекция быстрых функций NumPy, написанных на языке C. Пакет хорошо знаком исследователям, работающим с временными рядами. В модуле move пакета bottleneck можно найти быстрые реализации скользящих статистик.
Таблица 2 Скользящие статистики в pandas
Функция

Описание

move_mean()

Среднее значение в окне

move_std

Стандартное отклонение в окне

move_var()

Дисперсия в окне

move_min()

Минимальное значение в окне

move_max()

Максимальное значение в окне

move_median()

Медиана в окне

move_sum()

Сумма в окне

Разберем функцию bn.move_mean(), вычисляющую скользящее среднее. Функция не работает с объектом Series, ей нужно передать массив NumPy. С помощью параметра window можно задать ширину окна. Параметр min_count работает
следующим образом: если количество непропущенных значений в окне меньше min_count, окну присваивается значение NaN. По умолчанию для параметра
min_count установлено значение None, это значит, что параметру min_count мы назначаем значение параметра window.
Теперь мы создадим серию с 100 000 значений, сравним вычисление скользящего среднего с помощью функции rolling().mean() библиотеки pandas и с
помощью функции move_mean() пакета bottleneck.
# импортируем необходимые библиотеки
import bottleneck as bn
import pandas as pd
import numpy as np
# создаем серию со 100 000 значений
series = pd.Series(np.random.randn(100000))
%%time
roll_mean = series.shift(1).rolling(
min_periods=1, window=4).mean()
CPU times: user 4.91 ms, sys: 2.57 ms, total: 7.48 ms
Wall time: 6.51 ms
%%time

5. SciPy  99
roll_mean_bn = bn.move_mean(series.shift(1),
window=4, min_count=1)
CPU times: user 772 µs, sys: 387 µs, total: 1.16 ms
Wall time: 597 µs

Видим, что функция bn.move_mean() пакета bottleneck работает быстрее.

Задачи с собеседований (теория вероятности)
1. В урне имеется 3 белых и 4 черных шара. Из урны вытягиваются 3 шара.
Найти вероятность, что хотя бы один из них окажется белым.
2. Игральный кубик бросается 6 раз. Найти вероятность, что выпадет хотя
бы одна шестерка.

5. SciPy
SciPy (произносится как сайпай) представляет собой набор математических
и статистических функций для научных вычислений в Python. Библиотека
SciPy позволяет:
 выполнять статистические тесты;
 находить минимумы и максимумы функций;
 вычислять интегралы функций;
 поддерживать специальные функции;
 выполнять обработку сигналов;
 выполнять обработку изображений;
 работать с генетическими алгоритмами;
 решать обыкновенные дифференциальные уравнения.
Базовая структура данных – массив NumPy.
Чаще всего пакет SciPy используется для тестирования статистических гипотез.
Гипотеза в переводе с греческого обозначает «предположение». Статистическая гипотеза, которую мы выдвигаем, представляет собой некоторое предположение о генеральной совокупности, проверяемое по наблюдаемой выборке данных. Она называется нулевой и обозначается через H0. Например, мы выдвигаем
нулевую гипотезу о том, что средние значения двух генеральных совокупностей,
из которых извлечены сравниваемые зависимые выборки, не отличаются друг
от друга. Наряду с выдвинутой гипотезой H0 рассматривают и альтернативную
ей гипотезу H1 (средние значения двух генеральных совокупностей, из которых
извлечены сравниваемые зависимые выборки, различны).
В итоге проверки гипотез могут быть приняты неправильные решения, т. е.
могут быть допущены ошибки двух родов.
Ошибка первого рода состоит в том, что мы отвергаем нулевую гипотезу H0,
когда она верна. Вероятность ошибки I рода (вероятность ложного отклонения
нулевой гипотезы) обозначают p и называют p-значением (p-value). Можно
сказать, что p-значение – вероятность того, что случайная величина, имеющая

100



Инструменты

распределение выбранной статистики при условии верности нулевой гипотезы, примет значение, не меньшее, чем вычисленное значение этой статистики.
Пороговую (критическую) вероятность совершить ошибку первого рода
принято обозначать α или уровнем значимости (significance level). Уровень
значимости α связан с доверительной вероятностью 1 – α.
Традиционно выделяют три уровня значимости:
 p ≤ 0,05 (α = 0,05) – обычный уровень статистической значимости (для
наглядности традиционно обозначается одной звездочкой *);
 p ≤ 0,01 (α = 0,01) – высокий уровень значимости (для наглядности традиционно обозначается двумя звездочками **);
 p ≤ 0,001 (α = 0,001) – очень высокий уровень значимости (обозначается
тремя звездочками ***).
Наиболее часто уровень значимости принимают равным 0,05 или 0,01. Если,
например, мы приняли уровень значимости, равный 0,05, доверительная вероятность будет составлять 0,95.
Вы всегда должны помнить, что в статистике «вероятность» относится не к
гипотезам, а к величинам, которые являются гипотетическими частотами появления паттернов или закономерностей в рамках предполагаемой статистической модели.
Например, выше мы выдвинули нулевую гипотезу о том, что средние значения двух генеральных совокупностей, из которых извлечены сравниваемые
зависимые выборки, не отличаются друг от друга. Допустим, мы получили
p-значение 0,05, это означает, что в 5 случаях из 100 мы допускаем ошибку
первого рода – отвергаем нулевую гипотезу, когда она верна (т. е. средние значения генеральных совокупностей равны). Однако это не обозначает, что вероятность того, что между средними значениями генеральных совокупностей
есть разница, составляет 95 %.
Ошибка второго рода состоит в том, что мы делаем вывод об отсутствии оснований отвергнуть нулевую гипотезу H0, когда она неверна. Ошибку II рода
обозначают β.
Обратите внимание, фраза «нет оснований отклонить нулевую гипотезу» не
тождественна фразе «принять нулевую гипотезу», которая является неверной.
Нулевая гипотеза обычно имеет очень конкретную формулировку. В нашем
случае она звучит так: нет разницы между средними значениями генеральной
совокупности № 1 и генеральной совокупности № 2. Если мы не можем отклонить нулевую гипотезу, значит ли это, что данные значения равны? Вовсе не
обязательно. То, что нам не удалось найти статистически значимую разницу,
совершенно не означает, что мы доказали равенство двух величин. Мы можем
лишь сказать «у нас недостаточно оснований отклонить нулевую гипотезу».
Здесь полезно привести цитату Роналда Фишера, собственно, и предложившего идею «нулевой гипотезы2: «...нулевая гипотеза никогда не доказывается и не принимается, но лишь с определенной вероятностью опровергается
в ходе экспериментов. Можно сказать, что каждый эксперимент существует
только для того, чтобы дать фактам шанс опровергнуть нулевую гипотезу».
2

Она была предложена Фишером в рамках эксперимента «леди, дегустирующая чай»:
https://ru.wikipedia.org/wiki/Леди,_дегустирующая_чай.

5. SciPy  101
Кроме того, следует помнить: результаты применения статистических
критериев зависят от величины различий и от размера выборки, и одинаковые различия на выборках разного размера могут оказаться в одном случае
незначимыми (например, если есть две выборки по 20 наблюдений), а в другом (когда наблюдений будет по 1000) – значимыми на том же уровне значимости. Здесь можно привести шутливую цитату Эндрю Гельмана: «Маловероятно, что маленькое n найдет маленькие p-значения. Большое n, вероятно,
что-то да найдет. Ну а огромное n почти наверняка найдет множество маленьких p-значений».
С вероятностью ошибки II рода тесно связана другая величина, имеющая
большое статистическое значение, – мощность критерия. Она вычисляется по
формуле (1 − β) и характеризует способность критерия выявлять различия там,
где они есть. Таким образом, чем выше мощность, тем меньше вероятность
совершить ошибку II рода (проигнорировать различия там, где они есть).
Таблица 3 Ошибка I рода и ошибка II рода

Уменьшая α, мы уменьшаем вероятность ошибки I рода (вероятность найти
несуществующие различия), но повышаем вероятность ошибки II рода (вероятность проигнорировать различия, когда они есть). Таким образом, вероятности обеих ошибок обратно зависят друг от друга, и их нельзя минимизировать
одновременно. По мере уменьшения вероятности одной ошибки увеличивается вероятность другой, и наоборот.
Для принятия решения о том, можно ли отклонить нулевую гипотезу
и принять альтернативную, используют статистические критерии. Статистический критерий включает в себя метод расчета определенного показателя,
на основании которого принимается решение об отклонении нулевой гипотезы, а также условия принятия решения. Этот рассчитываемый показатель
называется эмпирическим (или экспериментальным) значением критерия.
Найденное эмпирическое значение сравнивается с известным (например,
заданным таблично или определенным с помощью той или иной статистической программы) эталонным числом, именуемым критическим значением критерия. В статистических таблицах критические значения приводятся,
как правило, для нескольких уровней значимости: 5 % (0,05), 1 % (0,01) и др.
Статистический критерий требует выполнения ряда предпосылок. Например, многие статистические критерии требуют нормальности распределения данных.
Статистический критерий зависит также от числа степеней свободы.

102



Инструменты

Количество степеней свободы – это количество значений в итоговом вычислении статистики, способных варьироваться. В третьем издании «Statistics in Plain
English» дается следующее определение степеней свободы. «Грубо говоря, это минимальный объем данных, необходимый для расчета статистики. На практике это
число или числа, используемые для аппроксимации количества наблюдений в наборе данных с целью определения статистической значимости». Количество степеней свободы рассчитывается как количество независимых значений, использованных при расчете статистики, минус количество рассчитанных статистик:
количество степеней свободы = количество независимых
значений – количество статистик.
Например, у нас 50 наблюдений, и мы хотим вычислить выборочную статистику, например среднее. Все 50 наблюдений используются в вычислениях,
и у нас одна статистика, таким образом, количество степеней свободы для расчета среднего будет равно 50 – 1 = 49.
Нетрудно понять, что количество степеней свободы линейно зависит от объема выборки (например, df = n – 1), а также от количества признаков или их
градаций: чем больше эти показатели, тем больше число степеней свободы.
Не существует единой формулы для определения числа степеней свободы для
всех возможных случаев, поэтому статистический критерий также устанавливает формулу для расчета числа степеней свободы.
Для большинства статистических критериев действует следующее правило:
если эмпирическое значение критерия для данного числа степеней свободы
оказывается строго больше критического значения, соответствующего выбранному уровню значимости, то нулевая гипотеза отклоняется в пользу альтернативной с соответствующей достоверностью.
Критерий бывает параметрическим или непараметрическим. Параметрический критерий основывается на оценке параметров (таких как среднее или
стандартное отклонение) распределения интересующей величины. Примерами параметрических критериев являются t-критерий Стьюдента, хи-квадрат
Пирсона и др. Непараметрические методы не основываются на оценке параметров распределения. Примерами непараметрических критериев являются
W-критерий Уилкоксона, Q-критерий Кохрена.
Критерий может быть предназначен для независимых выборок и зависимых
выборок. Если можно установить гомоморфную пару (то есть когда одному случаю
из выборки X соответствует один и только один случай из выборки Y и наоборот)
для каждого случая в двух выборках (и это основание взаимосвязи является важным для измеряемого на выборках признака), такие выборки называются зависимыми. Примеры зависимых выборок: пары близнецов, два измерения какого-либо признака до и после экспериментального воздействия, мужья и жёны. В случае
если такая взаимосвязь между выборками отсутствует, то эти выборки считаются
независимыми, например мужчины и женщины, психологи и математики.
Таким образом, общая процедура проверки статистической гипотезы включает в себя следующие шаги:
1. Сформулировать задачу.
2. Сформулировать нулевую и альтернативную гипотезы.
3. Выбрать требуемый уровень значимости.

5. SciPy  103
4. Выбрать соответствующий статистический критерий (критерий проверки
гипотезы), убедившись в выполнении условий его применимости.
5. Определить количество степеней свободы.
6. Вычислить эмпирическое значение критерия.
7. Сравнить эмпирическое значение критерия с критическим значением.
Давайте потренируемся в проверке статистических гипотез с помощью
SciPy. У нас есть задача снизить смертность от сахарного диабета. Для оценки
эффективности нового гипогликемического средства были проведены измерения уровня глюкозы в крови пациентов, страдающих сахарным диабетом,
до и после приема препарата.
Давайте импортируем необходимые библиотеки и функции, загрузим данные (10 наблюдений) и взглянем на них.
# импортируем библиотеки, функции
import numpy as np
import pandas as pd
from scipy.stats import chi2_contingency, ttest_rel
# загружаем и смотрим данные
data = pd.read_csv('Data/glucose.csv', sep=';')
data

Речь идет об измерениях признака (уровня глюкозы в крови) у пациента до
и после экспериментального воздействия, и мы можем установить гомоморфную пару для каждого случая в двух выборках. Таким образом, речь идет о зависимых выборках.
Нулевая гипотеза H0 звучит так: средние значения двух генеральных совокупностей, из которых извлечены сравниваемые зависимые выборки, не отличаются друг от друга. Альтернативная гипотеза H1 звучит так: средние значения двух генеральных совокупностей, из которых извлечены сравниваемые
зависимые выборки, отличаются друг от друга.
Задаем уровень значимости 0,05. Таким образом, если эмпирическое значение выбранного критерия для зависимых выборок равно или больше критического значения, соответствующего уровню значимости 0,05 (находим по

104



Инструменты

таблице критических значений соответствующего критерия), отклоняем нулевую гипотезу. Делаем вывод о наличии статистически значимых различий
содержания глюкозы в крови до и после приема нового препарата при уровне
значимости 0,05. Если значение рассчитанного критерия для зависимых выборок меньше табличного, значит, у нас нет достаточных оснований отклонить
нулевую гипотезу. Делаем вывод об отсутствии статистически значимых различий содержания глюкозы в крови до и после приема нового препарата.
Мы применяем t-критерий Стьюдента для зависимых выборок, убедившись,
что соблюдены условия его применимости. Критерий вычисляется по формуле:

где:
Md – среднее разностей показателей, измеренных до и после;
σd – стандартное отклонение разностей показателей;
n – количество исследуемых.
Для применения критерия необходимо, чтобы исходные данные имели нормальное распределение. Мы убедились в этом. Мы посмотрели скос (асиммет­
рию) распределения и эксцесс (остроту пика распределения), необходимо,
чтобы они были близки к нулю. Еще мы посмотрели медиану и среднее, для
нормального распределения они будут совпадать.
# проверяем данные на нормальность
print('Скос признака before:', data['before'].skew())
print('Эксцесс признака before:', data['before'].kurtosis())
print('')
print('Скос признака after:', data['after'].skew())
print('Эксцесс признака after:', data['after'].kurtosis())
Скос признака before: 0.9058504758902429
Эксцесс признака before: -0.4489742150083589
Скос признака after: 0.6767808127701386
Эксцесс признака after: 0.8331238763353594
# еще дополнительная проверка на нормальность
data[['before', 'after']].describe()

5. SciPy  105
Теперь нужно найти число степеней свободы df по формуле: df = n – 1. В нашем случае будет 9 степеней свободы.
После этого по таблице критических значений определяем критическое
значение t-критерия Стьюдента на пересечении строки – вычисленного числа
степеней свободы df и столбца – требуемого уровня значимости. Нам нужно
вычислить эмпирическое значение и сравнить его с критическим значением.
Итак, вычисляем эмпирическое значение. Сначала вычисляем разность
каж­дой пары значений.
# вычисляем разность каждой пары значений
diff = data['before'] - data['after']

Вычисляем среднее разностей:
.
# вычисляем среднее разностей
mean_diff = sum(diff) / len(data)
mean_diff
3.12

Вычисляем стандартное отклонение разностей от среднего:

# вычисляем стандартное отклонение разностей от среднего
std_diff = np.sqrt((sum((mean_diff - diff) ** 2)) / (len(data) - 1))
std_diff
1.1773793875477105
# еще можно так
std_diff = diff.std()
std_diff
1.1773793875477105

Вычисляем t-критерий Стьюдента для зависимых выборок:

# вычисляем t-критерий Стьюдента для зависимых выборок
t = mean_diff / (std_diff / np.sqrt(len(data)))
t
8.379887064504546

Сравним эмпирическое значение 8,38 с критическим значением – табличным значением, которое при числе степеней свободы df, равном 10 – 1 = 9,
и уровне значимости 0,05 (доверительной вероятности 0,95) составляет 2,262

106



Инструменты

(выделено красным овалом). Видим, что полученное эмпирическое значение
больше критического значения для уровня значимости 0,05.
Таблица 4 Таблица критических значений t-критерия Стьюдента

Полученное эмпирическое значение больше критического, поэтому отклоняем
нулевую гипотезу, т. е. делаем вывод о наличии статистически значимых различий
содержания глюкозы в крови до и после приема нового препарата при α = 0,05.
А теперь автоматически вычислим t-критерий Стьюдента для зависимых
выборок с помощью функции ttest_rel() библиотеки SciPy.
# можно воспользоваться функцией ttest_rel()
# библиотеки SciPy
stats.ttest_rel(data['before'], data['after'])
Ttest_relResult(statistic=8.379887064504544, pvalue=1.5250840961461116e-05)

Видим, что наше эмпирическое значение соответствует p-значению 0,000015.
Вероятность того, что t-статистика примет значение, не меньшее, чем вычисленное значение 8,38, когда средние значения генеральных совокупностей не
отличаются друг от друга, составляет 0,000015. В 0,0015 % случаев из 100 мы
рис­куем допустить ошибку первого рода – отвергаем нулевую гипотезу, когда
она верна (т. е. средние значения генеральных совокупностей равны).
Итак, наше p-значение меньше α = 0,05. Отклоняем нулевую гипотезу, т.е. делаем вывод о наличии статистически значимых различий содержания глюкозы
в крови до и после приема нового препарата при уровне значимости α = 0,05.
Допустим, мы строим скоринговую модель, у нас есть признак Семейное положение с категориями Холост и Женат и зависимая переменная Наличие просрочки
с категориями Нет просрочки и Есть просрочка. Мы хотим выяснить, существует ли
связь между семейным положением и риском просрочки. Нулевая гипотеза звучит так: в генеральной совокупности категории признака не отличаются друг от
друга с точки зрения распределения категорий зависимой переменной. Альтернативная гипотеза заключается в том, что в генеральной совокупности категории признака отличаются друг от друга с точки зрения распределения категорий
зависимой переменной. Мы применим критерий хи-квадрат Пирсона.

5. SciPy  107
Можно выделить три условия применимости критерия хи-квадрат:
 случайный выбор наблюдений;
 ожидаемые частоты менее 5 должны встречаться не более чем в 20 %
ячейках таблицы;
 суммы по строкам и столбцам всегда должны быть больше нуля.
Критерий хи-квадрат подчиняется распределению хи-квадрат со степенями
свободы df = (R − 1) (C − 1), где R и C – количество строк и столбцов в таблице сопряженности. В нашем случае количество степеней свободы будет равно
df = (2 − 1) (2 − 1) = 1.
Давайте вычислим эмпирическое значение критерия.
Мы строим двухвходовую таблицу сопряженности, где строки являются категориями признака Семейное положение, а столбцы – категориями зависимой
переменной Наличие просрочки. Для каждой ячейки таблицы фиксируем наблюдаемую частоту O (от observed). Затем для каждой ячейки фиксируем ожидаемую частоту E (от expected) согласно нулевой гипотезе. В итоге для каждой
ячейки вычисляем квадрат разности между наблюдаемой и ожидаемой частотой, поделенный на ожидаемую частоту.

Риc. 14 Наблюдаемые и ожидаемые частоты для вычисления хи-квадрат

Складываем результаты, вычисленные по каждой ячейке, и получаем эмпирическое значение хи-квадрат (χ2). Таким образом, критерий хи-квадрат
Пирсона – это показатель, вычисляющий степень суммарного расхождения
наблюдаемых (реальных) и ожидаемых частот:

где
Oi – наблюдаемая частота i-й ячейки;
Ei – ожидаемая частота i-й ячейки.

108



Инструменты

.
Сравним эмпирическое значение 2,216 с критическим значением – табличным значением, которое при числе степеней свободы df, равном 1, и уровне
значимости 0,05 (доверительной вероятности 0,95) составляет 3,84 (выделено
красным овалом).
Таблица 5 Таблица критических значений критерия хи-квадрат

Полученное эмпирическое значение меньше критического значения, по­
этому у нас нет оснований отклонить нулевую гипотезу. Можно сделать вывод,
что категории переменной Семейное положение действительно не отличаются
друг от друга с точки зрения распределения клиентов без просрочки и клиентов с просрочкой при уровне значимости α = 0,05.
Если бы мы воспользовались SciPy, то быстро бы вычислили, что в нашем
случае значение хи-квадрат 2,216 с одной степенью свободы соответствует
p-значению 0,1366. Вероятность найти различия по зависимой переменной,
когда их нет, составляет 0,1366. Это превышает уровень значимости α = 0,05.
Значит, у нас нет оснований отвергнуть нулевую гипотезу.
Использование распределения хи-квадрат для интерпретации критерия
хи-квадрат Пирсона требует от нас предположения, что дискретное распределение наблюдаемых частот можно аппроксимировать непрерывным распределением хи-квадрат. Это предположение не вполне корректно и дает определенную ошибку.
Для уменьшения ошибки аппроксимации английский статистик Фрэнк Йейтс
предложил поправку на непрерывность. Ее суть в следующем: аппроксимация
распределения критерия хи-квадрат может быть улучшена понижением абсолютного значения разности между наблюдаемой и ожидаемой частотами на
величину 0,5 перед возведением в квадрат.

5. SciPy  109

Это позволяет снизить значение хи-квадрат и таким образом увеличить
p-значение.
Поправка Йейтса призвана избежать переоценки статистической значимости небольших данных. Эта формула преимущественно используется тогда,
когда хотя бы одна из ячеек в таблице имеет ожидаемую частоту меньше 5.
Давайте автоматически вычислим критерий хи-квадрат Пирсона с помощью функции chi2_contingency() модуля stats библиотеки SciPy. Функция chi2_
contingency() имеет вид:
scipy.stats.chi2_contingency(observed,
correction=True,
lambda_=None)

Параметр observed задает таблицу сопряженности (массив NumPy или подобный массиву объект). Параметр correction задает поправку Йейтса (если задано
значение True и число степени свободы равно 1). Параметр lambda_ задает вычисляемый критерий (по умолчанию вычисляется критерий хи-квадрат Пирсона).
Функция возвращает значение критерия хи-квадрат, соответствующее
p-значение, число степеней свободы и ожидаемые частоты (в виде массива
Numpy).
# создаем двумерный массив – таблицу сопряженности
obs = np.array([[20, 13], [30, 37]])
# вычисляем значение критерия хи-квадрат, соответствующее
# p-значение, число степеней свободы и ожидаемые частоты,
# без поправки Йейтса
chi2_contingency(obs, correction=False)
(2.2161917684305745, 0.13656956347453514, 1, array([[16.5, 16.5], [33.5, 33.5]]))
# а еще можно было так
chi2, p, dof, ex = chi2_contingency(obs, correction=False)

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

110



Инструменты

высокий уровень значимости является показателем того, что такую оценку
размера эффекта, какая получена по выборке, с очень маленькой вероятностью можно было получить случайно.
Достигаемый уровень значимости зависит не только от размера эффекта, но
и от объема выборки, по которой оценивается эффект. Вспоминаем вышеприведенное шутливое высказывание Эндрю Гельмана. Если выборка небольшая,
скорее всего, нулевая гипотеза на ней не отвергается (если только она не слишком неразумная). Однако с ростом объема выборки начинают проявляться все
более тонкие отклонения данных от нулевой гипотезы. Велика вероятность,
что на достаточно большой выборке значительная часть разумных нулевых
гипотез будет отвергнута. Именно поэтому, даже если нулевая гипотеза отвергнута, это еще не значит, что полученный эффект имеет какую-то практическую значимость, ее нужно оценивать отдельно.
Например, было проведено большое исследование, в рамках которого на
протяжении трех лет у большой выборки женщин измеряли вес, а также оценивали, насколько активно они занимаются спортом. По итогам исследования
выяснилось, что женщины, которые в течение этого времени упражнялись не
меньше часа в день, набрали значительно меньше веса, чем женщины, которые упражнялись менее 20 минут в день. Статистическая значимость этого
результата была достаточно высока: p < 0,001. Проблема заключалась в размере эффекта: разница в набранном весе между двумя исследуемыми группами
женщин составила всего 150 граммов. 150 граммов за 3 года – это не очень
много. Крайне сомнительно, что этот эффект имеет какую-то практическую
значимость.
В медицинской практике часто досрочно прерывают испытания лекарств,
если, например, обнаруживается, что прием препарата ведет к значимому
увеличению опасных заболеваний (допустим, повышает риск инфаркта на
0,07 %). Эффект статистически значим, но при этом на первый взгляд кажется,
что размер эффекта ничтожен. Однако если пересчитать размер эффекта на
всю популяцию людей, которым этот препарат может быть выписан, результатом могут быть тысячи умерших. Поэтому разработчики такой препарат немедленно запрещают и снимают с рынка.
Этот пример показывает, что практическую значимость результата нельзя
определить на глаз. В идеале она должна определяться человеком, разбирающимся в предметной области.

Задачи с собеседований (математическая статистика)
1. В ходе исследования были опрошены 1000 человек. 20 % из них заинтересовались новым продуктом. Вычислите 95%-ный доверительный интервал
для реальной доли заинтересованных в продукте.
2. Вычислите объем выборки, предельная ошибка которой составит 4 %. При
этом мы принимаем 95%-ный доверительный уровень, генеральная совокупность значительно больше выборки, доля респондентов с наличием исследуемого признака равна 0,5. Принять, что выборка является простой случайной,
объем выборки значительно меньше генеральной совокупности.

6.4. Объекты DataFrame и Series  111

6. PANDAS
6.1. Почему pandas?
pandas – одна из самых популярных библиотек для исследования данных с открытым исходным кодом, доступных в настоящее время. Она дает своим пользователям возможность исследовать, манипулировать, запрашивать, агрегировать и визуализировать табличные данные. Табличные данные относятся
к двумерным данным, состоящим из строк и столбцов. Обычно мы называем
такую организованную структуру данных таблицей. pandas – это инструмент,
который мы будем использовать для анализа данных почти в каж­дом разделе
этой книги.
Библиотека pandas была создана Уэсом МакКинни в 2008 году, когда он работал в хедж-фонде AQR. В финансовом мире принято называть табличные данные «панельными данными» (panel data), с которыми не всегда удобно работать,
поскольку они часто являются громоздкими и неповоротливыми, как панды.

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

6.3. pandas работает с табличными данными
Существует множество форматов данных, таких как XML, JSON, CSV, Parquet,
текст и многие другие. Библиотека pandas умеет считывать данные, записанные в различных форматах, и всегда преобразовывает их в табличную форму.
Библиотека pandas создана только для анализа этой прямоугольной, обманчиво нормальной концепции хранения данных. pandas не является подходящей библиотекой для обработки данных более чем в двух измерениях. Основное внимание уделяется данным, которые являются одномерными или
двумерными.

6.4. Объекты DataFrame и Series
Объекты DataFrame и Series – это два основных объекта pandas, которые мы
будем использовать в этой книге.

112



Инструменты

Объект Series – одно измерение данных. Его называют серией. Он аналогичен одному столбцу данных или одномерному массиву.
Объект DataFrame – это двумерная структура, таблица, похожая на элект­
ронную таблицу Excel со строками и столбцами. Для простоты эту таблицу
называют датафреймом. В отличие от библиотеки NumPy, которая требует,
чтобы все элементы в массиве были одного и того же типа, каждый столбец
датафрейма (объект Series) может иметь отдельный тип, то есть в столбцах
могут быть записаны строковые значения, даты, целые числа, числа с плавающей точкой. Датафрейм имеет две размерности, ось строк 0 (двигаемся по
датафрейму вертикально) и ось столбцов 1 (двигаемся по датафрейму горизонтально). У датафрейма есть индекс, как правило, это последовательность
целых чисел, начинающаяся с 0. Значения индекса не ограничиваются целыми
значениями. Строки – это распространенный тип, который используется в индексе и обеспечивает более описательные метки.

Рис. 15 Структура датафрейма pandas

При работе с различными методами и функциями pandas нам нужно будет
указать ось, к которой будет применен метод или функция. Поясним на конкретном примере.
Давайте создадим датафрейм из двух столбцов.
# импортируем библиотеки pandas и numpy
import pandas as pd
import numpy as np
# создаем датафрейм
df = pd.DataFrame({'Empl': [10, 20],
'Age': [30, 40]})
df

6.6. Кратко о типах данных  113
Теперь с помощью метода .mean() вычислим среднее построкам и вычислим
среднее по столбцам (указываем axis=0 и axis=1 соответственно).
# вычисляем среднее по строкам
df.mean(axis=0)
Empl
15.0
Age
35.0
dtype: float64
# вычисляем среднее по столбцам
df.mean(axis=1)
0
20.0
1
30.0
dtype: float64

Видим, что в зависимости от выбранной оси мы получаем разные результаты.

6.5. Задачи, выполняемые pandas
В pandas вам будут доступны следующие операции:
 чтение данных;
 доступ к строкам и столбцам;
 фильтрация данных;
 агрегация данных;
 чистка данных;
 изменение формы данных;
 анализ временных рядов;
 визуализация.

6.6. Кратко о типах данных
Ниже приведены наиболее распространенные типы данных, которые часто
применяются в датафреймах:
 boolean – только два возможных логических значения, True и False;
 integer – целые числа без десятичных знаков;
 float – числа с десятичными знаками (числа с плавающей точкой);
 object – почти всегда строки, но технически может содержать любой объект Python;
 datetime – конкретная дата и время с точностью до наносекунды.
Тип данных object является наиболее запутанным и заслуживает более подробного обсуждения. Каждое значение в столбце типа object может быть любым объектом Python. Столбцы типа object могут содержать целые числа, числа с плавающей запятой или даже структуры данных, такие как списки или
словари. Что угодно может содержаться в столбцах объектов. Но почти всегда
столбцы с типом данных object содержат только строки. Когда вы видите столбец с типом данных object, вы должны ожидать, что значения будут строками.
Если у вас есть строки в значениях вашего столбца, тип данных будет object, но
при этом вам не гарантируется, что все значения будут строками.

114



Инструменты

До выпуска pandas версии 1.0 не существовало выделенного типа данных
string. Это было огромным ограничением и вызывало множество проблем.
В pandas по-прежнему есть тип данных object, который может хранить строки.
С добавлением типа данных string мы гарантируем, что каждое значение будет строкой в столбце со строковым типом данных. Этот новый тип данных все
еще помечен как «экспериментальный» в документации pandas, поэтому пока
лучше не использовать его для серьезной работы. Есть много ошибок, которые
необходимо исправить и отрегулировать поведение, прежде чем он будет готов к использованию. Поэтому в этой книге по-прежнему будет использоваться тип object для столбцов, содержащих строки.

6.7. Представление пропусков
Наборы данных часто содержат пропущенные значения, и для их идентификации требуется некоторое представление. Pandas использует объекты NaN и NaT
для представления пропусков:
 NaN (Not a Number) – «не является числом»;
 NaT (Not a Time) – «не является временем».
Представление пропущенного значения зависит от типа данных в столбце:
 boolean – нет представления пропуска;
 integer – нет представления пропуска;
 float – NaN;
 object – NaN;
 datetime – NaT.
Знание того, что столбец является либо boolean, либо integer, гарантирует, что
в этом столбце нет пропусков, поскольку pandas не допускает их. Если, например, вы хотите поместить пропущенное значение в столбец типа boolean или
integer, pandas преобразует столбец в столбец типа float. Это связано с тем, что
столбец типа float может содержать пропуски. Когда логические значения преобразуются в числа с плавающей запятой, False становится равным 0, а True становится равным 1.
В pandas 1.0 теперь стали доступны новые типы данных: тип integer, допус­
кающий значения NULL, тип boolean, допускающий значения NULL, тип float, допускающий значения NULL. Это совершенно новые типы данных, отличающиеся от исходных типов integer, boolean, float, и их поведение немного отличается. Основное
отличие состоит в том, что они имеют представление пропущенного значения.
Раньше библиотека pandas использовала библиотеку Numpy для главного
представления пропуска в виде NaN, которое продолжает существовать. С выпуском версии 1.0 разработчики pandas создали собственное представление
пропуска NA. Это новое и экспериментальное дополнение, поэтому его поведение может измениться.
Главная рекомендация для pandas 1.0 заключается в том, чтобы c большой
осторожностью использовать новый тип string, тип integer, допускающий значение NULL, тип boolean, допускающий значение NULL, а также NA, пока их не доработают. Они все еще являются экспериментальными, и их поведение может
измениться.

6.9. Подробно знакомимся с типами данных  115

6.8. Какую версию pandas использовать?
Библиотека pandas находится в постоянном развитии и регулярно выпускает
новые версии. В настоящее время pandas находится в основной версии 1, которая была выпущена в январе 2020 года. До основной версии 1 pandas была
в версии 0. Библиотеки Python используют форму abc для нумерации версий,
где a представляет номер мажорной версии. Он увеличивается всякий раз, когда происходят серьезные изменения, некоторые из которых несовместимы
с предыдущими версиями. b представляет номер минорной версии и увеличивается c внесением небольших изменений и улучшений, совместимых с предыдущими версиями. c представляет номер микроверсии и увеличивается
в основном при исправлении багов.
Часто, говоря о версии pandas, пишут только мажорную и минорную версии, поскольку микроверсия не так уж важна. Обычно в год выходит несколько минорных версий. Чтобы запустить код в этой книге, вам нужно запустить
pandas 1.0 или более позднюю версию.
# смотрим версию
pd.__version__
'1.4.2'

6.9. Подробно знакомимся с типами данных
Прежде чем приступить к работе с данными, полезно подробно изучить типы
данных, доступные в библиотеке pandas. Все значения в серии относятся к одному и тому же типу данных. Точно так же все значения отдельного столбца
датафрейма относятся к одному и тому же типу данных. В этом разделе мы
будем активно использовать метод .astype() для изменения типов данных.

6.9.1. Типы данных для работы с числами и логическими
значениями
Начнем с типов данных, предназначенных для работы с числами и логическими значениями.

6.9.1.1. Тип данных integer (тип для целых чисел, целочисленный тип),
'int64' или 'int32'
Давайте создадим серию, передав в функцию pd.Series() список целочисленных
значений.
# создаем серию целочисленных значений
s_int = pd.Series([10, 35, 130])
s_int
0
10
1
35
2
130
dtype: int64

116



Инструменты

Вывод показывает тип данных для значений серии. В данном случае
речь идет об int64, который формально представляет собой 64-битное целое
число. Этот тип данных унаследован непосредственно от NumPy и позволяет целым числам иметь размер 8, 16, 32 или 64 бита. С помощью 64 бит
мы можем представлять только целые числа от –9223372036854775808 до
9223372036854775807. В NumPy есть функция np.iinfo(), которая возвращает точную информацию о минимальном и максимальном целых числах для
каждого целочисленного типа данных. Нужно просто передать в функцию
нужный тип данных в виде строки.
# выводим диапазон чисел для типа int64
np.iinfo('int64')
iinfo(min=-9223372036854775808, max=9223372036854775807, dtype=int64)

Аналогично мы можем найти диапазон для 8-битовых целочисленных значений.
# выводим диапазон чисел для типа int8
np.iinfo('int8')
iinfo(min=-128, max=127, dtype=int8)

Диапазон чисел для int8 охватывает от –128 до 127, или 256 чисел. Это эквивалентно двойке, возведенной в 8-ю степень.
С помощью метода .astype() сменим тип наших данных на int8.
# сменим тип на int8
s_int.astype('int8')
0
10
1
35
2 -126
dtype: int8

Обратите внимание, что третье значение теперь отображается как –126
вмес­то исходного значения 130. Мы уже знаем, что максимальное 8-битное
целое число равно 127. Библиотека NumPy предполагает, что вы знаете, что
делаете, и не проверяет, что число 130 превышает максимум. Теперь наше число 130 представлено третьим целым числом, которое больше минимального
значения –128 и равно –126.
Тип целочисленного значения по умолчанию будет зависеть от операционной системы, в которой вы работаете. Для 32-разрядных машин Linux, macOS
и Windows используются 32 бита. Для 64-разрядных машин Linux и macOS используются 64 бита. Для 64-разрядных машин Windows будут использоваться
32 бита.

6.9. Подробно знакомимся с типами данных  117

6.9.1.2. Тип данных unsigned integer (тип для целых чисел без знака),
'uint64' или 'uint32'
Целочисленные типы данных по умолчанию делят половину своего диапазона
на отрицательные и положительные целые числа. Можно ограничить ваши целые числа только неотрицательными целыми числами, используя тип unsigned
integer, сокращенно uint. Доступны варианты 8, 16, 32 и 64 бита. Давайте преобразуем исходную серию s_int в тип uint8.
# сменим тип на uint8
s_int.astype('uint8')
0
10
1
35
2
130
dtype: uint8

Последнее значение верно записано как 130, так как диапазон нашего нового типа данных составляет от 0 до 255. Давайте проверим это с помощью
метода .iinfo().
# выводим диапазон чисел для типа uint8
np.iinfo('uint8')
iinfo(min=0, max=255, dtype=uint8)

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

6.9.1.3. Тип данных nullable integer (тип для целых чисел, допускающий
значения NULL), 'Int64'
С выпуском pandas версии 0.24 в конце 2019 года пользователям pandas стал
доступен новый целочисленный тип данных, допускающий значение NULL. Этот
новый тип данных допускает наличие пропусков в столбце целых чисел. Это
отдельный тип данных, отличающийся от обычных целочисленных типов данных. Исходные целочисленные типы данных все еще существуют и не могут
содержать пропуски. Давайте проверим это, попытавшись создать ряд целых
чисел с пропусками. Обратите внимание, что мы используем параметр dtype,
чтобы попытаться установить тип данных int64.
# создаем серию типа int64
pd.Series([10, 35, 130, np.nan], dtype='int64')
ValueError: cannot convert float NaN to integer

118



Инструменты

Если не использовать параметр dtype, то серия будет создана, но при этом
будет задействован более гибкий тип данных float64, который допускает пропущенные значения.
# серии с пропусками будет присвоен тип float64
pd.Series([10, 35, 130, np.nan])
0
10.0
1
35.0
2
130.0
3
NaN
dtype: float64

Целочисленный тип данных, допускающий значения NULL, представлен строковым значением 'Int' (в отличие от 'int'). Важным отличием является первая
заглавная буква I. Доступны те же четыре размера: 8, 16, 32 и 64. Давайте создадим серию целых чисел, допускающих значение NULL, используя строковое
значение Int64.
# создаем серию с типом nullable integer (Int64)
s_nullable_int = pd.Series([10, 35, 130, np.nan],
dtype='Int64')
s_nullable_int
0
10
1
35
2
130
3

dtype: Int64

Пропуск визуально представлен как значение , которое отличается от
значения NaN, когда серия имела тип float64. Библиотека pandas предложила свой
собственный объект NA для представления пропусков, который отличается от
NaN библиотеки numpy. Библиотека pandas преобразует любой пропуск в серии
целых чисел, допускающих значение NULL, в собственный объект NA. Давайте
воспользуемся объектом NA библиотеки pandas непосредственно при создании
серии, чтобы показать, что создается одна и та же серия.
# создаем серию с типом nullable integer (Int64)
pd.Series([10, 35, 130, pd.NA], dtype='Int64')
0
10
1
35
2
130
3

dtype: Int64

Целочисленный тип данных, допускающий значения NULL, помечен как
«экспериментальный», что указывает на то, что его поведение может измениться в будущем. Кроме того, встречаются некоторые ошибки, связанные
с этим типом данных. Здесь можно порекомендовать с осторожностью ис-

6.9. Подробно знакомимся с типами данных  119
пользовать этот тип данных для серьезной работы, пока он не перестанет
быть экспериментальным. Помните, что этот тип доступен только в pandas,
в NumPy этого типа нет.
Целочисленный тип данных, допускающий значения NULL, ведет себя иначе,
чем обычный целочисленный тип данных. При попытке создать серию со значениями, которые не находятся в пределах ее диапазона, будет выброшено исключение вместо попытки вычисления значения, как это было сделано выше.
Это, вероятно, наилучший вариант для предотвращения ошибок.
# создаем серию с типом nullable integer (Int8)
pd.Series([10, 35, 130], dtype='Int8')
TypeError: cannot safely cast non-equivalent int64 to int8

6.9.1.4. Тип данных nullable unsigned integer (тип для целых чисел
без знака, допускающий значения NULL), 'UInt64'
Целочисленный тип без знака, допускающий значения NULL, можно задать с помощью строкового значения 'UInt' (заглавные буквы U и I).
# создаем серию с типом nullable unsigned integer (UInt8)
pd.Series([10, 35, 130, pd.NA], dtype='UInt8')
0
10
1
35
2
130
3

dtype: UInt8

6.9.1.5. Тип данных float (тип для чисел с плавающей точкой), 'float64'
или 'float32'
Столбцы с плавающей точкой содержат числа с десятичными знаками. По умолчанию используется 64-битовый тип float. Это числовой тип данных в NumPy,
который используется для хранения чисел c плавающей точкой двойной точности (double precision). Стандартное значение с плавающей точкой двойной
точности (хранящееся во внутреннем представлении объекта Python типа float)
занимает 8 байт, или 64 бита. Поэтому соответствующий тип в NumPy называется float64. В NumPy есть дополнительные 16-битовый и 32-битовый типы
float. Все типы чисел с плавающей запятой могут содержать пропущенные значения. Давайте создадим серию чисел с плавающей точкой, содержащую один
пропуск, и проверим тип данных.
# создаем серию с типом float64
s_float = pd.Series([5.26, 1234.56789, np.nan])
s_float
0
5.26000
1
1234.56789
2
NaN
dtype: float64

120



Инструменты

Мы можем присвоить серии тип float32, который используется для хранения
чисел c плавающей точкой одинарной точности (single precision).
# присвоим тип float32
s_float.astype('float32')
0
5.260000
1
1234.567871
2
NaN
dtype: float32

Опять с помощью функции np.finfo() получим информацию о типе float. Тип
float32 гарантирует точность 6 значащих цифр, как это можно увидеть с помощью атрибута resolution ниже.
# выведем диапазон чисел и точность для типа float32
np.finfo('float32')
finfo(resolution=1e-06, min=-3.4028235e+38, max=3.4028235e+38, dtype=float32)

Тип float16, который используется для хранения чисел c плавающей точкой
половинной точности (half precision), гарантирует только 3 цифры точности.
# выведем диапазон чисел и точность для типа float16
np.finfo('float16')
finfo(resolution=0.001, min=-6.55040e+04, max=6.55040e+04, dtype=float16)

Переход к этому типу данных существенно изменяет второе фактическое
значение из-за его ограниченной точности.
# присвоим тип float16
s_float.astype('float16')
0
5.261719
1
1235.000000
2
NaN
dtype: float16

Мы можем изменить тип с float на integer и наоборот. Ниже мы попытаемся
перейти от float64 к int64. Сделать это не удастся, так как обычный целочисленный тип данных не допускает пропущенных значений.
# переведем из float64 в int64
s_float.astype('int64')
IntCastingNaNError: Cannot convert non-finite values (NA or inf) to integer

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

6.9. Подробно знакомимся с типами данных  121
# удалим пропуски и переведем из float64 в int64
s_float.dropna().astype('int64')
0
5
1
1234
dtype: int64

Поведение типа nullable integer отличается. Он не позволяет выполнить преобразование, если есть какие-либо числа с десятичными знаками.
# присвоим тип nullable integer (Int64)
s_float.astype('Int64')
TypeError: cannot safely cast non-equivalent float64 to int64

Если удалить десятичные знаки (с помощью округления), то преобразование в тип nullable integer станет возможным.
# округлим и присвоим тип nullable integer (Int64)
s_float.round(0).astype('Int64')
0
5
1
1235
2

dtype: Int64

Теперь выполним преобразование из типа int в тип float.
# преобразовываем из типа int64 в тип float64
s_int.astype('float64')
0
10.0
1
35.0
2
130.0
dtype: float64

Преобразование типа nullable integer в тип float тоже возможно. Поскольку
тип float является типом данных NumPy, он использует для представления пропусков значение NaN вместо pd.NA.
# преобразовываем из типа nullable integer (Int64)
# в тип float64
s_nullable_int.astype('float64')
0
10.0
1
35.0
2
130.0
3
NaN
dtype: float64

122



Инструменты

6.9.1.6. Тип данных nullable float (тип для чисел с плавающей точкой,
допускающий значения NULL), 'Float64'
С выходом pandas 1.2 (декабрь 2020 г.) в библиотеке появился тип nullable float.
Его можно задать с помощью строкового значения 'Float' (заглавная F), за которым следует размер в битах – 16 или 32. Сейчас мы выполним преобразование из типа float в тип nullable float.
# преобразовываем из типа float
# в тип nullable float (Float64)
nullable_float = s_float.astype('Float64')
nullable_float
0
5.26
1
1234.56789
2

dtype: Float64

6.9.1.7. Тип данных boolean (логический тип, булев тип), 'bool'
Логические значения имеют один 8-битовый тип данных в Numpy. Давайте
создадим серию логических значений.
# создадим серию логических значений
s_bool = pd.Series([True, False])
s_bool
0
True
1
False
dtype: bool

Мы можем выполнить преобразование из типов int и float в тип bool и наоборот. Единственное значение, которое будет преобразовано в False, – это 0. Все
остальные значения будут преобразованы в True. Используйте строковое значение 'bool' для преобразования в логический тип. Давайте создадим серию
с типом данных integer и выполним преобразование в логический тип.
# создаем серию с типом integer
s = pd.Series([0, 1, 59, -35])
# преобразовываем в тип boolean
s.astype('bool')
0
False
1
True
2
True
3
True
dtype: bool

Давайте создадим серию с типом данных float и выполним преобразование
в логический тип. Здесь тоже только значение 0 будет преобразовано в False.
Любое другое значение оценивается как True.

6.9. Подробно знакомимся с типами данных  123
# создаем серию с типом float
s = pd.Series([0, 0.0001, -3.99])
# преобразовываем в тип boolean
s.astype('bool')
0
False
1
True
2
True
dtype: bool

Преобразование серии логических значений в серию с целыми числами или
числами с плавающей точкой превратит все значения True в значения 1, а все
значения False – в значения 0.
# преобразуем из типа boolean в тип integer
s_bool.astype('int64')
0
1
1
0
dtype: int64

Использование типа int64 для хранения логического значения является излишним. Для экономии памяти можно воспользоваться наименьшим целочисленным типом, int8 (или uint8).

6.9.1.8. Тип данных nullable boolean (логический тип, допускающий
значения NULL), 'Boolean'
С выпуском pandas 1.0 для поддержки пропусков стал доступен новый логический тип, допускающий значения NULL. Тип nullable boolean есть только в pandas,
исходный тип boolean по-прежнему существует, но не поддерживает пропуски.
Давайте убедимся, что исходный тип boolean не может содержать пропуски.
# создаем серию с типом boolean
s = pd.Series([True, False, np.nan], dtype='bool')
s
0
True
1
False
2
True
dtype: bool

Выполнение кода не приводит к ошибке, вместо этого объект nan библиотеки
NumPy превращается в значение True. Это соответствует правилу, согласно которому каждое ненулевое значение и пропуск оцениваются как значение True
для логических значений.
Если взять серию логических значений и присвоить одному из значений
значение nan, то вся серия получит тип object.
# присвоение значения nan одному из логических
# значений дает серию с типом object
s.loc[0] = np.nan
s

124



Инструменты

0
NaN
1
False
2
True
dtype: object

Новый тип nullable boolean использует строковое значение 'boolean' вместо
'bool'. Давайте создадим серию с типом nullable boolean.
# создаем серию с типом nullable boolean
s = pd.Series([True, False, np.nan], dtype='boolean')
s
0
True
1
False
2

dtype: boolean

Выполнение арифметических операций с серией может изменить тип данных
в полученной серии. Деление всегда преобразует серию с данными типа integer
в серию с данными типа float, даже если результатом являются целые числа.
# создаем серию с типом integer
s = pd.Series([-15, 45])
s
0 -15
1
45
dtype: int64
# выполняем деление, получаем
# серию с типом float
s / 15
0 -1.0
1
3.0
dtype: float64

Используя деление с округлением до целого значения вниз (floor division),
мы получим результат в виде целого значения, пока делитель является целым
числом.
# используем деление с округлением
# до целого значения вниз
s // 77
0 -1
1
0
dtype: int64

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

6.9. Подробно знакомимся с типами данных  125
# выполняем умножение
s * 4.4
0
-66.0
1
198.0
dtype: float64

Все преобразования типов данных в этом разделе были выполнены с использованием строковых значений типа 'int8'. Существует альтернативный подход.
Вместо строкового значения вы можете использовать сам фактический объект,
доступный непосредственно из NumPy или pandas. Например, мы можем использовать np.int8 вместо строкового значения 'int8', чтобы указать тип данных.
# задаем тип int8 так
pd.Series([10, 50]).astype(np.int8)
0
10
1
50
dtype: int8
# а еще можно так
pd.Series([10, 50]).astype('int8')
0
10
1
50
dtype: int8

Все типы данных NumPy имеют то же самое имя, что и их аналоги в виде
строковых значений. Однако для типов данных pandas это не выполняется.
Типы данных pandas заканчиваются словом 'Dtype'. Например, для преобразования в 32-битовый тип nullable integer вы можете использовать pd.Int32Dtype().
# создаем серию с типом nullable integer (Int32)
pd.Series([10, 50, np.nan]).astype(pd.Int32Dtype())
0
10
1
50
2

dtype: Int32

Для преобразования в 64-битовый тип nullable float вы можете использовать

pd.Float64Dtype().

# создаем серию с типом nullable float (Float64)
pd.Series([7.3, 5.8, np.nan], dtype=pd.Float64Dtype())
0
7.3
1
5.8
2

dtype: Float64

Ниже приводится таблица типов данных для работы с числами и логическими
значениями, которые унаследованы библиотекой pandas от NumPy, и таблица

126



Инструменты

типов данных для работы с числами и логическими значениями, имеющихся
только в pandas.
Таблица 6 Типы данных для работы с числами и логическими значениями, унаследованные
библиотекой pandas от NumPy
Название

Короткое название в виде строкового значения
по умолчанию

Размеры
(количество битов)

Boolean

bool

8

Integer

int64 или int32

8, 16, 32, 64

Unsigned Integer

uint64 или uint32

8, 16, 32, 64

Float

float64

16, 32, 64

Пропуски доступны в типе float в виде np.nan.
Таблица 7 Типы данных для работы с числами и логическими значениями, имеющиеся
только в pandas
Название

Короткое название в виде
строкового значения по
умолчанию

Название объекта
pandas по
умолчанию

Размеры
(количество битов)

Nullable Boolean

Boolean

pd.BooleanDtype()

8

Nullable Integer

Int64

pd.Int64Dtype()

8, 16, 32, 64

Nullable Unsigned
Integer

UInt64

pd.UInt64Dtype()

8, 16, 32, 64

Nullable Float

Float64

pd.Float64Dtype()

32, 64

Пропуски доступны во всех типах в виде pd.NA.
Теперь разберем типы данных для работы со строками.

6.9.2. Типы данных для работы со строками
Теперь разберем типы данных для работы со строками.

6.9.2.1. Тип данных object (объектный тип), 'object'
До выхода версии 1.0 у pandas не было специального строкового типа данных.
Вместо этого использовался тип данных object для хранения строк. Как упоминалось ранее, тип данных object не имеет ограничений относительно того,
какой объект Python может быть внутри него. По сути, это универсальное средство для любого элемента, который вы хотите разместить в датафрейме, который не принадлежит к другим конкретным типам данных.
У типа данных object нет определенного размера в битах. Не существует
object64, есть только один тип данных object. Каждый элемент может быть разного типа и, следовательно, разного размера.
Хотя тип данных object может содержать любой объект Python, в основном он
используется для хранения строк. Давайте создадим серию с парой строковых
значений.

6.9. Подробно знакомимся с типами данных  127
# создаем серию со строковыми значениями
s_object = pd.Series(['some', 'strings'])
s_object
0
some
1
strings
dtype: object

Как видно из вывода, тип данных – 'object'. Если проверить тип, мы увидим в выводе dtype('O'). Тип данных object тоже унаследован непосредственно
от NumPy, в которой используется обозначение 'O' вместо полного названия.
# проверим тип
s_object.dtype
dtype('O')

Поскольку тип 'object' является наиболее гибким типом, серии с любым типом данных можно присвоить тип 'object'. Ниже мы присвоим серии с целыми
числами тип 'object'.
# присвоим серии с целыми числами тип object
s = pd.Series([5, 10])
s.astype('object')
0
5
1
10
dtype: object

Однако сами значения по-прежнему являются целыми числами. Мы убедимся в этом, найдя тип первого значения.
# значения – по-прежнему целые числа
type(s.loc[0])
numpy.int64

Серия с типом object может содержать все, что угодно. Серия ниже включает
в себя список, логическое значение, строку, число с плавающей точкой и словарь.
# серия с типом object может содержать все, что угодно
garbage_series = pd.Series([[1,2], True, 'some string',
4.5, {'key': 'value'}])
garbage_series
0
[1, 2]
1
True
2
some string
3
4.5
4
{'key': 'value'}
dtype: object

128



Инструменты

# элементом серии с типом object
# может быть все, что угодно
print(type(garbage_series.loc[0]))
print(type(garbage_series.loc[1]))
print(type(garbage_series.loc[2]))
print(type(garbage_series.loc[3]))
print(type(garbage_series.loc[4]))


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

6.9.2.2. Тип данных Categorical (категориальный тип), 'category'
Теперь познакомимся с категориальным типом данных, который есть
в pandas и отсутствует в NumPy. Категориальный тип данных часто используется, когда столбец данных имеет известные, ограниченные и дискретные
значения.
Давайте загрузим данные и отберем для манипуляций столбец job_position.
# записываем CSV-файл в объект DataFrame
credit = pd.read_csv('Data/credit_train.csv',
encoding='cp1251',
decimal=',',
sep=';')
# выводим первые 5 наблюдений датафрейма
credit.head()

# смотрим частоты категорий job_position
job_position = credit['job_position']
job_position.value_counts()
SPC
UMN
BIS
PNA
DIR
ATP
WRK

134680
17674
5591
4107
3750
2791
656

6.9. Подробно знакомимся с типами данных  129
NOR
537
WOI
352
INP
241
BIU
126
WRP
110
PNI
65
PNV
40
PNS
12
HSK
8
INV
5
ONB
1
Name: job_position, dtype: int64

Общее количество категорий должно быть известно. Вероятность появления новых категорий в будущем должна быть низкой. Общее количество категорий ограничено и намного меньше количества наблюдений. Значения должны быть дискретными. Если эти условия соблюдаются, рассматриваемый
столбец можно перевести в категориальный тип.
Самый простой способ присвоить серии категориальный тип – передать
строковое значение 'category' методу .astype().
# присваиваем тип Categorical
job_position_cat = job_position.astype('category')
job_position_cat.head()
0
UMN
1
UMN
2
SPC
3
SPC
4
SPC
Name: job_position, dtype: category
Categories (18, object): ['ATP', 'BIS', 'BIU', 'DIR', ..., 'UMN', 'WOI', 'WRK', 'WRP']

Убедимся в том, что серии просвоен тип Categorical.
# смотрим тип серии
job_position_cat.dtype
CategoricalDtype(categories=['ATP', 'BIS', 'BIU', 'DIR', 'HSK', 'INP', 'INV', 'NOR',
'ONB', 'PNA', 'PNI', 'PNS', 'PNV', 'SPC', 'UMN', 'WOI',
'WRK', 'WRP'],
, ordered=False)

Чем полезен тип Categorical?
Категориальные данные хранятся намного эффективнее, чем объектные.
Каждое уникальное значение в столбце типа Categorical сохраняется один раз
независимо от того, сколько раз оно повторяется в серии, и каждое из уникальных значений имеет целочисленный код, который на него ссылается. Именно
эти целые числа хранятся в памяти для представления данных.
Столбцы типа object хранят каждое значение в уникальной локации памяти. Например, строка 'SPC' появляется более 134 000 раз в серии job_position_cat.
Каждая из этих строк хранится в уникальной локации памяти. Использование

130



Инструменты

целых чисел для представления категорий может сэкономить огромное количество памяти.
Давайте создадим упрощенный пример, чтобы показать, как pandas хранит
категориальные данные внутри, используя списки Python. В этом примере
у нас будет три уникальных cтроковых значения. Они сохраняются ровно один
раз в списке cats ниже. Фактические данные хранятся в списке значений, содержащем значения 0, 1 и 2.
# создаем 2 списка
cats = ['Python', 'Java', 'Scala']
vals = [1, 1, 0, 2, 0, 1, 2, 2, 1, 2, 1]

Список cats, по сути, работает как сопоставление целочисленной локации
со строковым значением. Целое число 0 соответствует 'Python', 1 – 'Java' и 2 –
'Scala'. Мы можем преобразовать каждое значение в списке vals в соответствующую категорию, используя генератор списков.
# выполняем сопоставление
[cats[val] for val in vals]
['Java',
'Java',
'Python',
'Scala',
'Python',
'Java',
'Scala',
'Scala',
'Java',
'Scala',
'Java']

Уникальную последовательность категорий можно получить с помощью
средства доступа (аксессора) .cat и атрибута categories.
# выведем уникальный список категорий
job_position_cat.cat.categories
Index(['ATP', 'BIS', 'BIU', 'DIR', 'HSK', 'INP', 'INV', 'NOR', 'ONB', 'PNA',
'PNI', 'PNS', 'PNV', 'SPC', 'UMN', 'WOI', 'WRK', 'WRP'],
dtype='object')

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

# смотрим целочисленные коды
job_position_cat.cat.codes.head()
0
1
2
3

14
14
13
13

6.9. Подробно знакомимся с типами данных  131
4
13
dtype: int8

Одним из самых больших преимуществ использования категориальных
столбцов является экономия памяти. Вместо использования строки под каж­
дое значение используется целочисленный код. Целые числа занимают значительно меньше места, чем строки. Кроме того, библиотека pandas использует наименьший размер целочисленного типа для хранения кодов. Например,
если категорий меньше 128, используется int8.
С помощью метода .memory_usage() можно выяснить, сколько памяти позволяет
сэкономить использование типа Categorical. Чтобы получить точные данные об
объеме использованной памяти, для параметра deep нужно задать значение True.
# объем памяти для хранения серии типа object
orig_mem = job_position.memory_usage(deep=True)
orig_mem
10244888
# объем памяти для хранения серии типа Categorical
cat_mem = job_position_cat.memory_usage(deep=True)
cat_mem
172510

Сравним скорость выполнения операции приравнивания для обеих серий.
# выполним операцию приравнивания
# для серии типа object
%timeit -n 5 -r 2 job_position == 'SPC'
9.24 ms ± 266 µs per loop (mean ± std. dev. of 2 runs, 5 loops each)
# выполним операцию приравнивания
# для серии типа Categorical
%timeit -n 5 -r 2 job_position_cat == 'SPC'
The slowest run took 4.77 times longer than the fastest. This could mean that an intermediate
result is being cached.
231 µs ± 151 µs per loop (mean ± std. dev. of 2 runs, 5 loops each)

Любой столбец, независимо от его типа данных, может быть преобразован
в категориальный. Целые числа – это основной нестроковый тип данных, который используется для представления категориальных данных. Вот несколько примеров целочисленных категориальных данных:
 рейтинг фильма/отеля/ресторана с учетом того, что диапазон известен,
например целые числа (1–5);
 почтовые индексы определенного города;
 категория силы урагана (1–5).
Давайте присвоим серии с целочисленными значениями тип Categorical.

132



Инструменты

# присвоим серии с целочисленными значениями тип Categorical
credit_month_cat = credit['credit_month'].astype('category')
credit_month_cat.head(10)
0
10
1
6
2
12
3
12
4
10
5
10
6
6
7
10
8
12
9
10
Name: credit_month, dtype: category
Categories (31, int64): [3, 4, 5, 6, ..., 30, 31, 32, 36]

6.9.2.3. Тип данных string (строковый тип), 'string'
С выходом версии 1.0 в pandas стал доступен новый тип данных string. Этот тип
есть только в pandas и отсутствует в NumPy. Он может содержать только строки и пропуски. Опять же, используйте его с осторожностью, пока он является
экспериментальным.
Для создания серии с этим типом мы можем передать строковое значение 'string' в метод .astype(). Вы также можете напрямую использовать объект
pandas pd.StringDtype. Обе серии будут идентичны.
# создаем серию с типом string
s_string = pd.Series(['Python', 'Java', 'Scala', pd.NA],
dtype='string')
s_string
0
Python
1
Java
2
Scala
3

dtype: string
# создаем серию с типом string
s_string = pd.Series(['Python', 'Java', 'Scala', pd.NA],
dtype=pd.StringDtype())
s_string
0
Python
1
Java
2
Scala
3

dtype: string

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

6.9. Подробно знакомимся с типами данных  133
# серия с типом string может содержать только строки и пропуски
garbage_series = pd.Series([[1,2], True, 'some string', 4.5,
{'key': 'value'}])
garbage_series = garbage_series.astype('string')
garbage_series
0
[1, 2]
1
True
2
some string
3
4.5
4
{'key': 'value'}
dtype: object
# значения уже будут строками
print(type(garbage_series.loc[0]))
print(type(garbage_series.loc[1]))
print(type(garbage_series.loc[2]))
print(type(garbage_series.loc[3]))
print(type(garbage_series.loc[4]))


Вместе с тем функциональность обоих типов данных будет очень похожей.
Здесь мы применим средство доступа (аксессора) .str, чтобы сделать строки
прописными.
# сделаем буквы заглавными
s_string.str.upper()
0
PYTHON
1
JAVA
2
SCALA
3

dtype: string

Строки, полностью состоящие из чисел, можно преобразовать либо в целое
число, либо в число с плавающей точкой. Давайте создадим серию строк, которые выглядят точно так же, как числа с плавающей точкой. Библиотека pandas
всегда использует тип object в качестве типа данных по умолчанию для строк.
# создаем серию со строками, выглядящими как числа
s = pd.Series(['4.5', '3.19'])
s
0
4.5
1
3.19
dtype: object

134



Инструменты

Кавычки для строк отсутствуют в выводе, поэтому строки кажутся значениями с плавающей точкой. Но можно заметить, что десятичные дроби не
выровнены и каждое значение имеет разное количество цифр после запятой.
Давайте создадим фактический столбец типа float, чтобы вы могли увидеть разницу в визуальном отображении. Обратите внимание, что десятичные дроби
всегда будут выровнены.
# переводим в тип float64
s.astype('float64')
0
4.50
1
3.19
dtype: float64

Теперь представьте, у вас есть серия строковых значений, некоторые из которых могут быть преобразованы в числовые, а другие – нет. В этой ситуации
невозможно использовать метод .astype().
# создаем серию со строковыми значениями
s = pd.Series(['4.5', '3.19', 'NO ANSWER'])
s
0
4.5
1
3.19
2
NO ANSWER
dtype: object
# переводим в тип float64
s.astype('float64')
ValueError: could not convert string to float: 'NO ANSWER'

Вместо этого нужно обратиться к функции to_numeric(), которая работает аналогично методу .astype(), но при этом у нее есть дополнительная возможность
принудительно выполнить преобразование. Это можно сделать, задав для параметра errors значение 'coerce'. Любое значение, которое нельзя преобразовать, будет записано как пропуск.
# выполняем преобразование в тип float64
pd.to_numeric(s, errors='coerce')
0
4.50
1
3.19
2
NaN
dtype: float64

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

6.9. Подробно знакомимся с типами данных  135
# серии с целыми числами присваиваем тип object
# с помощь строкового значения str
s = pd.Series([10, 20, 99])
s.astype('str')
0
10
1
20
2
99
dtype: object

С помощью атрибута values, который возвращает массив NumPy, проверим,
являются ли наши значения строками.
# проверим, являются ли наши значения строками
s.astype('str').values
array(['10', '20', '99'], dtype=object)

Мы можем воспользоваться строковым значением 'string' для преобразования в новый тип string.
# преобразовываем в тип string
s.astype('string')
0
10
1
20
2
99
dtype: string

Ниже приводится таблица типов данных для работы со строками.
Таблица 8 Типы данных Object, Categorical и String
Название

Короткое название в виде
строкового значения по
умолчанию

Размеры
(количество битов)

Замечания

Object

object
str

Любой

Может содержать
любой питоновский объект

String

string

Любой

Может содержать
только строки

Categorical

category

Наименьший
по размеру тип
Integer, позволяющий хранить
все имеющиеся
категории

136



Инструменты

6.10. Чтение данных
Функция pd.read_csv() может считывать данные, хранящиеся в виде обычного
текста, разделенного разделителем. По умолчанию разделителем является запятая. Ниже приведены ее основные параметры.

Обратите внимание, что параметр squeeze, использующийся для превращения датафрейма с одним столбцом в серию (актуально при работе с данными,
представляющими временной ряд), объявлен устаревшим. Теперь к функции
нужно будет добавить метод .squeeze('columns').
# загружаем ежемесячные данные
# о продажах автомобилей
cars = pd.read_csv('Data/monthly_car_sales.csv',
header=0,
index_col=0,
squeeze=True,
parse_dates=True)
cars.head()
# загружаем ежемесячные данные
# о продажах автомобилей
cars = pd.read_csv('Data/monthly_car_sales.csv',
header=0,
index_col=0,
parse_dates=True).squeeze('columns')
cars.head()
Month
1960-01-01
1960-02-01
1960-03-01
1960-04-01
1960-05-01
Name: Sales,

6550
8728
12026
14395
14587
dtype: int64

6.11. Получение общей информации о датафрейме  137
Давайте с помощью функции pd.read_csv() прочитаем общедоступные данные об использовании велосипедов в городе Чикаго в датайфрейм pandas
с именем bikes.
По каждому наблюдению (поездке) фиксируются следующие переменные
(характеристики):
 количественная переменная Пол [gender];
 переменная даты и времени Дата и время начала поездки [starttime];
 переменная даты и времени Дата и время конца поездки [stoptime];
 количественная переменная Продолжительность поездки [tripduration];
 категориальная переменная Название станции – начала поездки
[from_station_name];
 категориальная переменная Название станции – конца поездки
[to_station_name];
 количественная переменная Емкость в стартовой точке [start_capacity];
 количественная переменная Емкость в конечной точке [end_capacity];
 количественная переменная Температура [temperature];
 количественная переменная Скорость ветра [wind_speed];
 категориальная переменная Тип погодного явления во время поездки [events].
С помощью метода .head() выведем первые 3 наблюдения.
# загружаем данные
bikes = pd.read_csv('Data/bikes.csv')
bikes.head(3)

Последняя строка блока кода часто будет заканчиваться методом .head().
По умолчанию этот метод возвращает первые пять строк DataFrame или Series.
Цель этого метода – ограничить вывод, чтобы он легко умещался на экране
или странице книги. Если метод .head() не используется, то pandas по умолчанию отображает первые и последние 5 строк данных (или все строки, если
DataFrame содержит 60 строк или меньше). Чтобы еще больше сократить вывод
(для экономии места на экране), методу .head() можно передать целое число
(обычно 3). Это целое число определяет количество возвращаемых строк.

6.11. Получение общей информации о датафрейме
С помощью свойства shape выведем информацию о количестве наблюдений
и количестве переменных.
# смотрим количество наблюдений
# и количество переменных
print(bikes.shape)
(50089, 11)

138



Инструменты

С помощью функции len() выведем информацию о количестве наблюдений.
# смотрим количество наблюдений
print(len(bikes))
50089

С помощью свойства dtypes выведем информацию о типе данных.
# смотрим типы данных
bikes.dtypes
gender
starttime
stoptime
tripduration
from_station_name
start_capacity
to_station_name
end_capacity
temperature
wind_speed
events
dtype: object

object
object
object
int64
object
float64
object
float64
float64
float64
object

По умолчанию pandas читает столбцы, содержащие строки, как столбцы
типа object.
Из визуализации датафрейма видно, что столбцы starttime и stoptime являются датой и временем. Однако результаты выше показывают, что переменные имеют тип object. К сожалению, функция pd.read_csv() не считывает эти
столбцы автоматически как дату и время. Она требует, чтобы вы передали
список столбцов, которые являются datetime, параметру parse_dates, иначе функция будет считывать эти переменные как строки. Давайте перечитаем данные,
используя параметр parse_dates.
# заново читаем данные, парсим даты
bikes = pd.read_csv('Data/bikes.csv',
parse_dates=['starttime', 'stoptime'])
bikes.dtypes.head()
gender
starttime
stoptime
tripduration
from_station_name
dtype: object

object
datetime64[ns]
datetime64[ns]
int64
object

С помощью метода .info() выведем информацию о типе данных и количест­
ве непропущенных наблюдений для каждой переменной.
# выведем информацию о типе данных и количестве
# непропущенных наблюдений для каждой переменной

6.12. Изменение настроек вывода с помощью функции get_options()  139
bikes.info()

RangeIndex: 50089 entries, 0 to 50088
Data columns (total 11 columns):
# Column
Non-Null Count
--- ------------------0 gender
50089 non-null
1 starttime
50089 non-null
2 stoptime
50089 non-null
3 tripduration
50089 non-null
4 from_station_name 50089 non-null
5 start_capacity
50083 non-null
6 to_station_name
50089 non-null
7 end_capacity
50077 non-null
8 temperature
50089 non-null
9 wind_speed
50089 non-null
10 events
50089 non-null
dtypes: float64(4), int64(1), object(6)
memory usage: 4.2+ MB

Dtype
----object
object
object
int64
object
float64
object
float64
float64
float64
object

С помощью свойства columns можно вывести информацию об именах столбцов.
# выведем имена столбцов
print(bikes.columns.tolist())
['gender', 'starttime', 'stoptime', 'tripduration', 'from_station_name', 'start_capacity',
'to_station_name', 'end_capacity', 'temperature', 'wind_speed', 'events']

6.12. Изменение настроек вывода с помощью функции
get_options()
С помощью функции get_options() можно настроить максимальное количество
отображаемых столбцов, максимальное количество отображаемых строк, максимальную ширину столбца.
# максимальное количество столбцов
pd.get_option('display.max_columns')
20
# максимальное количество строк
pd.get_option('display.max_rows')
60
# максимальная ширина столбца
pd.get_option('display.max_colwidth')
50
# задаем новые настройки
pd.set_option('display.max_columns', 30,
'display.max_rows', 100)

140



Инструменты

6.13. Знакомство с индексаторами [], loc и iloc
Библиотека pandas предлагает индексаторы [], loc и iloc для выбора подмножеств данных:
 df[]
 df.loc[]
 df.iloc[]
Одной из самых распространенных ошибок при использовании loc и iloc является добавление к ним круглых скобок вместо квадратных. Одна из основных причин этой ошибки заключается в том, что loc и iloc кажутся методами,
а все методы вызываются в круглых скобках. И loc, и iloc не являются методами, но доступ к ним осуществляется так же, как и к методам, через точечную
нотацию, что приводит к ошибке.
В Python квадратные скобки являются универсальным оператором для выбора подмножеств данных независимо от типа объекта. Квадратные скобки
выбирают подмножества списков, строк и выбирают одно значение в словаре.
Массивы Numpy используют оператор квадратных скобок для выбора подмножества. Помните, что если вы делаете выбор подмножества, скорее всего, вам
нужны квадратные, а не круглые скобки.
Давайте отберем один столбец.
# отберем один столбец
bikes['gender']
0
1
2
3
4
50084
50085
50086
50087
50088
Name:

Male
Male
Male
Male
Male
...
Male
Male
Male
Female
Male
gender, Length: 50089, dtype: object

Давайте извлечем несколько столбцов, передав индексатору [] список. Обратите внимание, что внутренние квадратные скобки мы используем для спис­ка,
а внешние квадратные скобки – для отбора подмножества.
# извлекаем несколько столбцов, передав
# индексатору [] список
bikes[['gender', 'tripduration']]

6.13. Знакомство с индексаторами [], loc и iloc  141

Индексатор loc в первую очередь выбирает подмножества по меткам строк
и столбцов. Он также делает выбор с помощью логического отбора.
В loc мы можем передать:
 отдельную метку;
 список меток;
 срез с метками;
 булеву серию.
Давайте отберем первые две строки в столбцах gender и tripduration. Для этого передаем в индексатор loc список меток строк (в нашем случае они являются целочисленными), а затем список имен столбцов.
# отбираем первые две строки столбцов
# start_capacity и tripduration, передав
# в loc список строк, список столбцов
bikes.loc[[0, 1], ['start_capacity', 'tripduration']]

В Python существует нотация среза, которая используется для выбора подмножеств из некоторых основных объектов Python, таких как списки, кортежи и строки. Нотация среза всегда состоит из трех компонентов – start (стартовоезначение), stop (конечное значение) и step (шаг). Синтаксически каждый компонент отделяется двоеточием следующим образом: start:stop:step.
Все компоненты нотации среза являются необязательными, и их необязательно включать. У каждого компонента есть значение по умолчанию,
если оно не включено в нотацию. У компонента start есть стартовое значение, у компонента stop – последнее значение, компонент step задает размер
шага 1. По сути, работаем как с диапазоном, который всегда задается внутри
квадратных скобок.
Давайте отберем первые четыре строки в столбцах c gender по tripduration,
передав в loc диапазон строк и диапазон столбцов. Обратите внимание, как мы

142



Инструменты

задали диапазон строк 0:3, это эквивалентно 0:3:1, потому что у step всегда есть
значение по умолчанию 1 и мы можем опустить его 0:3:1. Аналогично с диапазоном столбцов.
# отбираем первые четыре строки столбцов
# с gender по tripduration, передав
# в loc диапазон строк, диапазон столбцов
bikes.loc[0:3, 'gender':'tripduration']

Давайте отберем каждую 2-ю строку каждого 2-го столбца, передав в loc диа­
пазон строк и диапазон столбцов. Обратите внимание, как мы задали диапазон строк 0::2, это эквивалентно 0:50088:2, потому что у stop всегда есть конечное
значение по умолчанию (последняя cтрока с меткой индекса 50088) и мы можем
опустить его 0:50088:2.
# отберем каждую 2-ю строку каждого 2-го столбца,
# передав в loc диапазон строк, диапазон столбцов
bikes.loc[0::2, 'gender':'events':2]

6.13. Знакомство с индексаторами [], loc и iloc  143
# а можно было так
bikes.loc[0:50088:2, 'gender':'events':2]

А сейчас мы выполним отбор, начиная с пятой строки и столбца from_station_
name, передав в loc диапазон строк и диапазон столбцов. Мы задали диапазон строк 4:, это эквивалентно 4:50088:1. Поскольку у stop и step есть значения
по умолчанию, мы можем опустить их 4:50088:1. Мы задали диапазон столбцов 'from_station_name':, это эквивалентно 'from_station_name':'events':1. Вновь, поскольку у stop и step есть значения по умолчанию (последний столбец 'events'
и размер шага 1), мы можем опустить их 'from_station_name':'events':1.
# отбираем, начиная с пятой строки и столбца
# from_station_name, передав в loc диапазон
# строк, диапазон столбцов
bikes.loc[4:, 'from_station_name':]

Теперь мы отберем столбцы start_capacity и tripduration (т. е. все строки
в столбцах start_capacity и tripduration), передав в loc список нужных столбцов

144



Инструменты

после нотации двоеточия с запятой. Двоеточие перед списком столбцов как
раз обозначает отбор всех строк.
# отбираем столбцы start_capacity
# и tripduration, передав в loc список
# столбцов после двоеточия с запятой
bikes.loc[:, ['start_capacity', 'tripduration']]

Отберем диапазон столбцов, передав в loc диапазон столбцов после нотации
двоеточия с запятой.
# отбираем диапазон столбцов с помощью loc
bikes.loc[:, 'gender':'tripduration']

Двоеточие можно использовать и для отбора нужных строк.
Давайте отберем строки с метками индекса 1, 5 и 6, передав в loc список
строк перед запятой с двоеточием. Двоеточие после списка строк уже будет
обозначать отбор всех столбцов.

6.13. Знакомство с индексаторами [], loc и iloc  145
# отберем строки с метками индекса 1, 5 и 6
bikes.loc[[1, 5, 6], :]

Давайте отберем диапазон строк, передав в loc диапазон строк перед запятой с двоеточием.
# отберем каждую 10-ю строку
bikes.loc[0::10, :]

Индексатор iloc очень похож на индексатор loc, но использует целочисленную позицию для создания своего подмножества. Само слово iloc обозначает
integer location (целочисленная позиция) и напомнит, что оно делает.
Целочисленная позиция – это термин, используемый для ссылки на строку
или столбец. На первую строку/столбец ссылается целое число 0. На каждую
последующую строку ссылается следующее целое число. На последнюю строку/столбец ссылается число n – 1, где n – 1 – количество строк/столбцов.
В iloc мы можем передать:
 отдельное целочисленное значение;
 список целочисленных значений;
 срез с целочисленными значениями.
Однако, в отличие от loc, в iloc невозможен логический отбор.

146



Инструменты

Давайте используем нотацию среза для отбора строк с целочисленными
позициями 2 и 3 и список для отбора столбцов с целочисленными позициями 2 и 3. Обратите внимание, что в iloc целочисленная позиция step всегда исключается. В loc целочисленная позиция step всегда включена. Итак, передаем диа­пазон строк и диапазон столбцов (срез с целочисленными значениями
строк и срез с целочисленными значениями столбцов) в iloc.
# отбираем строки с индексами 2 и 3
# и столбцы с индексами 2 и 3
bikes.iloc[2:4, 2:4]

Теперь отберем столбцы с индексами 3 и 5, передав в iloc список столбцов
после двоеточия с запятой. Двоеточие перед списком столбцов вновь обозначает отбор всех строк.
# отбираем столбцы с индексами 3 и 5, передав в
# iloc список столбцов после двоеточия с запятой
bikes.iloc[:, [3, 5]]

Теперь отберем строки с индексами 3 и 5, передав в iloc список строк перед
запятой с двоеточием. Напомним, что двоеточие после списка строк обозначает отбор всех столбцов.
# отбираем строки с индексами 3 и 5, передав в
# iloc список строк перед запятой с двоеточием
bikes.iloc[[3, 5], :]

6.14. Фильтрация данных  147

Библиотека pandas предлагает еще два редко встречающихся индексатора:

at и iat. Эти индексаторы аналогичны loc и iloc соответственно, но выбирают

только одну ячейку датафрейма. Поскольку они выбирают только одну ячейку,
вы должны передать строку и столбец либо в виде метки (loc), либо в виде целочисленной позиции (iloc).
Давайте с помощью at отберем строку с меткой индекса 3 и столбец
tripduration.
# отбираем строку с меткой индекса 3 и
# столбец tripduration
bikes.at[3, 'tripduration']
667

Давайте с помощью at отберем строку с индексом 3 и столбец с индексом 3
(это будет столбец tripduration).
# отбираем строку с индексом 3 и
# столбец с индексом 3
bikes.iat[3, 3]

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

6.14.1. Одно условие
Ниже мы приведем пример фильтрации на основе одного условия, которое
проверяется для каждой строки. Возвращаются только те строки, которые
удовлетворяют этому условию.
# отбор по одному условию
filt = bikes['tripduration'] > 5000
bikes[filt].head(3)

148



Инструменты

6.14.2. Несколько условий
Мы можем отфильтровать данные на основе нескольких условий. В следующем примере мы отберем поездки женщин с продолжительностью поездки
более 5000 (секунд).
# отбор по нескольким условиям
filt1 = bikes['tripduration'] > 5000
filt2 = bikes['gender'] == 'Female'
filt = filt1 & filt2
bikes[filt].head(3)

Итак, мы отобрали поездки женщин с продолжительностью поездки более
5000 (секунд).
В следующем примере несколько условий, но требуется, чтобы только одно
из условий было истинным. Мы возвращаем все строки, в которых либо поездку на велосипеде совершает женщина, либо продолжительность поездки
превышает 5000.
# только одно из условий является истинным
filt = filt1 | filt2
bikes[filt].head(3)

6.14.3. Несколько условий в одном столбце
Мы можем отфильтровать данные на основе нескольких условий одного
столбца.
# несколько условий в одном столбце events
filt = ((bikes['events'] == 'rain') |
(bikes['events'] == 'snow') |
(bikes['events'] == 'tstorms') |
(bikes['events'] == 'sleet'))
bikes[filt].head(3)

6.14. Фильтрация данных  149

Как вариант можно воспользоваться методом .isin().
# несколько условий в одном столбце events,
# используем isin
filt = bikes['events'].isin(['rain', 'snow',
'tstorms', 'sleet'])
bikes[filt].head(3)

Можно скомбинировать метод .isin() с каким-нибудь другим фильтром.
# сочетание isin и дополнительного фильтра
filt1 = bikes['events'].isin(['rain', 'snow',
'tstorms', 'sleet'])
filt2 = bikes['tripduration'] > 2000
filt = filt1 & filt2
bikes[filt].head(3)

6.14.4. Использование метода .query()
Метод .query() предоставляет альтернативный и часто более читаемый способ
фильтрации данных, чем описанные выше. Строка с условием просто передается в метод .query() для фильтрации данных.
# отбор по одному условию
bikes.query('tripduration > 5000').head(3)

# отбор по нескольким условиям
bikes.query('tripduration > 5000 and gender=="Female"').head(3)

150



Инструменты

# только одно из условий является истинным
bikes.query('tripduration > 5000 or gender=="Female"').head(3)

Мы можем проверить, равно ли каждое значение в интересующем столбце одному или нескольким другим заданным значениям, используя слово in
в своем запросе. Используем синтаксис для создания списка в строке запроса,
чтобы он содержал все значения, которые нужно проверить. Сейчас мы отберем поездки, совершенные, когда шел снег или дождь.
# отбор с помощью слова in
bikes.query('events in ["snow", "rain"]').head(3)

Мы можем инвертировать результат, поставив not перед in. Сейчас отберем
поездки, которые были совершены в дни, когда не было облачно, частично облачно или преимущественно облачно.
# отбор с помощью слова not in
bikes.query('events not in ["cloudy", "partlycloudy", "mostlycloudy"]').head(3)

К сожалению, метод .query() не дает нам возможности выбрать подмножест­
во столбцов при фильтрации данных. Вам нужно будет сделать обычный отбор столбца после вызова метода. Здесь мы используем только скобки, чтобы
выбрать два столбца, после того как найдем все поездки, когда шел снег или
дождь.

6.15. Агрегирование данных  151
# отберем три столбца для поездок, совершенные,
# когда шел снег или дождь
cols = ['starttime', 'temperature', 'events']
bikes.query('events in ["snow", "rain"]')[cols].head(3)

6.15. Агрегирование данных
6.15.1. Группировка и агрегирование с помощью одного
столбца
Взгляните на рисунок ниже. Мы сгруппируем наши исходные данные в независимые датафреймы на основе уникальных значений одного или нескольких
столбцов. Наши группы основаны на значении столбца Dept. Как только данные будут разбиты на эти независимые датафреймы, для каждого мы выполним агрегирование. Значения столбца Salary агрегируются с помощью функции sum, а столбец Experience агрегируется с помощью функции mean.

Рис. 16 Схема группировки данных в pandas

Группировка данных – чрезвычайно распространенный метод, используемый в анализе данных, который может помочь нам ответить на множество
вопросов. Вот несколько примеров:
 Какова максимальная заработная плата для каждого отдела в компании?
 Какова средняя температура и количество осадков за каждый месяц
в разных городах?
 Какие рубашки входят в пятерку самых продаваемых в каждом магазине?

152



Инструменты

Большинство задач, связанных с группировкой данных в pandas, решаются
с помощью метода .groupby(). Этот единственный метод отвечает за группировку данных на независимые датафреймы и выполнение агрегирования. Обычно это делается в одной строчке кода.
Наиболее распространенным типом действия, выполняемым над каждой группой, является агрегирование, хотя можно манипулировать данными
в каждой группе любым удобным для вас способом.
Процедура агрегирования с помощью метода .groupby() имеет три отдельных
компонента: группирующий столбец, агрегируемый столбец и функцию агрегирования.
 Группирующий столбец. Каждое отдельное значение в этом столбце
образует отдельную группу.
 Агрегируемый столбец. Столбец, к которому мы применяем агрегирующую функцию. Этот столбец обычно является количественным.
 Агрегирующая функция. Функция, применяемая к агрегируемому
столбцу.
Как правило, мы будем использовать цепочку, состоящую из метода .groupby()
и метода, который, собственно, возвращает нужный результат. Поэтому процедура агрегирования будет включать два этапа. Во-первых, с помощью метода .groupby() мы сообщаем pandas, как мы хотели бы группироваться, а затем
добавляем метод .agg(), чтобы сообщить pandas, как агрегировать. Общий синтаксис имеет следующий вид:
df.groupby('группирующий столбец').agg(
new_column=('агрегируемый столбец',
'агрегирующая функция'))

Группирующий столбец, агрегируемый столбец и агрегирующая функция
агрегирования могут быть предоставлены в виде строковых значений. Имя
нового столбца new_column мы определяем в методе .agg() и задаем его равным кортежу из двух элементов – агрегируемого столбца и агрегирующей
функции.
Выполним нашу первую процедуру агрегации – вычислим среднюю длительность поездки в зависимости от погоды во время поездки.
# вычислим среднюю длительность поездки
# в зависимости от погоды во время поездки
bikes.groupby('events').agg(
avg_tripduration=('tripduration', 'mean'))

6.15. Агрегирование данных  153

При выполнении агрегирования с помощью метода .groupby() важно идентифицировать каждый компонент агрегирования. Для вышеприведенного примера мы имеем:
 группирующий столбец – events;
 агрегируемый столбец – tripduration;
 агрегирующая функция – функция mean.
Ниже приведены допустимые строковые имена агрегирующих функций:
 sum;
 min;
 max;
 mean;
 median;
 std;
 var;
 count – количество непропущенных значений;
 size – количество всех элементов;
 first – первое значение в группе;
 last – последнее значение в группе;
 idxmax – индекс максимального значения в группе;
 idxmin – индекс минимального значения в группе.

6.15.2. Группировка и агрегирование с помощью
нескольких столбцов
Мы можем выполнить группировку на основе нескольких столбцов, передав
в метод .groupby() список групирующих столбцов. Давайте вычислим среднюю
длительность поездки в зависимости от комбинации пола и типа погодного
явления во время поездки.
# вычислим среднюю длительность поездки
# в зависимости от комбинации пола
# и погоды во время поездки
bikes.groupby(['gender', 'events']).agg(
avg_tripduration=('tripduration', 'mean'))

154



Инструменты

Столбцы gender и events больше не являются столбцами и были помещены в индекс. Речь идет о многоуровневом индексе (MultiIndex), теперь gender
и events считаются уровнями индекса.
На практике многоуровневый индекс не всегда добавляет особой ценности и может помешать процессу обучения. Обычно переходят к обычному
индексу. Часто это обусловлено тем, что агрегирование – промежуточная
процедура, вы получили агрегированные данные (например, создавали
признаки – групповые средние) и затем хотите присоединить их к исходным данным, многоуровневый индекс здесь вам только помешает. Поэтому
с помощью метода .reset_index() можно вернуть группирующие столбцы из
уровней в столбцы.
# вычислим среднюю длительность поездки в зависимости
# от комбинации пола и погоды во время поездки,
# избавимся от многоуровневого индекса
bikes.groupby(['gender', 'events']).agg(
avg_tripduration=('tripduration', 'mean')).reset_index()

6.15. Агрегирование данных  155

Чтобы агрегировать несколько столбцов, мы передаем в метод .agg() для
каждого столбца новое имя и приравниваем его к кортежу из двух элементов,
содержащему агрегируемый столбец и агрегирующую функцию. Сейчас мы
вычислим среднюю продолжительность поездки и среднюю температуру для
каждого типа погодного явления.
# мы вычислим среднюю продолжительность поездки
# и среднюю температуру для каждого типа
# погодного явления, избавимся от
# многоуровневого индекса
bikes.groupby('events').agg(
avg_tripduration=('tripduration', 'mean'),
avg_temp=('temperature', 'mean')).reset_index()

156



Инструменты

При желании мы можем задать несколько группирующих столбцов, несколько агрегируемых столбцов, а также несколько агрегирующих функций. Мы немного упростим задачу: вычислим среднюю длительность поездки, максимальную длительность поездки, среднюю температуру в зависимости от типа
погоды во время поездки, при этом избавимся от многоуровневого индекса.
# вычислим среднюю длительность поездки,
# максимальную длительность поездки, среднюю температуру
# в зависимости от типа погоды во время поездки,
# избавимся от многоуровневого индекса
bikes.groupby('events').agg(
avg_tripduration=('tripduration', 'mean'),
max_tripduration=('tripduration', 'max'),
avg_temp=('temperature', 'mean')).reset_index()

6.15.3. Группировка с помощью сводных таблиц
С помощью метода .pivot_table() мы можем построить сводную таблицу.
Чтобы воспользоваться методом .pivot_table(), нам нужно задать следующие
параметры:

6.15. Агрегирование данных  157
 index – столбец вертикальной группировки (столбцы, которые будут размещены по вертикали);
 columns – столбец горизонтальной группировки (столбцы, которые будут
размещены по горизонтали);
 values – агрегируемый столбец;
 aggfunc – агрегирующая функция (по умолчанию используется среднее).
Загрузим данные американской автостраховой компании StateFarm (добавлены категориальные признаки). Они представляют собой записи о 8293 клиентах, классифицированных на два класса: 0 – отклика нет на предложение
автостраховки (7462 клиента) и 1 – отклик есть на предложение автостраховки
(831 клиент). По каждому наблюдению (клиенту) фиксируются следующие переменные (характеристики):
 количественный признак Пожизненная ценность клиента [Customer
Lifetime Value];
 категориальный признак Вид страхового покрытия [Coverage];
 категориальный признак Образование [Education];
 категориальный признак Тип занятости [EmploymentStatus];
 категориальный признак Пол [Gender];
 количественный признак Доход клиента [Income];
 количественный признак Размер ежемесячной автостраховки [Monthly
Premium Auto];
 количественный признак Количество месяцев со дня подачи последнего
страхового требования [Months Since Last Claim];
 количественный признак Количество месяцев с момента заключения
страхового договора [Months Since Policy Inception];
 количественный признак Количество открытых страховых обращений
[Number of Open Complaints];
 количественный признак Количество полисов [Number of Policies];
 бинарная зависимая переменная Отклик на предложение автостраховки
[Response].
# загружаем данные
ins = pd.read_csv('Data/StateFarm_missing.csv', sep=';')
ins.head()

С помощью pivot_table() посмотрим, как варьирует средний доход клиента
по комбинациям пола и образования.
# смотрим, как варьирует средний доход клиента
# по комбинациям пола и образования
ins.pivot_table(index='Education',

158



Инструменты
columns='Gender',
values='Income',
aggfunc='mean')

Теперь избавимся от шума, с помощью метода .round() округлим значения
дохода до ближайшей тысячи, а с помощью метода .astype() превратим их в целые числа.
# смотрим, как варьирует средний доход клиента
# по комбинациям пола и образования,
# округлим и превратим в целые числа
ins.pivot_table(
index='Education',
columns='Gender',
values='Income',
aggfunc='mean').round(-3).astype('int')

Те же самые результаты мы могли бы получить, используя агрегацию с методом .groupby().
# все то же самое можно получить, используя
# агрегацию с groupby
ins.groupby(['Gender', 'Education']).agg(
mean_salary=('Income', 'mean')) \
.round(-3).astype('int64')

6.15. Агрегирование данных  159

Заметим, что расположение всех данных по вертикали немного затрудняет
сравнение.
Сводные таблицы создают широкие данные с новыми столбцами для каждого уникального значения одного из группирующих столбцов. Широкие данные
обычно легче читать и принимать решения. Метод .groupby() возвращает длинные данные с результатами по каждой группе в одном столбце, что затрудняет
сравнение.
По умолчанию для параметра aggfunc использутся значение 'mean'. Для свод­
ных таблиц доступны все те же строковые названия агрегирующих функций,
что и для метода .groupby(). Давайте найдем максимальные значения дохода по
комбинациям пола и образования.
# смотрим максимальный доход клиента
# по комбинациям пола и образования
ins.pivot_table(index='Education',
columns='Gender',
values='Income',
aggfunc='max')

Программа Microsoft Excel хорошо известна своими сводными таблицами,
которые создаются путем перетаскивания разных столбцов в разные поля, тем
самым мы можем «поворачивавать» данные. В pandas вам придется изменить
значения параметров и снова вызвать метод .pivot_table(), чтобы получить тот
же эффект. Давайте повернем таблицу, разместив пол по индексу, а образование – по столбцам.

160



Инструменты

# смотрим максимальный доход клиента
# по комбинациям пола и образования,
# поменяли Education и Gender местами
ins.pivot_table(index='Gender',
columns='Education',
values='Income',
aggfunc='max')

Вы можете задать стиль датафрейма, изменив цвет текста, цвет фона, шрифт
и некоторые другие элементы с помощью свойства style.
Давайте посмотрим среднюю пожизненную ценность клиента по комбинациям типа занятости и образования, результаты переведем в целые числа.
# посмотрим среднюю пожизненную ценность клиента
# по комбинациям типа занятости и образования,
# результаты переводим в целые числа
emp_edu_mean_clv = ins.pivot_table(
index='EmploymentStatus',
columns='Education',
values='Customer Lifetime Value',
aggfunc='mean').astype('int64')
emp_edu_mean_clv

Метод .highlight_max() выделяет максимальное значение в каждом столбце или строке. По умолчанию он выделяет максимальное значение каждого
столбца.
# подсветим максимальные значения в каждом столбце
emp_edu_mean_clv.style.highlight_max()

6.15. Агрегирование данных  161
Мы можем выделить максимальное значение в каждой строке, установив
для параметра axis значение 'columns' или 1. А если для параметра axis задать
значение None, будет подсвечена ячейка с максимальным значением во всей
таблице.
# подсветим максимальные значения в каждой строке
emp_edu_mean_clv.style.highlight_max(axis='columns')

С помощью метода .background_gradient() задаем интенсивность фона ячейки
в зависимости от значения в ячейке.
# задаем интенсивность фона ячейки
# в зависимости от значения в ячейке
emp_edu_mean_clv.style.background_gradient(cmap='Oranges')

Мы можем подсветить минимальное и максимальное значения каждого
столбца, используя разные цвета.
# подсветим минимальные и максимальные
# значения в каждом столбце
emp_edu_mean_clv.style.highlight_max(color='yellow') \
.highlight_min(color='lightblue')

162



Инструменты

Можно использовать метод .pivot_table(), чтобы получить информацию
о размере каждой комбинации группирующих столбцов. При этом нет необходимости использовать агрегируемый столбец (параметр values). Библиотека
pandas знает, что размер группы не зависит от того, что она агрегирует, поэтому вам не нужно задавать агрегируемый столбец. Сейчас мы вычислим размер
каждой уникальной комбинации типа занятости и образования.
# вычислим размер каждой уникальной комбинации
# типа занятости и образования
ins.pivot_table(index='EmploymentStatus',
columns='Education',
aggfunc='size')

Для параметра margins можно установить значение True, чтобы добавить одну
дополнительную строку и столбец в сводную таблицу, здесь с помощью агрегирующей функции мы вычисляем одну и ту же статистику для всей строки или
столбца. В сводной таблице ниже мы видим, что средний доход всех бакалавров (Bachelor) составляет 37 521.5, а средний доход всех пенсионеров (Retired) –
20 489.5. Кроме того, мы сравним средние доходы для каждой комбинации со
средним доходом в целом по выборке.
# смотрим, как варьирует средний доход клиента
# по комбинациям типа занятости и образования,
# добавляем среднюю зарплату для
# всей строки и всего столбца
ins.pivot_table(index='EmploymentStatus',
columns='Education',
values='Income',
aggfunc='mean',
margins=True)

6.15. Агрегирование данных  163
# вычислим средний доход
print(ins['Income'].mean())
37785.17199372814

В сводной таблице может быть и один группирующий столбец. Сейчас мы
найдем средний доход для каждого типа занятости.
# задаем один группирующий столбец
ins.pivot_table(index='EmploymentStatus',
values='Income',
aggfunc='mean').round(-3)

При использовании одного группирующего столбца результат будет точно
таким же, как и при агрегации с помощью метода .groupby(). Преимущество использования агрегации с помощью метода .groupby() заключается в возможности переименовать результирующий столбец.
# тот же результат с .groupby()
ins.groupby('EmploymentStatus').agg(
average_income=('Income', 'mean')).round(-3)

Мы можем повернуть эту таблицу с одним столбцом, используя параметр

columns и отказавшись от параметра index.
# поворачиваем таблицу с одним столбцом
ins.pivot_table(columns='EmploymentStatus',
values='Income',
aggfunc='mean').round(-3)

164



Инструменты

Можно использовать любое количество группирующих столбцов при создании сводных таблиц. С этой целью нужно создать список столбцов, которые
мы разместим по строкам или столбцам. Сейчас мы зададим два столбца вертикальной группировки (тип занятости, пол) и один столбец горизонтальной
группировки (образование). Мы найдем максимальный доход по типу занятости, полу и образованию. Уникальные комбинации типа занятости и пола
(столбцы вертикальной группировки) помещаются в индекс. Итоговый датафрейм получит многоуровневый индекс.
# задаем два столбца вертикальной группировки
# (тип занятости, пол) и один столбец
# горизонтальной группировки (образование)
ins.pivot_table(index=['EmploymentStatus', 'Gender'],
columns='Education',
values='Income',
aggfunc='max')

А сейчас мы зададим один столбец вертикальной группировки (тип занятости) и два столбца горизонтальной группировки (пол и образование).
# зададим один столбец вертикальной группировки
# (тип занятости) и два столбца горизонтальной
# группировки (пол и образование)
ins.pivot_table(index='EmploymentStatus',
columns=['Gender', 'Education'],
values='Income',
aggfunc='max')

6.15. Агрегирование данных  165
При создании сводной таблицы лучше сохранять многоуровневый индекс.
Несколько уровней при использовании сводной таблицы (если вам случится
их создавать). Это противоположно совету, который мы рекомендовали при
агрегировании с помощью метода .groupby(). Причина заключается в том,
что, скорее всего, сводные таблицы будут конечным продуктом, который вы
будете использовать в презентации или отчете, и этот продукт не предполагает дальнейших манипуляций (например, объединения с исходными данными).
Мы можем задать несколько агрегируемых столбцов. Мы вычислим среднюю пожизненную ценность клиента и средний доход по комбинациям образования и пола, дополнительно добавим итоговые значения по строкам
и столбцам.
# зададим два агрегируемых столбца
# (пожизненная ценность клиента и доход)
ins.pivot_table(
index='Education',
columns='Gender',
values=['Income', 'Customer Lifetime Value'],
aggfunc='mean', margins=True)

Мы можем задать несколько агрегирующих функций. Вычислим минимальный, средний и максимальный доходы по комбинациям образования и пола,
дополнительно добавим итоговые значения по строкам и столбцам.
# зададим несколько агрегирующих функций
ins.pivot_table(
index='Education',
columns='Gender',
values='Income',
aggfunc=['min', 'mean', 'max'], margins=True)

166



Инструменты

6.16. Анализ частот с помощью таблиц сопряженности
С помощью метода .value_counts() мы можем вывести абсолютные частоты категорий переменной. По умолчанию если есть пропуски, то они опускаются,
вывести их можно, задав dropna=False.
Давайте выведем абсолютные частоты категорий переменной Coverage, посмотрим встречаемость различных видов страхового покрытия.
# выведем абсолютные частоты категорий
ins['Coverage'].value_counts(dropna=False)
Basic
5038
Extended
2501
Premium
749
NaN
5
Name: Coverage, dtype: int64

Задав для параметра normalize значение True, мы выведем уже относительные
частоты (абсолютные частоты, поделенные на общее количество наблюдений).
# выведем относительные частоты категорий
ins['Coverage'].value_counts(dropna=False,
normalize=True)
Basic
0.607500
Extended
0.301580
Premium
0.090317
NaN
0.000603
Name: Coverage, dtype: float64

Абсолютные частоты категорий можно также получить с помощью цепочки методов .groupby() и .size(). Однако информация по пропускам выведена не
будет.
# выведем абсолютные частоты категорий
# с помощью .groupby() и .size()
ins.groupby('Coverage').size().reset_index(name='count')

6.16. Анализ частот с помощью таблиц сопряженности  167

С помощью цепочки методов .groupby() и .size() можно посмотреть абсолютные частоты комбинаций вида страхового покрытия и пола.
# выведем абсолютные частоты категорий
# с помощью .groupby() и .size()
ins.groupby(['Coverage', 'Gender']).size().reset_index(name='count')

Абсолютные частоты можно вывести и с помощью метода .pivot_table().
# выведем абсолютные частоты категорий
# с помощью .pivot_table()
ins.pivot_table(index='Coverage',
columns='Gender',
aggfunc='size')

Функция pd.crosstab() создана специально для подсчета совместной встречаемости значений двух и более столбцов. Название происходит от слова
cross tabulation («кросстабуляция»). Кроме того, часто используется термин
contingency tables («таблицы сопряженности»).
К сожалению, crosstab – это функция, а не метод. Это означает, что она не
привязана к какому-либо датафрейму, но доступ к ней должен осуществ­
ляться непосредственно из pd. У нее много таких же параметров, что и у метода .pivot_table(), и используется она аналогично. Поскольку она не привязана ни к какому объекту DataFrame, в качестве значений параметров мы
указываем серии вместо строковых значений. По умолчанию функция вычисляет размер каждой группы, поэтому нет необходимости устанавливать
параметр aggfunc.
# строим таблицу сопряженности вида
# страхового покрытия и пола

168



Инструменты

pd.crosstab(index=ins['Coverage'],
columns=ins['Gender'])

Результат идентичен результату, полученному с помощью метода .pivot_
table(). Однако у функции pd.crosstab() есть большое преимущество, а именно ее

способность возвращать относительные частоты с помощью параметра нормализации. Это нелегко сделать с помощью .groupby() или .pivot_table(). Функция
pd.crosstab() позволяет нормализовать строки, столбцы и итоговые значения.
Например, нас интересует относительная частота мужчин в каждом виде страхового покрытия. Нам нужно нормализовать каждую строку, для этого для параметра normalize задаем значение 'index'. Все строки должны в сумме давать 100 %.
# вычислим относительные частоты мужчин и женщин
# в каждом виде страхового покрытия,
# нормализация по строкам
pd.crosstab(index=ins['Coverage'],
columns=ins['Gender'],
normalize='index').round(3) * 100

А теперь нас интересуют относительные частоты видов страхового покрытия для мужчин. Нам нужно нормализовать каждый столбец, для этого для
параметра normalize задаем значение 'columns'. Все столбцы должны в сумме давать 100 %.
# вычислим относительные частоты видов
# страхового покрытия для мужчин и женщин,
# нормализация по столбцам
pd.crosstab(index=ins['Coverage'],
columns=ins['Gender'],
normalize='columns').round(3) * 100

6.17. Выполнение SQL-запросов в pandas  169
Можно найти относительные частоты с учетом всех данных, установив для
параметра normalize значение 'all'.
# вычислим относительные частоты
# с учетом всех данных
pd.crosstab(index=ins['Coverage'],
columns=ins['Gender'],
normalize='all').round(3) * 100

Согласно возвращенному датафрейму, 4,1% всех клиентов – это мужчины,
оформившие вид страхового покрытия Premium.
Теперь вернемся к абсолютным частотам и зададим итоги по строкам
и столбцам, установив для параметра margins значение True.
# вернемся к абсолютным частотам, зададим
# итоги по строкам и столбцам
pd.crosstab(index=ins['Coverage'],
columns=ins['Gender'],
margins=True)

6.17. Выполнение SQL-запросов в pandas
Библиотека pandas стала удобным инструментом для выполнения SQL-запросов. Давайте загрузим данные и подключимся к базе данных, а затем на
конкретных примерах сравним синтаксис SQL-запросов и нативных запросов
в pandas.
# импортируем библиотеки
import pandas as pd
import sqlite3
# загружаем данные
airports = pd.read_csv('Data/airports.csv')
# взглянем на данные
airports.head()

170



Инструменты

# создаем подключение
connection = sqlite3.connect('Data/flights.sqlite')
airports.to_sql('AIRPORTS', connection, if_exists='replace')
# подтверждаем отправку данных
connection.commit()

Отбор столбца
# отбираем столбец с помощью метода .read_sql()
query_res = pd.read_sql("SELECT id FROM AIRPORTS;", connection)
query_res.head()

# отбираем столбец в pandas
pandas_res = airports.loc[:, 'id']
pandas_res.head()
0
1
2
3
4
Name:

6523
323361
6524
6525
6526
id, dtype: int64

Отбор столбца с условием
# отбираем столбец с помощью метода .read_sql()
query_res2 = pd.read_sql("SELECT id FROM AIRPORTS WHERE ident='KLAX';",
connection)
query_res2

6.13. Знакомство с индексаторами [], loc и iloc...  171
# отбираем столбец с условием в pandas
pandas_res2 = airports[airports['ident'] == 'KLAX']['id']
pandas_res2
27909 3632
Name: id, dtype: int64

Отбор столбцов с несколькими условиями
# отбираем столбцы с несколькими условиями с помощью метода .read_sql()
query_res3 = pd.read_sql("SELECT ident, name, municipality FROM AIRPORTS "
"WHERE iso_region='US-CA' AND "
"type='large_airport';",
connection)
query_res3

# отбираем столбцы с несколькими условиями в pandas
pandas_res3 = airports[(airports['iso_region'] == 'US-CA') & (
airports['type'] == 'large_airport')][['ident', 'name', 'municipality']]
pandas_res3

172



Инструменты

query_res3

Отбор столбцов c условием и сортировкой
по выбранной переменной
# отбираем все столбцы с условием, упорядочив по id,
# с помощью метода .read_sql()
query_res4 = pd.read_sql("SELECT * FROM AIRPORTS WHERE "
"type='small_airport' ORDER BY id;",
connection,
index_col='index')
query_res4.head()

6.17. Выполнение SQL-запросов в pandas  173

# отбираем все столбцы с условием, упорядочив по id, в pandas
pandas_res4 = airports[airports['type'] == 'small_airport'].sort_values('id')
pandas_res4.head()

Отбор столбцов c условием и обратной
сортировкой по выбранной переменной
# отбираем все столбцы с условием, упорядочив по id
# в обратном порядке, с помощью метода .read_sql()
query_res5 = pd.read_sql("SELECT * FROM AIRPORTS WHERE type='small_airport' "
"ORDER BY id DESC;",
connection,
index_col='index')
query_res5.head()

174



Инструменты

# отбираем все столбцы с условием, упорядочив по id
# в обратном порядке, в pandas
pandas_res5 = airports[airports['type'] == 'small_airport'].sort_values(
'id', ascending=False)
pandas_res5.head()

Отбор столбцов cо спископодобным условием – учитываем
только строковые значения переменной В списке
# отбираем все столбцы со спископодобным условием, учитывая лишь
# строковые значения переменной type В списке,
# с помощью метода .read_sql()
query_res6 = pd.read_sql("SELECT * FROM AIRPORTS WHERE type IN "
"('heliport', 'balloonport');",
connection,
index_col='index')
query_res6.head()

6.17. Выполнение SQL-запросов в pandas  175

# отбираем все столбцы со спископодобным условием, учитывая
# лишь строковые значения переменной type В списке, в pandas
pandas_res6 = airports[airports['type'].isin(
['heliport', 'balloonport'])]
pandas_res6.head()

Отбор столбцов cо спископодобным условием – учитываем
только строковые значения переменной ВНЕ списка
# отбираем все столбцы со спископодобным условием,
# учитывая лишь строковые значения переменной type
# ВНЕ списка, с помощью метода .read_sql()
query_res7 = pd.read_sql("SELECT * FROM AIRPORTS WHERE type NOT IN "
"('heliport', 'balloonport');",
connection,
index_col='index')
query_res7.head()

176



Инструменты

# отбираем все столбцы со спископодобным условием, учитывая лишь
# строковые значения переменной type ВНЕ списка, в pandas
pandas_res7 = airports[~airports['type'].isin(
['heliport', 'balloonport'])]
pandas_res7.head()

Вывод статистик по переменным
# выводим статистики по переменной elevation_ft
# с помощью метода .read_sql()
query_res8 = pd.read_sql("SELECT MAX(elevation_ft), MIN(elevation_ft), "
"AVG(elevation_ft) FROM AIRPORTS;",
connection)
query_res8

# закрываем подключение
connection.close()
# выводим статистики по переменной elevation_ft

6.17. Выполнение SQL-запросов в pandas  177
pandas_res8 = airports.agg({'elevation_ft': ['min', 'max', 'mean']})
pandas_res8

Слияние таблиц/датафреймов
# импортируем класс date модуля datetime
from datetime import date
# это наши клиенты
customers = {'CustomerID': [10, 11],
'Name': ['Mike', 'Marcia'],
'Address': ['Address for Mike',
'Address for Marcia']}
customers = pd.DataFrame(customers)
customers

# это наши заказы, сделанные клиентами, они связаны
# с клиентами с помощью столбца CustomerID
orders = {'CustomerID': [10, 11, 10],
'OrderDate': [date(2014, 12, 1),
date(2014, 12, 1),
date(2014, 12, 1)]}
orders = pd.DataFrame(orders)
orders

# создаем подключение
connection = sqlite3.connect('Data/sales.sqlite')
customers.to_sql('CUSTOMERS', connection, if_exists='replace')
orders.to_sql('ORDERS', connection, if_exists='replace')
# подтверждаем отправку данных
connection.commit()
# выполняем слияние таблиц CUSTOMERS и ORDERS без
# сохранения идентификатора с помощью метода .read_sql()
query_res9_1 = pd.read_sql("SELECT Name, Address, OrderDate "
"FROM CUSTOMERS c "
"INNER JOIN ORDERS o "
"ON c.CustomerID = o.CustomerID",
connection)
query_res9_1

178



Инструменты

# выполняем слияние таблиц CUSTOMERS и ORDERS с
# сохранением идентификатора с помощью метода .read_sql()
query_res9_2 = pd.read_sql("SELECT CUSTOMERS.CustomerID, Name, "
"Address, OrderDate FROM CUSTOMERS "
"INNER JOIN ORDERS "
"ON CUSTOMERS.CustomerID=ORDERS.CustomerID",
connection)
query_res9_2

# выполняем слияние датафреймов customers и orders
pandas_res9 = customers.merge(orders)
pandas_res9

Задача с собеседования (теория вероятности)
1. По данным ФБР, около 80 % всех преступлений против собственности
остаются нераскрытыми. Предположим, что в вашем городе совершено 3 таких преступления, каждое из которых считается независимым друг от друга.
Какова вероятность раскрытия точно одного из трех преступлений? Какова вероятность раскрытия по крайней мере одного преступления?

Задача с собеседования
(математическая статистика)
1. Почему на практике мы, как правило, пользуемся бутстрепированными
доверительными интервалами метрик качества (например, бутстрепированным доверительным интервалом AUC-ROC) вместо того чтобы вычислять доверительные интервалы по асимптотическому методу, пользуясь центральной
предельной теоремой?

7.1. Основы работы с классами, строящими модели...  179

7. SCIKIT- LEARN
Библиотека scikit-­learn (произносится как сайкит-лёрн) – это проект с открытым исходным кодом. Ее можно свободно использовать и распространять.

7.1. Основы работы с классами, строящими модели
предварительной подготовки данных и модели
машинного обучения
В библиотеке scikit-learn каждая модель предварительной подготовки данных и каждая модель машинного обучения реализована в собственном классе. При этом классы, в которых реализованы модели машинного обучения
с учителем, в зависимости от решаемой задачи называются классификаторами (classifier) или регрессорами (regressor). Классы-классификаторы обычно
имеют название [Метод_машинного_обучения]Classifier, например DesicionTreeClassifier,
RandomForestClassifier. Классы-регрессоры обычно имеют название [Метод_машинного_
обучения]Regressor, например DesicionTreeRegressor, RandomForestRegressor. Если модель
машинного обучения с учителем позволяет выполнить только одну задачу –
либо задачу регрессии, либо задачу классификации – или речь идет о модели
машинного обучения без учителя, то класс носит название метода, например
класс LinearRegression, потому что линейная регрессия решает только задачу регрессии, или класс DBSCAN по названию метода кластеризации DBSCAN.

Рис. 17 Классы моделей машинного обучения в scikit-learn

При работе с классом – моделью предварительной подготовки – мы выполняем следующие операции:
 импортируем из соответствующего модуля класс, в котором реализована
соответствующая модель предварительной подготовки;
 создаем экземпляр класса – объект-модель;
 обучаем модель, т. е. вычисляем параметры, с помощью которых будем
выполнять преобразование, – используем метод .fit() объекта-модели;
 применяем модель, т. е. выполняем преобразование с помощью найденных параметров, – используем метод .transform() объекта-модели;
 либо обучаем и применяем модель сразу – используем метод .fit_
transform() объекта-модели.
В отличие от классов, в которых реализованы модели машинного обучения,
большинство классов, выполняющих предварительную подготовку, будут работать только с массивом признаков, а массив меток не используется.

180



Инструменты

Самый простой способ реализовать собственный класс, строящий модель
предварительной подготовки, – воспользоваться наследованием базовых
классов BaseEstimator и TransformerMixin. Наш класс, как и любой класс предварительной подготовки библиотеки scikit-learn, должен иметь методы __init__,
.fit() и .transform(). Кроме того, наш класс должен иметь методы __get_params__
и __set_params__ (их не нужно специально создавать, они наследуются от базового класса BaseEstimator), с помощью этих методов мы можем задавать и получать
доступ к параметрам. Например, если вы напишете свой класс, не выполнив
наследование класса BaseEstimator, и, например, захотите воспользоваться им
в конвейере в ходе поиска гиперпараметров, то получите ошибку 'название_вашего_класса' object has no attribute 'set_params'.
Все атрибуты инициализируются в методе __init__. Никаких операций
с атрибутами в методе __init__ не должно быть. Названия параметров должны
совпадать с названиями атрибутов.
Несмотря на то что большинство классов, выполняющих предварительную
подготовку, будут работать только с массивом признаков, метод .fit() должен принимать в качестве аргументов X и y. Это требуется для совместимости
с конвейерами scikit-learn! Обычно для y задают значение None. В методе .fit()
происходит вычисление параметров модели и всегда возвращается self.
Метод .transform() должен принимать в качестве аргумента только X, здесь
мы понимаем, что преобразование зависимой переменной нам не требуется.
В методе .transform() происходит применение вычисленных параметров модели и всегда возвращается X.
Вспомогательные методы, предназначенные для правильной внутренней
работы класса, оформляйте в виде частных и защищенных методов.
Импорт библиотек, классов и модулей внутри методов не допускается.
Базовый класс TransformerMixin позволяет нам связать методы .fit() и .transform()
в цепочку (то есть мы можем применить .fit_transform(), используя методы нашего класса .fit() и transform()). Часто пишут собственный метод .fit_transform().
Мы сейчас напишем класс MeanImputer, который будет выполнять замену пропусков средними значениями. Нам потребуется импорт библиотек pandas,
numpy и math. Базовые классы BaseEstimator и TransformerMixin импортировать
не будем, поскольку класс не будет использоваться в конвейере и метод .fit_
transform() мы применять не будем.
# импортируем библиотеки pandas, numpy, math
import pandas as pd
import numpy as np
import math
# создаем собственный класс, выполняющий замену
# пропусков средним значением
class MeanImputer():
"""
Параметры
--------copy: bool, по умолчанию True
Возвращает копию.
Возвращает

7.1. Основы работы с классами, строящими модели...  181
------X : pandas.DataFrame или numpy.ndarray
Датафрейм pandas или массив NumPy
с импутированными значениями.
"""

Метод init, задающий

конструктор класса

(инициализация
атрибутов)


Частный метод
is_numpy,
предназначенный

для правильной

внутренней работы

класса (проверка
типа объекта)














Метод fit, задающий

модели
обучение
Вычисляем един
ственный параметр

модели – среднее

значение перемен
ной





Добавляем параметр copy
в определение метода __init__

def __init__(self, copy=True ): 
# все параметры для инициализации публичных атрибутов
# должны быть заданы в методе __init__

# публичный атрибут
self.copy = copy
 Используем значение copy для
инициализации публичного атрибута класса,
который представлен как self.copy
def __is_numpy(self, X):
# частный метод, который с помощью функции isinstance()
# проверяет, является ли наш объект массивом NumPy
return isinstance(X, np.ndarray)
def fit(self, X, y=None):
# метод .fit() должен принимать
# в качестве аргументов X и y
# создаем пустой словарь, в котором ключами
# будут имена/целые числа, а значениями – средние
self._encoder_dict = {}
# записываем результат метода __is_numpy
is_np = self.__is_numpy(X)
# если 1D-массив, то переводим в 2D
if len(X.shape) == 1:
X = X.reshape(-1, 1)
# записываем количество столбцов
ncols = X.shape[1]
# если объект – массив NumPy
if is_np:
# по каждому столбцу массива NumPy
for col in range(ncols):
# вычисляем среднее и записываем в словарь
self._encoder_dict[col] = np.nanmean(X[:, col])
# если объект – датафрейм pandas
else:
# по каждому столбцу датафрейма pandas
for col in X.columns:
# вычисляем среднее и записываем в словарь
self._encoder_dict[col] = X[col].mean()


# fit возвращает self
return self

182



Инструменты























Метод transform,

задающий при
менениемодели























def transform(self, X):
# transform принимает в качестве
# аргумента только X
#
#
#
#
#
#

выполняем копирование массива во избежание
предупреждения SettingWithCopyWarning
"A value is trying to be set on a copy of
a slice from a DataFrame (Происходит попытка
изменить значение в копии среза данных
датафрейма)"

if self.copy:
X = X.copy()
# записываем результат метода __is_numpy
is_np = self.__is_numpy(X)
# если 1D-массив, то переводим в 2D
if len(X.shape) == 1:
X = X.reshape(-1, 1)
# записываем количество столбцов
ncols = X.shape[1]
# применяем преобразование к X
# если объект – массив NumPy:
if is_np:
# по каждому столбцу массива NumPy
for col in range(ncols):
# заменяем пропуски средним
# значением из словаря
X[:, col] = np.nan_to_num(
X[:, col],
nan=self._encoder_dict[col])
# если объект – датафрейм pandas:
else:
# по каждому столбцу датафрейма pandas
for col in X.columns:
# заменяем пропуски средним
# значением из словаря
X[col] = np.where(X[col].isnull(),
self._encoder_dict[col],
X[col])
# transform возвращает X
return X

7.1. Основы работы с классами, строящими модели...  183
Кратко опишем наш класс MeanImputer. В методе __init__ мы задаем параметр
copy, выполняющий копирование объекта, используем значение параметра
copy для инициализации публичного атрибута self.copy, задаем частный метод __isnumpy, проверяющий, является ли наш объект массивом NumPy, метод
.fit() вычисляет параметр модели – среднее значение по каждой переменной –
и кладет в словарь, метод .transform() применяет преобразование – импутацию

пропусков с помощью вычисленного параметра модели – среднего значения,
которое берет из словаря. Таким образом, для хранения средних значений нам
понадобится словарь. Вновь обратите внимание, что метод .fit() должен принимать в качестве аргументов X и y. А вот метод .transform() принимает в качест­
ве аргумента только X.
Давайте рассмотрим, что происходит в методе __init__.


Метод init, зада
ющий
конструктор класса
(инициализация
атрибутов)

Добавляем параметр copy
в определение метода __init__

def __init__(self, copy=True):
# все параметры для инициализации публичных атрибутов
# должны быть заданы в методе __init__
Используем значение copy для
инициализации публичного атрибута класса,
который представлен как self.copy
Рис. 18 Метод __init__, задающий конструктор класса

В методе __init__ мы выполняем инициализацию публичного атрибута self.copy.
Частный метод __is_numpy проверяет, является ли наш объект массивом
NumPy, для проверки мы используем функцию isinstance()).

Частный
метод is_numpy, пред
назначенный для правильной

внутренней
работы класса

(проверка типа объекта)

def __is_numpy(self, X):
# частный метод, который с помощью функции isinstance()
# проверяет, является ли наш объект массивом NumPy
return isinstance(X, np.ndarray)

Рис. 19 Частный метод __is_numpy для проверки типа объекта

184



Инструменты

Теперь более подробно разберем операции, происходящие в теле метода

.fit().





def fit(self, X, y=None):
# метод .fit() должен принимать
# в качестве аргументов X и y





# создаем пустой словарь, в котором ключами
# будут имена/целые числа, а значениями – средние
self._encoder_dict = {}




# записываем результат метода __is_numpy
is_np = self.__is_numpy(X)


Метод fit, зада
ющий обучение

модели

# если 1D-массив, то переводим в 2D
if len(X.shape) == 1:
X = X.reshape(-1, 1)


Вычисляем
един
ственный параметр модели –

среднее значение

переменной











# записываем количество столбцов
ncols = X.shape[1]




# fit возвращает self
return self

# если объект – массив NumPy
if is_np:
# по каждому столбцу массива NumPy
for col in range(ncols):
# вычисляем среднее и записываем в словарь
self._encoder_dict[col] = np.nanmean(X[:, col])
# если объект – датафрейм pandas
else:
# по каждому столбцу датафрейма pandas
for col in X.columns:
# вычисляем среднее и записываем в словарь
self._encoder_dict[col] = X[col].mean()

Рис. 20 Метод fit для вычисления параметров

Первым делом мы создаем пустой словарь, в котором будем хранить средние значения по каждой переменной.
# создаем пустой словарь, в котором ключами
# будут имена/целые числа, а значениями – средние
self._encoder_dict = {}
Ключом будет название переменной (в случае датафрема pandas) или соответствующее ей целочисленное значение (в случае массива NumPy), а значением – среднее значение переменной). При этом нам важно, чтобы наш массив NumPy был двумерным, поскольку классы библиотеки scikit-learn работают с двумерными массивами признаков, если же вы передадите одномерный
массив NumPy или серию pandas, то получите ошибку:
# создаем 1D-массив признаков
X_toy = np.array([0.1, 0.4, 0.1, 0.9, 0.5])

7.1. Основы работы с классами, строящими модели...  185
# создаем 1D-массив меток
y_toy = np.array([1, 0, 0, 1, 0])
logreg = LogisticRegression(solver='lbfgs',
max_iter=200)
logreg.fit(X_toy, y_toy)
ValueError: Expected 2D array, got 1D array instead:
array=[0.1 0.4 0.1 0.9 0.5].
Reshape your data either using array.reshape(-1, 1) if your data has a single
feature or array.reshape(1, -1) if it contains a single sample.
Добавляем проверку формы массива. Если массив является одномерным,
применяем метод .reshape(-1, 1) и получаем двумерный массив.
# если 1D-массив, то переводим в 2D
if len(X.shape) == 1:
X = X.reshape(-1, 1)
Таким образом, класс выполняет «тихое» неявное преобразование, что допустимо для классов, которые вы используете только для внутреннего применения. Если класс применяется в рамках большого и публичного проекта, то
лучше сделать поведение класса явным.
Вместо применения метода .reshape() мы могли бы с помощью конструкции
assert проверить корректность данных, с которыми должны работать методы
нашего класса.
# проверяем, является ли наш массив двумерным
assert len(X.shape) == 2, 'Array must be 2-dim.'
В случае одномерного массива NumPy мы получили бы сообщение 'Array must

be 2-dim' и вручную преобразовали бы массив в двумерный.
А еще лучше использовать тип исключения ValueError.

# проверяем, является ли наш массив двумерным
if len(X.shape) == 1:
raise ValueError('Array must be 2-dim.')
Затем записываем количество столбцов в переменную ncols. Для этого берем
второй элемент кортежа, возвращаемого свойством shape (т. е. элемент с индексом 1, первый элемент нам не нужен, поскольку он содержит информацию
о количестве строк).
# записываем количество столбцов
ncols = X.shape[1]
Затем мы записываем результат метода __isnumpy в переменную is_np.
# записываем результат __is_numpy()
is_np = self.__is_numpy(X)
Результат представляет собой булево значение False/True, поскольку функция isinstance() возвращает True, если указанный объект (в нашем случае объект – массив NumPy) является таковым, и False в противном случае. Проверка

186



Инструменты

типа объекта позволяет нам выполнять отдельную стратегию вычисления
и применения параметров для массива NumPy и отдельную стратегию вычисления и применения параметров для датафрейма pandas. Качественно
написанный класс должен уметь работать и с датафреймами pandas, и с массивами NumPy.
Например, мы не можем использовать для массива NumPy следующий программный код:
# по каждому столбцу датафрейма pandas
for col in X.columns:
# вычисляем среднее и записываем в словарь
self._encoder_dict[col] = np.mean(X[col])
Мы просто получим ошибку о том, что массив NumPy не имеет свойства

columns.

Наконец, по каждой переменной вычисляем среднее значение и кладем
в словарь. Способ итерирования по столбцам определяется значением переменной is_np. Для массива NumPy используем цикл for c функцией range(), в качестве аргумента функции range() используем ncols, поэтому ключами словаря
будут целочисленные значения, соответствующие переменным, а значениями – средние значения переменных. Вычисление средних значений выполняется с помощью функции библиотеки NumPy nanmean(). Для датафрейма pandas
используем цикл for со списком имен переменных, получаемым с помощью
свойства columns, поэтому ключами словаря будут имена переменных, а значениями – средние значения переменных. Вычисление средних значений выполняется с помощью метода библиотеки pandas .mean().

7.1. Основы работы с классами, строящими модели...  187
Переходим к методу .transform().
def transform(self, X):
# transform принимает в качестве
# аргумента только X
#
#
#
#
#
#

выполняем копирование массива во избежание
предупреждения SettingWithCopyWarning
"A value is trying to be set on a copy of
a slice from a DataFrame (Происходит попытка
изменить значение в копии среза данных
датафрейма)"

if self.copy:
X = X.copy()
# записываем результат метода __is_numpy
is_np = self.__is_numpy(X)
# если 1D-массив, то переводим в 2D
if len(X.shape) == 1:
X = X.reshape(-1, 1)
Метод transform,
задающий применение модели

# записываем количество столбцов
ncols = X.shape[1]
# применяем преобразование к X
# если объект – массив NumPy:
if is_np:
# по каждому столбцу массива NumPy
for col in range(ncols):
# заменяем пропуски средним
# значением из словаря
X[:, col] = np.nan_to_num(
X[:, col],
nan=self._encoder_dict[col])
# если объект – датафрейм pandas:
else:
# по каждому столбцу датафрейма pandas
for col in X.columns:
# заменяем пропуски средним
# значением из словаря
X[col] = np.where(X[col].isnull(),
self._encoder_dict[col],
X[col])
# transform возвращает X
return X

Рис. 21 Метод transform для применения параметров

В тело метода .transform() мы первым делом добавили копирование
объекта. В противном случае произойдет срабатывание предупреждения
SettingWithCopy. Оно звучит так: A value is trying to be set on a copy of a slice

188



Инструменты

from a DataFrame (Происходит попытка изменить значение в копии среза данных датафрейма).
Звучит несколько туманно, было бы лучше, если бы сообщение выглядело
примерно так: You are attempting to make an assignment on an object that
is either a view or a copy of a DataFrame. This occurs whenever you make a
subset selection from a DataFrame and then try to assign new values to this
subset (Вы пытаетесь применить операцию присваивания к объекту, который является либо представлением, либо копией объекта DataFrame.
Это происходит всякий раз, когда вы отбираете подмножество и затем
пытаетесь присвоить этому подмножеству новые значения). Одним словом, библиотеке pandas важно знать, с чем она работает – копией или представлением. С помощью метода .copy() мы избавляемся от этого предупреждения.
Мы вновь проверяем, не является ли массив NumPy одномерным, записываем количество столбцов в переменную ncols, записываем результат метода
__isnumpy в переменную is_np.
В заключение по каждой переменной выполняем замену пропусков средним значением, взятым из словаря. Способ итерирования по столбцам определяется значением переменной is_np. Вновь для массива NumPy используем
цикл for c функцией range(), в качестве аргумента функции range() используем ncols. Замена пропусков средними значениями выполняется с помощью
функции библиотеки NumPy np.nan_to_num(). Для датафрейма pandas используем цикл for со списком имен переменных, получаемым с помощью свойства columns. Замена пропусков средними значениями выполняется с помощью функции библиотеки NumPy np.where() и метода .isnull() библиотеки
pandas. Первый аргумент функции np.where – проверяемое условие, второй
аргумент – что возвращаем, если условие выполняется, третий аргумент – что
возвращаем, если условие не выполняется.
Теперь создаем игрушечные обучающий и тестовый наборы данных и проверяем на них наш класс. Качественно написанный класс должен работать как
с датафреймами pandas (необходимо для удобства работы в pandas при создании первого прототипа, наброска модели), так и с массивами NumPy (необходимо для совместимости с конвейерами scikit-learn для проверки гипотез, связанных с гиперпараметрами моделей предподготовки и моделей машинного
обучения). Начнем с игрушечных датафреймов pandas.
# создаем игрушечный обучающий датафрейм pandas
toy_train = pd.DataFrame(
{'Balance': [8.3, np.NaN, 10.2, 3.1],
'Age': [23, 29, 36, np.NaN]})
toy_train

7.1. Основы работы с классами, строящими модели...  189
# создаем игрушечный тестовый датафрейм pandas
toy_test = pd.DataFrame(
{'Balance': [10.4, np.NaN, 22.5, 1.1],
'Age': [13, 19, 66, np.NaN]})
toy_test

Теперь посмотрим, какими должны быть значения в обучающем и тестовом
датафреймах pandas.
# смотрим, как будут выглядеть преобразования в игрушечных
# обучающем и тестовом датафреймах pandas
for col in toy_train.columns:
toy_train[col].fillna(toy_train[col].mean(), inplace=True)
toy_test[col].fillna(toy_train[col].mean(), inplace=True)
print('обучающий датафрейм')
print(toy_train)
print('')
print('тестовый датафрейм')
print(toy_test)

0
1
2
3

Balance
8.3
7.2
10.2
3.1

Age
23.000000
29.000000
36.000000
29.333333

0
1
2
3

Balance
10.4
7.2
22.5
1.1

Age
13.000000
19.000000
66.000000
29.333333

Создаем экземпляр нашего класса MeanImputer, обучаем (вычисляем параметр –
среднее значение по каждой переменной) и применяем (заменяем пропуски
с помощью найденного параметра – среднего значения по каждой переменной).
# создаем экземпляр класса MeanImputer
imp = MeanImputer()
# обучаем модель
imp.fit(toy_train)
# выполняем преобразование игрушечного
# обучающего датафрейма pandas
toy_train = imp.transform(toy_train)
toy_train

190



Инструменты

# выполняем преобразование игрушечного
# тестового датафрейма pandas
toy_test = imp.transform(toy_test)
toy_test

Видим, что значения, полученные с помощью класса MeanImputer, совпадают
с ожидаемыми. Теперь применим наш класс для отдельной переменной датафрейма, отключив копирование. Обратите внимание на двойные квадратные скобки вокруг переменной Age, это необходимо, чтобы получить 2-мерный массив признаков, как того требует библиотека scikit-learn.
# создаем игрушечный обучающий датафрейм pandas
toy_train = pd.DataFrame(
{'Balance': [8.3, np.NaN, 10.2, 3.1],
'Age': [23, 29, 36, np.NaN]})
# создаем экземпляр класса, отключив копирование
imp = MeanImputer(copy=False)
# обучаем модель
imp.fit(toy_train[['Age']])
# применяем модель
toy_train['Age'] = imp.transform(toy_train[['Age']])
toy_train

/anaconda3/lib/python3.7/site-packages/ipykernel_launcher.py:57: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a SettingWithCopyWarning:
DataFrame. Try using
/anaconda3/lib/python3.7/site-packages/ipykernel_launcher.py:57:
= value
instead
the caveats
in the documentation:
A.loc[row_indexer,col_indexer]
value is trying to be set on a copy
of a slice
from aSee
DataFrame.
Try using
http://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view.loc[row_indexer,col_indexer]
= value instead See the caveats in the documentation:
http://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-viewversus-a-copy
versus-a-copy

7.1. Основы работы с классами, строящими модели...  191
Видим, что хотя преобразование выполнено, оно сопровождается выдачей
предупреждения SettingWithCopy.
Давайте включим копирование и проделаем все то же самое.
# создаем игрушечный обучающий датафрейм pandas
toy_train = pd.DataFrame(
{'Balance': [8.3, np.NaN, 10.2, 3.1],
'Age': [23, 29, 36, np.NaN]})
# создаем экземпляр класса, включив копирование
imp = MeanImputer(copy=True)
# обучаем модель
imp.fit(toy_train[['Age']])
# применяем модель
toy_train['Age'] = imp.transform(toy_train[['Age']])
toy_train

Видим, что предупреждение SettingWithCopy не выводится.
Теперь проверим работу класса с массивами NumPy.
# создаем игрушечный обучающий массив NumPy
np_toy_train = np.array(pd.DataFrame(
{'Balance': [8.3, np.NaN, 10.2, 3.1],
'Age': [23, 29, 36, np.NaN]}))
np_toy_train
array([[ 8.3,
[ nan,
[10.2,
[ 3.1,

23. ],
29. ],
36. ],
nan]])

# создаем игрушечный тестовый массив NumPy
np_toy_test = np.array(pd.DataFrame(
{'Balance': [10.4, np.NaN, 22.5, 1.1],
'Age': [13, 19, 66, np.NaN]}))
np_toy_test
array([[10.4,
[ nan,
[22.5,
[ 1.1,

13. ],
19. ],
66. ],
nan]])

# обучаем модель
imp.fit(np_toy_train)
# выполняем преобразование игрушечного
# обучающего массива NumPy
np_toy_train = imp.transform(np_toy_train)

192



Инструменты

np_toy_train
array([[ 8.3
[ 7.2
[10.2
[ 3.1

,
,
,
,

23.
],
29.
],
36.
],
29.33333333]])

# выполняем преобразование игрушечного
# тестового массива NumPy
np_toy_test = imp.transform(np_toy_test)
np_toy_test
array([[10.4
[ 7.2
[22.5
[ 1.1

,
,
,
,

13.
],
19.
],
66.
],
29.33333333]])

Опять видим, что значения, полученные с помощью класса MeanImputer, совпадают с ожидаемыми. А теперь проверим работу нашего класса с одномерным массивом NumPy.
# создаем 1D-массив NumPy
array = np.array([8.3, np.NaN, 10.2, 3.1])
array
array([ 8.3, nan, 10.2, 3.1])
# проверяем размерность массива
array.ndim

1
# проверяем работу класса
imp.fit(array)
array = imp.transform(array)
array
array([[ 8.3],
[ 7.2],
[10.2],
[ 3.1]])
# проверяем размерность массива
array.ndim

2
Часто вам придется работать с уже готовыми классами, выполнять их отладку, улучшать их, а для этого нужно подробно изучить тело класса. Не всегда все
операции, происходящие внутри класса, будут очевидны, поэтому можно выполнить декомпозицию класса, разложить его на более простые компоненты.
Давайте выполним частичную декомпозицию нашего класса MeanImputer(),
подробно разберемся, что происходит внутри метода .fit(), на примере датафрейма pandas и массива NumPy.

7.1. Основы работы с классами, строящими модели...  193
# создаем игрушечный датафрейм pandas
toy_train = pd.DataFrame(
{'Balance': [8.3, np.NaN, 10.2, 3.1],
'Age': [23, 29, 36, np.NaN]})
# создаем пустой словарь encoder_dict
encoder_dict = {}
# по каждой переменной
for col in toy_train.columns:
# печатаем имя
print(col)
# вычисляем среднее и записываем в словарь
encoder_dict[col] = toy_train[col].mean()
# печатаем словарь
print(encoder_dict)
print('')
# печатаем итоговый словарь
print('итоговый словарь', encoder_dict)
Balance
{'Balance': 7.2}
Age
{'Balance': 7.2, 'Age': 29.333333333333332}
итоговый словарь {'Balance': 7.2, 'Age': 29.333333333333332}

Видим, что мы создаем пустой словарь, а потом на каждой итерации добавляем пару «ключ-значение», ключами будут имена переменных, а значениями – средние значения.
# создаем игрушечный обучающий массив NumPy
np_toy_train = np.array(pd.DataFrame(
{'Balance': [8.3, np.NaN, 10.2, 3.1],
'Age': [23, 29, 36, np.NaN]}))
# создаем пустой словарь encoder_dict
encoder_dict = {}
# по каждой переменной
for col in range(np_toy_train.shape[1]):
# печатаем имя
print(col)
# вычисляем среднее и записываем в словарь
encoder_dict[col] = np.nanmean(np_toy_train[:, col])
# печатаем словарь
print(encoder_dict)
print('')
# печатаем итоговый словарь
print('итоговый словарь', encoder_dict)
0
{0: 7.2}
1
{0: 7.2, 1: 29.333333333333332}
итоговый словарь {0: 7.2, 1: 29.333333333333332}

194



Инструменты

Видим, что мы создали пустой словарь, а потом на каждой итерации добавляем пару «ключ-значение», ключами будут целочисленные значения, соответствующие переменным, а значениями – средние значения.
После построения моделей предварительной подготовки мы строим модель
машинного обучения.
При работе с классом – моделью машинного обучения – мы выполняем следующие операции:
 импортируем из соответствующего модуля класс, в котором реализована
интересующая модель машинного обучения;
 создаем экземпляр класса – объект-модель;
 обучаем модель, т. е. вычисляем параметры модели, с помощью которых
будем получать прогнозы, – используем метод .fit() объекта-модели;
 применяем модель, т. е. вычисляем прогнозы:
 для вычисления спрогнозированных значений зависимой переменной –
используем метод .predict() объекта-модели;
 для вычисления вероятностей классов зависимой переменной – используем метод .predict_proba() объекта-модели;
 оцениваем качество модели c помощью полученных прогнозов и/или
вероятностей – используем метод .score() объекта-модели (по умолчанию метрикой качества для классификаторов будет правильность, для
регрессоров – R-квадрат).
Самый простой способ реализовать собственный класс, строящий модель машинного обучения, – воспользоваться наследованием базового класса BaseEstimator. Наш класс, как и любой класс предварительной подготовки
библио­теки scikit-learn, должен иметь методы __init__, .fit() и .predict(). Наш
класс должен иметь методы __get_params__ и __set_params__ (они наследуются
от базового класса BaseEstimator), с помощью этих методов мы можем задавать
и получать доступ к параметрам. Базовые классы RegressorMixin и ClassifierMixin
позволяют нам воспользоваться методом .score(), для задачи регрессии будет
вычислен R-квадрат, для задачи классификации – правильность.
Даже если вы будете реализовывать модель машинного обучения без учителя, метод .fit() должен принимать в качестве аргументов X и y. Это требуется
для совместимости с конвейерами scikit-learn! Обычно для y задают значение
None. В методе .fit() происходит вычисление параметров модели и всегда возвращается self.
Остальные требования будут такими же, как для классов, строящих модели предварительной подготовки. Все атрибуты инициализируются в методе
__init__. Никаких операций с атрибутами в методе __init__ не должно быть.
Названия параметров должны совпадать с названиями атрибутов. Вспомогательные методы, предназначенные для правильной внутренней работы класса, оформляются в виде частных методов. Импорт библиотек, классов и модулей внутри методов не допускается.
Давайте посмотрим, как выглядят модели машинного обучения изнутри.
Метод ближайших соседей (k nearest neighbors – KNN), возможно, является
самым простым методом машинного обучения. Построение модели заключа-

7.1. Основы работы с классами, строящими модели...  195
ется в запоминании обучающего набора данных. Для того чтобы сделать прогноз для новой точки данных, метод находит ближайшие к ней точки обучающего набора, то есть находит «ближайших соседей».
Начнем с классификации. В простейшем варианте метод ближайших соседей
рассматривает лишь одного ближайшего соседа – точку, ближе всего расположенную к точке, которую мы хотим классифицировать. Прогнозом будет ответ,
уже известный для данной точки обучающего набора. Если мы рассматриваем
более одного соседа, то для присвоения метки используется голосование (voting).
Это означает, что в случае бинарной классификации для каждой точки тестового
набора мы подсчитываем количество соседей, относящихся к классу 0, и количество соседей, относящихся к классу 1. Затем мы присваиваем точке тестового
набора наиболее часто встречающийся класс: другими словами, мы выбираем
класс, набравший большинство среди k ближайших соседей.
Для регрессии мы просто используем усреднение, т. е. вычисляем среднее
значение по меткам соседей.
Давайте напишем класс KNN_Estimator, в котором реализуем метод ближайших соседей для классификации и регрессии.
# пишем класс KNN-модели
class KNN_Estimator():
"""
KNN-модель.
Параметры:
----------k: int, по умолчанию 2
Количество ближайших соседей, которое определяет
класс/значение предсказываемого наблюдения.
task: string, 'classification' по умолчанию
Тип решаемой задачи.
"""
# пишем защищенный метод, вычисляющий евклидово расстояние
def _euclidean_distance(self, x1, x2):
"""
Вычисляет евклидово расстояние между двумя векторами.
"""
distance = 0
for i in range(len(x1)):
distance += pow((x1[i] - x2[i]), 2)
return math.sqrt(distance)
# пишем защищенный метод голосования
def _vote(self, neighbor_labels):
"""
Возвращает самый часто встречающийся класс
среди ближайших соседей.
"""
# подсчитываем абсолютные частоты классов
# для каждого наблюдения
counts = np.bincount(neighbor_labels.astype('int'))
# возвращаем индекс максимального значения –
# максимальной абсолютной частоты
return counts.argmax()

196



Инструменты

def __init__(self, k=5, task='classification'):
# инициализируем k – количество ближайших соседей
self.k = k
# решаемая задача
self.task = task
# создаем пустой список, в котором будем
# хранить ближайших соседей для
# каждого наблюдения набора
self.k_nearest_neighbors_ = []
def fit(self, X, y):
# просто запоминаем обучающий массив признаков
# и обучающий массив меток
self.X_memorized = X
self.y_memorized = y
def predict(self, X):
# создаем массив прогнозов, равный
# длине тестового набора
y_pred = np.empty(X.shape[0])
# для каждого наблюдения тестового набора
# предсказываем наиболее часто встречающийся
# класс / среднее значение среди k ближайших соседей
if self.task == 'classification':
for i, test_sample in enumerate(X):
idx = np.argsort([self._euclidean_distance(
test_sample, x) for x in self.X_memorized])[:self.k]
k_nearest_neighbors = np.array(
[self.y_memorized[i] for i in idx])
self.k_nearest_neighbors_.append(k_nearest_neighbors)
y_pred[i] = self._vote(self.k_nearest_neighbors_[i])
if self.task == 'regression':
for i, test_sample in enumerate(X):
idx = np.argsort([self._euclidean_distance(
test_sample, x) for x in self.X_memorized])[:self.k]
k_nearest_neighbors = np.array(
[self.y_memorized[i] for i in idx])
self.k_nearest_neighbors_.append(k_nearest_neighbors)
y_pred[i] = np.mean(self.k_nearest_neighbors_[i])
return y_pred

Давайте создадим обучающий массив признаков, обучающий массив меток
для классификации, тестовый массив признаков.
# создаем обучающий массив признаков
X_trn = np.array([[0.1, 0.2, 0.3],
[0.7, 0.5, 0.2],
[0.1, 0.2, 0.2],
[0.9, 0.7, 3.5],
[0.2, 0.4, 1.4],
[0.4, 0.1, 0.5]])
# создаем обучающий массив меток для классификации
y_trn = np.array([1, 0, 1, 0, 0, 1])

7.1. Основы работы с классами, строящими модели...  197
# создаем тестовый массив признаков
X_tst = np.array([[0.1, 0.7, 1.1],
[0.5, 0.3, 2.8],
[0.1, 0.1, 0.2],
[0.9, 0.7, 1.5]])

Теперь обучаем модель KNN-классификации и получаем прогнозы для тес­
тового массива признаков.
# обучаем модель KNN-классификации
knn = KNN_Estimator(k=3, task='classification')
knn.fit(X_trn, y_trn)
# получаем прогнозы для тестового
# массива признаков
pred = knn.predict(X_tst)
pred
array([1., 0., 1., 0.])

Посмотрим ближайших соседей по каждому наблюдению тестового массива
признаков.
# посмотрим ближайших соседей по каждому
# наблюдению тестового массива признаков
knn.k_nearest_neighbors_
[array([0, 1, 1]), array([0, 0, 1]), array([1, 1, 1]), array([0, 1, 0])]

Создаем обучающий массив меток для регрессии.
# создаем обучающий массив меток для регрессии
y_trn = np.array([1.2, 0.5, 1.4, 2.2, 3.5, 5.9])

Теперь обучаем модель KNN-регрессии и получаем прогнозы для тестового
массива признаков.
# обучаем модель KNN-регрессии
knn = KNN_Estimator(k=3, task='regression')
knn.fit(X_trn, y_trn)
# получаем прогнозы для тестового
# массива признаков
pred = knn.predict(X_tst)
pred
array([3.53333333, 3.86666667, 2.83333333, 3.3

])

Посмотрим ближайших соседей по каждому наблюдению тестового массива
признаков.
# посмотрим ближайших соседей по каждому
# наблюдению тестового массива признаков
knn.k_nearest_neighbors_

198



Инструменты

[array([3.5,
array([2.2,
array([1.4,
array([3.5,

5.9,
3.5,
1.2,
5.9,

1.2]),
5.9]),
5.9]),
0.5])]

Задачи с собеседований (Python)
1. Напишите функцию, которая находит общие элементы, кратные 7, в двух
списках ниже:
a = [2, 4, 7, 9, 14, 20, 21, 22]
b = [3, 5, 8, 10, 14, 20, 21, 30]

2. Напишите функцию, возвращающую список из элемента первого списка,
отсортированный по элементам второго (порядок элементов по совпадающему «ключу» второго не важен):
a = ["a", "b", "c", "d", "e", "f"]
b = [1, 0, 9, 3, 2, 0]

7.2. Строим свой первый конвейер моделей
Давайте начнем работу с данными. Для этого нужно импортировать необходимые библиотеки, модуль os, функцию train_test_split() и классы
StandardScaler и LogisticRegression. Модуль os позволяет взаимодействовать
с операционной системой – узнавать/менять файловую структуру, переменные среды, узнавать имя и права пользователя и др. Функция train_test_
split() разбивает данные на обучающие и тестовые массивы признаков и меток. Класс StandardScaler потребуется для предварительной подготовки. Класс
LogisticRegression необходим для построения модели машинного обучения –
логистической регрессии.
# импортируем библиотеки pandas, numpy
import pandas as pd
import numpy as np
# импортируем модуль os и функцию train_test_split()
import os
from sklearn.model_selection import train_test_split
# импортируем класс StandardScaler,
# выполняющий стандартизацию
from sklearn.preprocessing import StandardScaler
# импортируем класс LogisticRegression
from sklearn.linear_model import LogisticRegression

Взглянем на наш рабочий каталог.
# взглянем на наш рабочий каталог
os.getcwd()
'/Users/artemgruzdev/Documents/GitHub/Data Preprocessing in Python'

7.2. Строим свой первый конвейер моделей  199
Если данные лежат в другом каталоге, можем сменить его с помощью метода os.chdir().
Давайте загрузим данные с помощью функции read_csv() библиотеки
pandas. Они записаны в файле StateFarm.csv, который находится в каталоге
Data нашего рабочего каталога '/Users/artemgruzdev/Documents/GitHub/Data
Preprocessing in Python'.
Взглянем на данные.
# записываем CSV-файл в объект DataFrame
data = pd.read_csv('Data/StateFarm.csv', sep=';')
# смотрим данные
data.head(3)

Перед нами – данные американской автостраховой компании StateFarm.
Они представляют собой записи о 8262 клиентах, классифицированных на два
класса: 0 – отклика нет на предложение автостраховки (7435 клиентов) и 1 –
отклик есть на предложение автостраховки (827 клиентов). По каждому наблюдению (клиенту) фиксируются следующие переменные (характеристики):
 количественный признак Пожизненная ценность клиента [Customer
Lifetime Value];
 количественный признак Доход клиента [Income];
 количественный признак Размер ежемесячной автостраховки [Monthly
Premium Auto];
 количественный признак Количество месяцев со дня подачи последнего
страхового требования [Months Since Last Claim];
 количественный признак Количество месяцев с момента заключения
страхового договора [Months Since Policy Inception];
 количественный признак Количество открытых страховых обращений
[Number of Open Complaints];
 количественный признак Количество полисов [Number of Policies];
 бинарная зависимая переменная Отклик на предложение автостраховки
[Response].
Мы будем учить модель правильно классифицировать клиентов на неоткликнувшихся и откликнувшихся. Метрикой качества будет правильность
(accuracy). Правильность – это количество правильно спрогнозированных наблюдений, поделенное на общее количество наблюдений.
Обязательно нужно выполнить проверку (валидацию) модели, т. е. посмот­
реть, как модель выдает прогнозы на данных, не участвовавших в обучении.
Самый простой способ проверки – случайное разбиение на обучающую и тес­
товую выборки.

200



Инструменты
Все данные
Обучающие

Тестовые

Рис. 22 Разбиение данных на обучающую и тестовую выборки

Сначала мы случайным образом разбиваем имеющиеся данные на две выборки: обучающую и тестовую. Формирование тестовой выборки – это способ
преодолеть такие несовершенства неидеального мира, как ограничения в объеме данных и ресурсов, а также невозможность получения дополнительных
данных из порождающего распределения. В данном случае тестовая выборка
должна представлять собой новые, еще неизвестные модели данные. Важно
использовать тестовую выборку лишь однократно. Обычно 2/3 доступных данных назначают в обучающую выборку, а оставшуюся 1/3 данных – в тестовую
выборку. Другими популярными методами разбиения на обучающую/тестовую выборки являются 60/40, 70/30, 80/20 или даже 90/10, если набор данных
относительно велик.
Затем необходимо построить на обучающей выборке (обучить) модели
предварительной подготовки, например модель импутации, модель стандартизации и только потом модель машинного обучения, которая, как мы предполагаем, может оказаться подходящей для решения данной задачи. Таким
образом, в реальности мы чаще всего строим конвейер моделей.
После обучения моделей возникает закономерный вопрос: а насколько «хорошо» качество полученного конвейера? И вот теперь наступает время использовать независимую тестовую выборку. Поскольку модели внутри конвейера
еще «не видели» эти тестовые данные, такой шаг даст относительно надежную
и несмещенную оценку качества на новых, незнакомых данных. Мы берем
тес­товую выборку и используем последнюю модель конвейера – модель машинного обучения для прогнозирования меток классов зависимой переменной по наблюдениям тестовой выборки. Затем мы берем спрогнозированные
метки классов и сравниваем их с фактическими метками классов для оценки
качества. Однако есть несколько тонких моментов.
У любой модели есть параметры, которые мы находим в ходе обучения. Например, у нас будет класс SimpleImputer, который обучает модель предварительной подготовки – модель импутации (замены) пропущенных значений.
Параметрами модели импутации будут статистики, которые мы используем
для импутации пропусков (среднее, мода, медиана).
Часто нам придется пользоваться классом StandardScaler, строящим модель
стандартизации. Самая простая стандартизация подразумевает, что из каждого значения переменной мы вычтем среднее значение и полученный результат разделим на стандартное отклонение (в случае присутствия бинарных
переменных для улучшения интерпретируемости делят на два стандартных
отклонения):
xi – mean(x)
.
stdev(x)

7.2. Строим свой первый конвейер моделей  201

Параметрами модели стандартизации будут статистики, которые мы используем для стандартизации (среднее и стандартное отклонение).
Стандартизация необходима для некоторых методов машинного обучения,
в частности для линейных моделей, которые мы будем строить чуть позже.
Она приводит количественные независимые переменные к единому масштабу. Поэтому стандартизацию еще называют масштабированием, не путайте
с масштабируемостью – свойством метода или модели демонстрировать приемлемую или высокую скорость вычислений с увеличением размера набора
данных (например, метод градиентного бустинга, реализованный в LightGBM,
имеет хорошую масштабируемость, а метод иерархического кластерного анализа имеет плохую масштабируемость, поскольку медленно выполняется на
больших наборах данных). Если не привести признаки к единому масштабу,
то прогноз будут определять признаки, имеющие наибольший разряд и соответственно наибольшую дисперсию. Различный масштаб признаков приведет
к ухудшению сходимости в случае применения градиентного спуска (например, в логистической регрессии градиентный спуск используют для поиска таких регрессионных коэффициентов, при которых мы получаем наименьшее
значение логистической функции потерь). Кроме того, единый масштаб позволит нам сравнивать регрессионные коэффициенты при признаках между
собой. Категориальные признаки стандартизировать не нужно. Мы подробнее
поговорим обо всем этом во втором томе.
Для класса LogisticRegression, строящего модель машинного обучения –
модель логистической регрессии, параметрами будут регрессионные коэффициенты для соответствующих признаков. Регрессионные коэффициенты мы
находим в ходе градиентного спуска, лежащего в основе обучения логистической регрессии.
Для класса DecisionTreeClassifier, строящего иную модель машинного
обуче­ния – дерево решений CART, параметрами будут правила расщепления
(признак расщепления и расщепляющее значение). Правила расщепления мы
находим в ходе процедуры рекурсивного разбиения данных на узлы.
Помимо параметров, у модели есть гиперпараметры. Параметры мы находим в ходе обучения модели. А вот гиперпараметры – это параметры, которые
нельзя «выучить» в процессе обучения, они сами регулируют ход обучения, их
задают перед обучением модели и настраивают на отложенной выборке.
Модель импутации не может самостоятельно выяснить оптимальную стратегию импутации. Поэтому стратегия импутации – это гиперпараметр (гиперпараметр strategy класса SimpleImputer), который позволяет улучшить качест­
во модели и настраивается на отложенной выборке.

202



Инструменты

Модель стандартизации не может самостоятельно выяснить оптимальную
стратегию стандартизации. Поэтому можно подбирать стандартизацию (например, вместо класса StandardScaler попробовать класс MinMaxScaler), и это
позволяет улучшить качество модели.
Логистическая регрессия не может в процессе обучения самостоятельно выяснить оптимальное значение силы регуляризации. Поэтому сила
регуляризации – это гиперпараметр модели (гиперпараметр C класса
LogisticRegression), который позволяет улучшить качество модели и настраивается на отложенной выборке. Дерево решений CART не может самостоя­
тельно выяснить оптимальное значение максимальной глубины. Поэтому
максимальная глубина – это тоже гиперпараметр (гиперпараметр max_depth
класса DecisionTreeClassifier, строящего дерево CART), который мы настраиваем на отложенной выборке.
Не путайте параметры и гиперпараметры с параметрами – именами, которые указываются при объявлении метода __init__ класса и являются нашими
внешними способами обратиться к атрибуту, находящемуся внутри класса.
И вот когда мы строим модели с разными значениями гиперпараметров на
обучающей выборке, а проверяем их качество на тестовой выборке, возникает проблема. Мы используем тестовую выборку и для настройки гиперпараметров, и для оценки качества модели. Поскольку мы использовали тестовую
выборку для настройки гиперпараметров, мы больше не можем использовать
ее для оценки качества модели. Теперь для оценки качества модели нам необходим независимый набор данных, то есть набор, который не использовался
для построения модели и настройки ее гиперпараметров и применяется лишь
однократно для оценки качества модели. Помним, что тестовая выборка –
прообраз новых данных, о которых мы ничего не знаем.
Допустим, у нас есть набор данных из одного признака и зависимой переменной. Мы можем выделить обучающую выборку – для обучения модели, валидационную выборку – для настройки гиперпараметров и тестовую
выборку – для итоговой оценки качества выбранной модели. Затем мы
создадим конвейер, состоящий, допустим, из модели импутации и модели логистической регрессии (без константы). Несколько раз обучим его на
обуча­ющей выборке, используя разные значения гиперпараметров – разные
стратегии импутации и разные значения силы регуляризации. Каждый раз
будем получать параметры – разные статистики для импутации и разные регрессионные коэффициенты. Ищем лучшую стратегию импутации и лучшее
значение силы регуляризации, анализируя качество прогнозов на валидационной выборке. Валидационная выборка нужна только для настройки, это
своего рода площадка, на которой кандидаты-гиперпараметры «соревнуются» между собой. Допустим, нашли конвейер с наилучшими гиперпараметрами. Для модели импутации лучшей стратегией оказалась замена медианой,
а лучшим значением силы регуляризации для логистической регрессии стало
значение 10. У модели импутации будет параметр – значение медианы для
признака, у модели логистической регрессии параметром будет регрессионный коэффициент.

7.2. Строим свой первый конвейер моделей  203
Гиперпараметр
Параметр

32.5
Imputer('median').fit(X_tr, y_tr) → Imputer('median').transform(X_tr) → LogReg(C=10).fit(X_tr, y_tr) → coef -0.15

X_tr 20 25 NaN 40 50
y_tr 0 1
0
1 0

20 25 32.5 40 50
0 1
0
1
0

20
0

25 32.5 40 50
1
0
1 0

32.5
Imputer('median').transform(X_val) → LogReg(C=10).predict(X_val) → coef -0.15

X_val NaN 44 NaN 34 48
y_val
0
0
1
1 0

32.5 44 32.5 34 48
0
0
1
1 0

32.5 44 32.5 34 48 ACC 0.9
0
0
1
1 0

Рис. 23 Обучение конвейера на обучающей выборке и применение к валидационной выборке

Здесь же заметим, что для импутации пропусков в обучающей и валидационной выборках мы можем использовать только медианы признаков, полученные на этапе обучения. И так с любой моделью предварительной подготовки, у которой параметрами будут статистики! Нельзя отдельно вычислить статистики для импутации на обучающей выборке, а затем отдельно вычислить
статистики для импутации на валидационной выборке и потом использовать
эти значения для импутации признака в соответствующей выборке. Помним,
что вычисление статистик для импутации – это тоже модель предварительной подготовки данных, которую мы строим на обучающей выборке и применяем ее к переменным обучающей и валидационной выборок. С помощью
валидационной выборки – прообраза новых данных – мы можем проверить
эффективность модели импутации (модель импутации средним, модель импутации медианой). Например, если бы мы добавили стандартизацию, то для
стандартизации переменных в обучающей и валидационных выборках мы
могли бы использовать только среднее и стандартное отклонение переменных
в обучающей выборке. Вычисление среднего и стандартного отклонения для
каждой стандартизируемой переменной – это тоже модель предварительной
подготовки данных, которую мы строим на обучающей выборке и применяем
ее к переменным обучающей и валидационной выборок. С помощью валидационной выборки – прообраза новых данных – мы можем проверить эффективность той или иной модели стандартизации (стандартизация с вычитанием из исходного значения переменной среднего и деления на стандартное отклонение, стандартизация с вычитанием из исходного значения переменной
минимального значения и деления на разницу между максимальным и минимальным значениями).
Итак, мы нашли наилучшую комбинацию гиперпараметров, нужно проверить, так ли она на самом деле хороша. Затем заново обучим конвейер моделей с найденными наилучшими гиперпараметрами на объединенной обучающе-валидационной выборке. Медиана для импутации и регрессионный коэффициент будут вычислены заново и будут отличаться от медианы и регрессионного коэффициента, полученных при обучении на обучающей выборке.

204



Инструменты

Для импутации пропусков в обучающе-валидационной и тестовой выборках
мы можем использовать только медиану признака, вычисленную на обучающе-валидационной выборке. Получаем итоговую оценку качества конвейера
на тестовой выборке.
40
Imputer('median').transform(X_tr_val)

Imputer('median').fit(X_tr_val, y_tr_val)
X_tr_val 20 25 NaN 40 50 NaN 44 NaN 34 48
y_tr_val 0 1
0
1 0
0
0
1
1 0

LogReg(C=10).fit(X_tr_val, y_tr_val)
X_tr_val 20 25
y_tr_val 0 1

40
0

40 50
1 0

40
0

44
0

40
1

20 25 40 40 50 40 44 40 34 48
0 1 0 1 0 0 0 1 1 0

coef -0.148
34 48
1 0

40
Imputer('median').transform(X_tst)
X_tst
y_tst

NaN 44 45
1
0 1

40
1

44
0

45
1

coef -0.148
LogReg(C=10).predict(X_tst)
NaN 44
1
0

45
1

ACC 0.91

Рис. 24 Обучение конвейера на обучающе-валидационной выборке и применение
к тестовой выборке

Если итоговая оценка качества конвейера моделей на тестовой выборке
нас устроит, мы объединяем все три выборки, заново обучаем на объединенном наборе наш конвейер с найденными наилучшими гиперпараметрами и применяем к новым данным. Медиана для импутации и регрессионный коэффициент будут вычислены заново и будут отличаться от медианы
и регрессионного коэффициента, полученного при обучении на обучающе-валидационной выборке. Для импутации пропусков в объединенном
наборе (или, как его еще называют, историческом наборе) и новых данных
мы можем использовать только медиану признака, вычисленную на историческом наборе.

7.2. Строим свой первый конвейер моделей  205
Imputer('median').fit(X_hist, y_hist)
X_hist
X_hist

20
0

25
1

NaN
0

40
1

50
0

NaN
0

44
0

NaN
1

34
1

48
0

NaN
1

44
0

45
1

44
1

44
0

45
1

44
Imputer('median').transform(X_hist)
X_hist
X_hist

20
0

25
1

44
0

40
1

50
0

44
0

44
0

44
1

34
1

48
0

coef -0.149

LogReg(C=10).fit(X_hist, y_hist)
X_hist
X_hist

20
0

25
1

44
0

40
1

50
0

44
0

44
0

44
1

34
1

44
1

48
0

44
0

coef -0.149

44
Imputer('median').transform(X_new)
X_new

53

NaN

36

53

44

36

45
1

LogReg(C=10).predict(X_new)
53

44

36

Рис. 25 Обучение конвейера на исторических данных и применение к новым данным

В противном случае мы просто будем настраивать модели нашего конвейера под тестовую выборку, ведь любой выбор, сделанный исходя из метрики
на тестовом наборе, «сливает» модели информацию тестового набора. В итоге мы можем получить оптимистичные результаты. Полезно держать в голове
картинку: как только вы использовали тестовый набор для оценки качества
модели, он «сгорает».
Для простоты пока пренебрежем этим недостатком случайного разбиения
на обучающую и тестовую выборки, допустим, наша задача – построить модель стандартизации, а затем модель машинного обучения – модель логистической регрессии, не прибегая к поиску оптимальных гиперпараметров. Такое
часто бывает, когда, например, дана задача классификации и нужно сопоставить качество нескольких методов машинного обучения, строят базовые модели логис­тической регрессии, случайного леса и градиентного бустинга и сравнивают.Мы получим оценку качества модели на тестовых данных, и если качество нас устраивает, мы обучаем модель на всех доступных данных (т. е. объединеняем обучающую и тестовую выборки) и применяем модель, обученную
на всех доступных данных, к новым данным.
Давайте сделаем случайное разбиение данных на обучающую и тестовую
выборки: сформируем обучающий массив признаков, тестовый массив признаков, обучающий массив меток, тестовый массив меток. Это можно будет
сделать с помощью функции train_test_split() модуля model_selection биб­
лиотеки scikit-learn. В scikit-­learn для массива данных обычно используется
заглавная X, а для массива меток – строчная y.

206



Инструменты

# разбиваем данные на обучающие и тестовые: получаем обучающий
# массив признаков, тестовый массив признаков, обучающий массив
# меток, тестовый массив меток
X_train, X_test, y_train, y_test = train_test_split(
1 data.drop('Response', axis=1),
2 data['Response'],
3 test_size=0.3,
4 stratify=data['Response'],
5 random_state=42)
1

Указываем массив признаков. Для этого мы используем метод .drop() библиотеки pandas.
Этот метод удаляет зависимую переменную Response, при удалении мы перемещаемся по
оси столбцов, поэтому c помощью параметра axis указываем ось 1 (по умолчанию задана ось
0). По умолчанию операция не выполняется на месте (регулируется параметром inplace), в
противном случае мы удалим зависимую переменную Response из датафрейма data и уже не
сможем создать массив меток.

2

Затем создаем массив меток, просто указав зависимую переменную.

3
4

5

С помощью параметра test_size настраиваем нужный размер тестовой
выборки (в процентах). По умолчанию 0,25.
С помощью параметра stratify (по умолчанию не используется) можно задать
стратифицированное разбиение на обучение и тест, чтобы распределение
классов зависимой переменной в тестовой выборке соответствовало
распределению классов в обучающей.
Поскольку разбиение является случайным, надо позаботиться о
воспроизводимости результатов. Для этого с помощью параметра random_state
задаем стартовое значение генератора псевдослучайных чисел.

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

В нашем наборе только количественные признаки, если бы здесь были категориальные признаки, мы обязательно превратили бы их в количественные
с помощью дамми-кодирования (линейные модели работают только с количественными признаками, и для моделирования мы используем массивы NumPy,
каждый столбец которого должен быть количественным признаком, помним, что
датафреймы pandas внутренне преобразовываются в массивы NumPy). У нас каждый уровень категориальной переменной стал бы отдельным бинарным столбцом со значения­ми 0 или 1, и такие переменные не нужно стандартизировать.
С помощью метода .fit() мы строим модель standardscaler на обучающем
массиве признаков. В данном случае метод .fit() вычисляет параметры модели – среднее значение и стандартное отклонение для каждой переменной
обучающего массива признаков.
# обучаем модель стандартизации, т. е. по каждому признаку
# в обучающем массиве признаков вычисляем
# среднее значение признака и стандартное
# отклонение признака для трансформации
standardscaler.fit(X_train)

7.2. Строим свой первый конвейер моделей  207
Чтобы применить модель к нашим данным, то есть отмасштабировать
(scale) обучающие и тестовые данные, мы воспользуемся методом .transform().
Под капотом метод .transform() применяет параметры, найденные с помощью метода .fit(), – из каждого значения переменной обучающего и тестового
массивов признаков вычитает среднее значение соответствующей переменной в обучающем массиве признаков и делит на стандартное отклонение этой
переменной, также взятое по обучающему массиву признаков.
При этом значения NaN обрабатываются как пропущенные значения: игнорируются при обучении и сохраняются в ходе применения.
# применяем модель стандартизации к обучающему массиву признаков: из исходного
# значения признака вычитаем среднее значение признака, вычисленное
# по ОБУЧАЮЩЕМУ массиву признаков, и результат делим на стандартное
# отклонение признака, вычисленное по ОБУЧАЮЩЕМУ массиву признаков
X_train_standardscaled = standardscaler.transform(X_train)
# применяем модель стандартизации к тестовому массиву признаков: из исходного
# значения признака вычитаем среднее значение признака, вычисленное
# по ОБУЧАЮЩЕМУ массиву признаков, и результат делим на стандартное
# отклонение признака, вычисленное по ОБУЧАЮЩЕМУ массиву признаков
X_test_standardscaled = standardscaler.transform(X_test)

Ранее мы говорили, что для импутации пропусков и стандартизации признаков в обучающей выборке и выборке, которую используем для проверки, можно
использовать только статистики, полученные на этапе обучения. Теперь сделаем
более широкое обобщение. Если вы используете такие операции, как укрупнение редких категорий по порогу, импутацию пропусков статистиками, стандартизацию, биннинг и конструирование признаков на основе статистик (frequency
encoding, likelihood encoding), т. е. если вы используете любые операции, предполагающие вычисления по набору данных, они должны быть осуществлены после
разбиения на обучающую и тестовую выборки (если предполагается проверочная выборка, то после разбиения на обучающую и проверочную выборки, а потом после разбиения на обучающе-проверочную и тестовую выборки).
Если мы используем случайное разбиение на обучающую и тестовую выборки и выполняем перечисленные операции до разбиения, получается, что
при вычислении статистик для импутации пропусков, среднего и стандартного отклонения для стандартизации, правил биннинга, частот и вероятностей
положительного класса зависимой переменной в категориях признака для
frequency encoding и likelihood encoding соответственно использовались все
наблюдения набора, часть из которых потом у нас войдут в тестовую выборку (по сути, выборку новых данных). Поэтому получается, что статистики для
импутации, статистики для стандартизации, правила биннинга, частоты и вероятности положительного класса в категориях признака, которые мы получили на всем наборе, пришли к нам частично из «будущего» (из новой, тестовой выборки, которой по факту еще нет). Однако мы должны смоделировать
наиболее близкую к реальности ситуацию, когда у нас есть только обучающая
выборка, а никаких новых данных еще нет.
Теперь обучим модель машинного обучения – модель логистической регрессии,
реализованную в классе LogisticRegression. Логистическая регрессия используется для прогнозирования бинарной зависимой переменной: не вернет или вернет кредит, не откликнется или откликнется на предложение автостраховки.

208



Инструменты

Импортируем класс LogisticRegression,
LogisticRegression и обучаем.

создаем

экземпляр

класса

# создаем экземпляр класса LogisticRegression
logreg = LogisticRegression(solver='lbfgs', max_iter=200)
# обучаем модель логистической регрессии, т. е.
# находим параметры – регрессионные коэффициенты
logreg.fit(X_train_standardscaled, y_train)
# оцениваем качество модели на обучающих данных
print("Правильность на обучающей выборке: {:.3f}".format(
logreg.score(X_train_standardscaled, y_train)))
# оцениваем качество модели на тестовых данных
print("Правильность на тестовой выборке: {:.3f}".format(
logreg.score(X_test_standardscaled, y_test)))
Правильность на обучающей выборке: 0.900
Правильность на тестовой выборке: 0.900

Обратите внимание, что для печати результатов мы воспользовались методом .format(). Однако можно применить способ с использованием f-cтроки
или так называемый «быстрый» способ с использованием оператора % (позволяет избежать применения фигурных скобок внутри круглых скобок для форматирования количественных значений).
# применим для печати f-строки
train_score = logreg.score(X_train_standardscaled, y_train)
test_score = logreg.score(X_test_standardscaled, y_test)
print(f"Правильность на обучающей выборке: {train_score:.3f}")
print(f"Правильность на тестовой выборке: {test_score:.3f}")
Правильность на обучающей выборке: 0.900
Правильность на тестовой выборке: 0.900
# применим для печати оператор %
print("Правильность на обучающей выборке: %.3f" % train_score)
print("Правильность на тестовой выборке: %.3f" % test_score)
Правильность на обучающей выборке: 0.900
Правильность на тестовой выборке: 0.900

Теперь получим спрогнозированные классы и вероятности классов (более
точно – оценки уверенности модели в том или ином классе зависимой переменной).
# вычисляем спрогнозированные значения зависимой переменной
# для тестового массива признаков
logreg_predvalues = logreg.predict(X_test_standardscaled)
logreg_predvalues[:5]
array([0, 0, 0, 0, 0])
# вычисляем вероятности классов зависимой переменной
# для тестового массива признаков

7.2. Строим свой первый конвейер моделей  209
logreg_probabilities = logreg.predict_proba(
X_test_standardscaled)
logreg_probabilities[:5]
array([[0.91027855,
[0.89847518,
[0.88300257,
[0.90234211,
[0.92554507,

0.08972145],
0.10152482],
0.11699743],
0.09765789],
0.07445493]])

Давайте взглянем на константу и регрессионные коэффициенты. Для этого
нам надо воспользоваться атрибутами intercept_ и coef_.
# извлекаем константу
intercept = np.round(logreg.intercept_[0], 3)
intercept
-2.205
# извлекаем коэффициенты
coef = np.round(logreg.coef_, 3)
coef
array([[-0.022, 0.042, 0.079, -0.056, -0.004, -0.017, -0.109]])

С помощью уже знакомой функции zip() «сшиваем» константу и коэффициенты с названиями признаков.
# печатаем название "Константа"
print("Константа:", intercept)
# печатаем название "Регрессионные коэффициенты"
print("Регрессионные коэффициенты:")
# для удобства сопоставим каждому названию
# признака соответствующий коэффициент
for c, feature in zip(coef[0], X_train.columns):
print(feature, c)
Константа: -2.205
Регрессионные коэффициенты:
Customer Lifetime Value -0.022
Income 0.042
Monthly Premium Auto 0.079
Months Since Last Claim -0.056
Months Since Policy Inception -0.004
Number of Open Complaints -0.017
Number of Policies -0.109

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

210



Инструменты

Здесь мы использовали логистическую регрессию для прогнозирования отклика на предложение автостраховки и применили стандартизацию. Регрессионный коэффициент при признаке Количество полисов [Number of Policies]
отрицателен и равен –0,109, это обозначает, что при увеличении количества
полисов на одно стандартное отклонение (что примерно составляет 2,368, мы
просто вычисляем стандартное отклонение переменной Количество полисов)
натуральный логарифм шансов отклика уменьшается на 0,109. Регрессионный коэффициент при признаке Доход клиента [Income] положителен и равен
0,042, это обозначает, что при увеличении дохода клиента на одно стандартное отклонение (что примерно составляет 30652,718, мы просто вычисляем
стандартное отклонение переменной Доход клиента) натуральный логарифм
шансов отклика увеличивается на 0,042.

7.3. Разбираемся с дилеммой смещения–дисперсии
и знакомимся с бутстрепом
В прогнозном моделировании нам важно построить модель на обучающих
данных, а затем получить точные прогнозы для новых, еще не встречавшихся данных, состоящих из тех же самых признаков, что и использованная
нами обучающая выборка. Мы помним, что прообразом этих новых данных
является валидационная или тестовая выборка. Если модель может выдавать точные прогнозы на ранее не встречавшихся данных, можно сказать,
что модель обладает способностью обобщать результат на новые данные.
Нам требуется построить модель с максимальной обобщающей способностью
(generalization). Для задач классификации в качестве метрики обобщающей
способности обычно используется правильность, AUC-ROC, для задач регрессии – корень из среднеквадратичной ошибки, R-квадрат. Определение
правильности было дано выше, а смысл других метрик кратко поясним. Если
говорить упрощенно, AUC-ROC – это вероятность того, что классификатор
присвоит случайно отобранному наблюдению положительного класса более
высокий ранг, чем наблюдению отрицательного класса (если пренебречь
вероятностью того, что ранг обоих будет одинаковым). Значение AUC-ROC
варьирует от 0,5 до 1. Чем выше, тем лучше. R2 показывает процент вариации или изменчивости зависимой переменной, который объясняется моделью. R2 принимает значения от 0 до 1. Чем выше, тем лучше. RMSE в среднем
измеряет, насколько наши прогнозы по модели отличаются от фактических
значений. Метрика RMSE, в отличие от R2, не масштабирована в определенном диапазоне, она имеет размерность исследуемой величины (и в этом
тоже есть удобство). Чем меньше, тем лучше. В четвертой части мы дадим
более точные определения метрик.
Если обучающий набор и новые данные имеют много общего между собой,
можно ожидать, что модель будет точно прогнозировать новые данные. Однако в ряде случаев на новых данных модель работает существенно хуже. Почему
так происходит?
Причина заключается в излишней сложности модели. На этапе подготовки данных часто отсутствует априорная информация о полезности тех или

7.3. Разбираемся с дилеммой смещения–дисперсии...  211
иных признаков. Избыточное включение признаков, не несущих новой информации, ведет к тому, что модель становится слишком сложной. Модель
может быть слишком сложной и по своей структуре в силу неверно подобранных гиперпараметров: слишком большая глубина деревьев, слишком
большое количество слоев в нейронной сети. Очень сложная модель слишком точно подстраивается под особенности обучающего набора, улавливает не только фактические взаимосвязи, но и случайные возмущения обучающих данных. По сути, такая модель восстанавливает не только искомую
зависимость, но и выполняет подгонку конкретных наблюдений, фактически осуществляет аппроксимацию погрешностей собственных измерений.
В итоге мы получаем модель, которая идеально работает на обучающем наборе, но плохо обобщает результат на новые данные, поскольку описывает
случайные возмущения в данных, не имеющие ничего общего с истинной
формой связи между зависимой переменной и признаками. Такую ситуацию называют переобучением (overfitting).
С другой стороны, включение недостаточного числа полезных признаков,
упрощение структуры модели за счет уменьшения глубины деревьев, количества слоев нейронной сети, наоборот, приводит к тому, что модель не может в достаточной мере уловить фактические зависимости, и качество модели даже на обучающей выборке остается довольно низким. Такую ситуацию
называют недообучением (underfitting).
Вы можете диагностировать недообучение по большой ошибке модели
в обучающей выборке наряду с большой ошибкой модели в тестовой выборке. Борьба с недообучением будет заключаться в том, что мы усложняем модель с помощью настройки гиперпараметров (добавляем количество
деревьев, увеличиваем количество итераций, увеличиваем глубину деревьев, уменьшаем регуляризацию3) и/или увеличиваем количество признаков
(конструируем новые признаки, обогащаем данные новыми внешними
признаками).
Переобучение можно диагностировать по очень низкой ошибке модели
в обучающей выборке наряду с более высокой ошибкой модели в тестовой
выборке. В таком случае мы упрощаем модель с помощью настройки гиперпараметров (уменьшаем глубину деревьев, уменьшаем количество итераций,
увеличиваем регуляризацию) и/или уменьшаем количество признаков.
Мы настраиваем сложность модели вышеописанными способами и проверяем обобщающую способность, используя отложенную выборку.

3

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

212



Инструменты

Недообучение

Переобучение

Ошибка

Оптимальная
сложность
Очень низкая ошибка в обучающей
выборке наряду с более высокой
ошибкой в тестовой выборке
свидетельствует о переобучении
Низкая ошибка в обучающей
выборке наряду с низкой ошибкой в
тестовой выборке свидетельствует о
недообучении

Сложность модели
Рис. 26 Классическая иллюстрация недообучения и переобучения

Важнейшим понятием, помогающим понять проблему поиска оптимальной
модели, является дилемма смещения–дисперсии. Ниже мы воспользуемся
популярным и упрощенным изложением дилеммы дисперсии-смещения, поскольку тема выходит за рамки книги, а наиболее полное и точное изложение
дилеммы вы найдете в книге: Abu-Mostafa, Yaser S., Malik Magdon-Ismail, and
Hsuan-Tien Lin. Learning From Data: A Short Course. [United States]: AMLBook.
com, 2012. Print.
Предположим, что у нас есть обучающее множество, состоящее из набора
точек x1,...xn и вещественных значений yi, связанных с каждой из этих точек xi.
Мы предполагаем, что есть функция с шумом y = f(x) + ε.

Мы хотим найти функцию f (x; D), которая аппроксимирует истинную функцию f(x) настолько хорошо, насколько возможно, в смысле некоторого алгоритма обучения, обученного на обучающем наборе (выборке) D = {(x1,y1),(xn,yn)}. Для
оценки качества аппроксимации воспользуемся среднеквадратичной ошибкой

между y и f (x; D), мы хотим, чтобы значение (y – f (x; D))2 было минимальным
как для точек x1,...xn, так и для точек за пределами выборки. Сделать идеально
мы не можем, поскольку yi содержит шум. Шум по определению является случайной величиной. Это значит, что мы не можем предсказать его точное значение. Поэтому мы должны быть готовы принять неустранимую ошибку в любой
функции, с которой будем работать.
Поиск функции f , которая обобщается для точек вне обучающего набора, может быть осуществлен любым из бесконечного числа алгоритмов, используемых для обучения с учителем. Оказывается, что какую бы функцию
мы ни выбрали, мы можем разложить ее ожидаемую ошибку следующим
образом:

7.3. Разбираемся с дилеммой смещения–дисперсии...  213

 



2


ED ,  y  f  x; D    Bias D  f  x; D  





  Var
2

D

 f  x; D     2 .



После некоторых преобразований получаем:



 







2
2
2




ED ,  y  f  x; D    ED  f  x; D    f ( x )  ED  ED  f  x; D    f ( x; D )    2 .









Математические ожидания варьируют в зависимости от разных вариантов
обучающего набора x1,...xn, y1,...yn из одного и того же совместного распределения P(x, y), их можно получить, например, с помощью бутстрепа. Три члена
представляют собой:
 квадрат смещения. Смещение (bias) – это отклонение среднего ответа
обученного алгоритма от истинного ответа. Тем самым смещение показывает, насколько далеко наш прогноз оказался от фактического значения зависимой переменной;
 дисперсию. Дисперсия (variance) – это разброс ответов обученного алгоритма относительно среднего ответа. Дисперсия показывает, насколько
сильно может изменяться прогноз в зависимости от выборки, иными
словами, она характеризует чувствительность метода обучения к изменениям в обучающей выборке;
 неустранимую ошибку σ2.
Из всего вышесказанного следует, что выбор сложности модели – это компромисс между смещением и дисперсией. Чем более сложной является модель, тем больше точек данных она пропускает и тем больше будет смещение,
а дисперсия будет меньше. Это хорошо иллюстрирует рисунок ниже.

Рис. 27 Недостаточно сложная модель и слишком сложная модель

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

214



Инструменты

Разберем дилемму смещения и дисперсии на примере одиночного дерева решений. Мы можем построить глубокое дерево решений так, что листья
будут содержать по одному наблюдению. Например, у нас в лист попало
наблюдение с фактической меткой Хороший клиент и у нас спрогнозированной меткой будет Хороший клиент. Мы получаем однозначный прогноз:
у нас будет идеально низкое смещение. При этом мы понимаем, что такое
глубокое дерево, по сути, запомнило весь обучающий набор и будет иметь
высокую дисперсию. Немного изменив набор данных, мы можем получить
совершенно другое дерево.
Хорошим наглядным представлением, которое может помочь в понимании
рассматриваемых идей, является мишень с кругами. Наши прогнозы – это выстрелы. Нас интересует удаленность выстрелов от «яблочка» (смещение) и кучность выстрелов (дисперсия).

Рис. 28 Дилемма смещения–дисперсии на примере мишени с кругами

Графики кривых обучения и валидации тоже дают наглядное представление
о смещении и дисперсии модели.

7.3. Разбираемся с дилеммой смещения–дисперсии...  215

Рис. 29 Дилемма смещения–дисперсии на графиках кривых обучения и валидации

Рассказывая о дилемме смещения–дисперсии, мы упомянули, что разные
варианты обучающего набора мы можем получить с помощью бутстрепа. Бутстреп широко используется для валидации и тестирования различных статис­
тических гипотез. Давайте подробнее познакомимся с этим методом.
На основе исходной выборки мы сначала формируем бутстреп-выборки наших данных. Для этого из исходной выборки объемом n наблюдений мы случайным образом выбираем наблюдение с возвращением n раз (поскольку отбор с возвращением, то одно и то же наблюдение может быть выбрано несколько раз). Мы получаем выборку, которая имеет такой же размер, что и исходная
выборка, однако некоторые наблюдения будут отсутствовать в нем (примерно
37 % наблюдений исходного набора), а некоторые попадут в него несколько раз.

Рис. 30 Бутстреп

216



Инструменты

Здесь сразу возникает вопрос, почему бутстреп-выборка не содержит примерно 37 % наблюдений обучающегоn набора. Вероятность выбрать одно на1
 1
n   : lim
1  ,соответственно,
  0, 368
блюдение в каждой попытке
равна
вероятность не выбрать
n  
e
 n
n
1
 1
n   :равна
lim  1  .У насвсего
0, 368n попыток, все попытки являются незаэто наблюдение
n 
e
 n
висимыми, поэтому вероятность не выбрать данное наблюдение при любой из
n
1
 1
подумаем, что будет происходить, когда n будет
попыток
n  равна
: lim  1   .Теперь
 0, 368
n 
n
e


n

1
 1
увеличиваться. Мы можем взять предел при n   : lim  1     0, 368.
n 
e
 n
Предположим, что мы хотим создать бутстреп-выборку для списка из 10 наблюдений, проиндексированных от 1 до 10 включительно: ['1', '2', '3', '4', '5', '6',
'7', '8', '9', '10']. Первая бутстреп-выборка может выглядеть как ['10', '9', '7', '8', '1',
'3', '9', '10', '10', '7']. Второй бутстреп-выборкой может быть ['4', '8', '5', '8', '3', '9',
'2', '6', '1', '6']. Каждая бутстреп-выборка содержит ровно столько наблюдений,
сколько наблюдений в исходной выборке. Из-за отбора с возвращением какие-то наблюдения могли не попасть в бустреп-выборку, а какие-то наблюдения попали в бутстреп-выборку несколько раз. Мы видим, что в первой бутстреп-выборке наблюдение 10 встречается три раза. Каждый раз наблюдения,
не попавшие в бустреп-выборку, формируют out-of-bag-выборку, которую
можно использовать для валидации модели.
Давайте на игрушечных данных проиллюстрируем, как можно использовать
бутстреп для оценки качества модели.
Мы начнем с того, что импортируем необходимые библиотеки, классы
и функции. Функция display() позволяет визуализировать несколько датафреймов в одном выводе. Класс DecisionTreeRegressor строит модель машинного обучения – модель дерева решений.
# импортируем библиотеки pandas, numpy
import pandas as pd
import numpy as np
# импортируем функции train_test_split() и display()
from sklearn.model_selection import train_test_split
from IPython.display import display
# импортируем класс StandardScaler,
# выполняющий стандартизацию
from sklearn.preprocessing import StandardScaler
# импортируем класс LogisticRegression
from sklearn.linear_model import LogisticRegression
# импортируем класс DecisionTreeRegressor
from sklearn.tree import DecisionTreeRegressor

На основе первых 15 наблюдений набора данных от компании StateFarm
сформируем игрушечный массив меток и массив признаков. Вместо задачи
классификации возьмем задачу регрессии, в качестве зависимой переменной
выберем переменную Customer Lifetime Value, в качестве признаков – переменные Income и Monthly Premium Auto. В качестве модели возьмем дерево решений.

7.3. Разбираемся с дилеммой смещения–дисперсии...  217
# записываем CSV-файл в объект DataFrame
data = pd.read_csv('Data/StateFarm.csv', sep=';')
# сформируем игрушечные массив меток
# и массив признаков
var_lst = ['Income', 'Monthly Premium Auto',
'Customer Lifetime Value']
toy_data = data[var_lst].head(10)
toy_labels = toy_data.pop('Customer Lifetime Value')

Взглянем на них.
# смотрим массив признаков
toy_data

# смотрим массив меток
toy_labels
0
18975.456110
1
4715.321344
2
5018.885233
3
4932.916345
4
5744.229745
5
13891.735670
6
7380.976717
7
3090.034104
8
2521.633095
9
2652.061785
Name: Customer Lifetime Value, dtype: float64

Теперь покажем, как можно сформировать бутстреп-выборку и out-of-bagвыборку.
# получаем индексы наблюдений исходной выборки
sample_indices = np.arange(toy_data.shape[0])
# задаем стартовое значение генератора
# псевдослучайных чисел

218



Инструменты

rng = np.random.RandomState(42)
# получаем индексы наблюдений бутстреп-выборки:
# sample_indices – используем индексы исходной выборки
# size – размер тот же, что у исходной выборки
# replace=True – отбор с возвращением
bootstrap_indices = rng.choice(sample_indices,
size=sample_indices.shape[0],
replace=True)
# получаем бутстреп-выборку
toy_data_boot = toy_data.iloc[bootstrap_indices]
toy_labels_boot = toy_labels.iloc[bootstrap_indices]
display(toy_data_boot)
display(toy_labels_boot)

6
7380.976717
3
4932.916345
7
3090.034104
4
5744.229745
6
7380.976717
9
2652.061785
2
5018.885233
6
7380.976717
7
3090.034104
4
5744.229745
Name: Customer Lifetime Value, dtype: float64

Видим, что в бутстреп-выборке некоторые наблюдения встретились несколько раз (например, наблюдение 6), а некоторые наблюдения не встретились ни разу. Теперь взглянем на out-of-bag-выборку.
# получаем out-of-bag-выборку
toy_data_out_boot = toy_data[~toy_data.index.isin(
toy_data_boot.index)]
toy_labels_out_boot = toy_labels[~toy_labels.index.isin(
toy_data_boot.index)]
display(toy_data_out_boot)
display(toy_labels_out_boot)

7.3. Разбираемся с дилеммой смещения–дисперсии...  219

0
18975.456110
1
4715.321344
5
13891.735670
8
2521.633095
Name: Customer Lifetime Value, dtype: float64

В out-of-bag-выборке мы находим наблюдения, отсутствующие в бутстреп-выборке.
Теперь напишем функцию, возвращающую бутстреп-выборки и out-of-bagвыборки, и с помощью бустрепа оценим качество модели дерева на наших
игрушечных данных.
# пишем функцию, возвращающую бутстреп-выборки
# и out-of-bag-выборки
def generate_bootstrap(rng, X, y, verbose=True):
# получаем индексы наблюдений исходной выборки
sample_indices = np.arange(X.shape[0])
# получаем индексы наблюдений бутстреп-выборки,
# бутстреп-выборка имеет тот же размер,
# что и исходная, отбор с возвращением
bootstrap_indices = rng.choice(sample_indices,
size=sample_indices.shape[0],
replace=True)
X_boot = X.iloc[bootstrap_indices]
y_boot = y.iloc[bootstrap_indices]
X_out_boot = X[~X.index.isin(X_boot.index)]
y_out_boot = y[~y.index.isin(X_boot.index)]
if verbose:
print(f"{i}-итерация")
print(f"индексы в бустреп-выборке: {X_boot.index.tolist()}")
print(f"индексы в out-of-bag-выборке: {X_out_boot.index.tolist()}\n")
# возвращаем выборки
return X_boot, y_boot, X_out_boot, y_out_boot

Возьмем 3 бустреп-выборки (на практике берут от 2000 до 10 000 бутстреп-выборок). У нас будет три итерации. На каждой итерации на бутстреп-выборке обучаем модель дерева, а на out-of-bag-выборке оцениваем
качество модели, значение метрики кладем в предварительно созданный пус­
той список. В итоге получим список из трех значений метрики качества (по
умолчанию для задачи регрессии вычисляется R-квадрат), вычисляем среднее значение метрики. Обратите внимание: начиная с версий Python 3.4+ мы
можем воспользоваться функцией mean() модуля statistics для вычисления
среднего в списке. А начиная с версий Python 3.8+ для значений с плавающей
точкой мы можем использовать функцию fmean() этого же модуля, которая
будет работать быстрее, чем mean().

220



Инструменты

# создаем контейнер для генератора
# псевдослучайных чисел
rng = np.random.RandomState(42)
# создаем экземпляр класса StandardScaler
standardscaler = StandardScaler()
# создаем экземпляр класса DecisionTreeRegressor
tree = DecisionTreeRegressor(random_state=42)
# создаем пустой список, в который
# будем записывать значения R2
test_score_lst = []
# на каждой итерации...
for i in range(1, 4):
# формируем бустреп-выборку и out-of-bag-выборку
X_boot, y_boot, X_out_boot, y_out_boot = generate_bootstrap(
rng, toy_data, toy_labels)
# обучаем на бутстреп-выборке
tree.fit(X_boot, y_boot)
# записываем значение R2
test_score = tree.score(X_out_boot, y_out_boot)
# кладем значение R2 в список
test_score_lst.append(test_score)
1-итерация
индексы в бустреп-выборке: [6, 3, 7, 4, 6, 9, 2, 6, 7, 4]
индексы в out-of-bag-выборке: [0, 1, 5, 8]
2-итерация
индексы в бустреп-выборке: [3, 7, 7, 2, 5, 4, 1, 7, 5, 1]
индексы в out-of-bag-выборке: [0, 6, 8, 9]
3-итерация
индексы в бустреп-выборке: [4, 0, 9, 5, 8, 0, 9, 2, 6, 3]
индексы в out-of-bag-выборке: [1, 7]
# смотрим список
print(test_score_lst)
[-0.043216359884440836, 0.6652528178872501, -2.3683596789799646]
# получаем среднее значение R2
mean_r2 = sum(test_score_lst) / len(test_score_lst)
print("Среднее значение R2: %.3f" % mean_r2)
Среднее значение R2: -0.582
# среднее значение в списке можно вычислить
# с помощью mean() или fmean() пакета statistics
import statistics
mean_r2 = statistics.fmean(test_score_lst)
print("Среднее значение R2: %.3f" % mean_r2)
Среднее значение R2: -0.582

7.3. Разбираемся с дилеммой смещения–дисперсии...  221
Если вам нужно оценить качество базовой модели, запускаете бутстреп на
всем историческом наборе. Оценкой качества модели будет оценка качества,
усредненная по out-of-bag-выборкам. Если нужно оценить гиперпараметры
модели, то разбиваете набор на обучающую и тестовую выборки, на обучающей выборке запускаете бутстреп, out-of-bag-выборки выступают в качестве
валидационных выборок. Модель с наилучшей комбинацией гиперпарамет­
ров, найденной в ходе бутстрепа, обучаете на всей обучающей выборке и применяете к тестовой выборке для итоговой оценки качества.
Если вы планируете перед построением модели осуществить операции
предварительной подготовки, предполагающие вычисление статистик (импутацию, стандартизацию и прочее), такие операции должны происходить внут­
ри процедуры бутстрепа. На каждой итерации в бутстреп-выборке вычисляете
статистики для импутации и стандартизации, с помощью этих статистик заменяете пропуски и выполняете стандартизацию в бутстреп-выборке и out-ofbag-выборке. Давайте проиллюстрируем этот случай.
# создаем контейнер для генератора
# псевдослучайных чисел
rng = np.random.RandomState(42)
# создаем экземпляр класса StandardScaler
standardscaler = StandardScaler()
# создаем экземпляр класса LogisticRegression
logreg = LogisticRegression(solver='lbfgs', max_iter=200)
# создаем пустой список, в который будем
# записывать значения правильности
test_score_lst = []
# на каждой итерации...
for i in range(1000):
# формируем бустреп-выборку и out-of-bag-выборку
X_boot, y_boot, X_out_boot, y_out_boot = generate_bootstrap(
rng, X_train, y_train, verbose=False)
# выполняем стандартизацию
standardscaler.fit(X_boot)
X_boot_scaled = standardscaler.transform(X_boot)
X_out_boot_scaled = standardscaler.transform(X_out_boot)
# обучаем на бутстреп-выборке
logreg.fit(X_boot_scaled, y_boot)
X_out_boot_scaled)
# записываем значение правильности
test_score = logreg.score(
X_out_boot_scaled, y_out_boot)
# кладем значение правильности в список
test_score_lst.append(test_score)
# получаем среднее значение правильности
mean_acc = statistics.fmean(test_score_lst)
print("Среднее значение правильности: %.3f" % mean_acc)
Среднее значение правильности: 0.900

222



Инструменты

Вернемся к дилемме смещения–дисперсии. На практике полезно разложить функцию потерь на смещение и дисперсию. Это позволяет лучше понять,
как настраиваемые нами гиперпараметры влияют на смещение и дисперсию.
Давайте выполним разложение квадратичной функции потерь на смещение
и дисперсию для конкретного примера с помощью функции bias_variance_
decomp() из пакета Себастьяна Рашки mlxtend.
Мы воспользуемся набором данных Boston Housing. Задача, связанная
с этим набором данных, заключается в том, чтобы спрогнозировать медианную стоимость домов в нескольких районах Бостона в 1970-е годы на основе
такой ​​информации, как уровень преступности, близость к реке Чарльз, удаленность от радиальных магистралей и т. д. Набор данных содержит 506 наблюдений и 13 признаков.
Импортируем необходимые данные.
# импортируем необходимые данные
from mlxtend.data import boston_housing_data

Теперь создаем обучающий и тестовый массивы признаков и массив значений зависимой переменной.
# создаем массив признаков и массив значений зависимой переменной
X, y = boston_housing_data()
# создаем обучающие и тестовые массивы признаков
# и значений зависимой переменной
X_train, X_test, y_train, y_test = train_test_split(X,
y,
test_size=0.3,
random_state=123)

Нам нужно написать функцию _draw_bootstrap_sample(), генерирующую
бутстреп-выборки на основе обучающей выборки, во многом аналогичную
той, что писали ранее. Она будет использована внутри функции bias_variance_
decomp().
# пишем функцию, генерирующую бутстреп-выборки
def _draw_bootstrap_sample(rng, X, y):
# получаем индексы наблюдений исходной выборки
sample_indices = np.arange(X.shape[0])
# получаем индексы наблюдений бутстреп-выборки, бутстреп-выборка
# имеет тот же размер, что и исходная, отбор с возвращением
bootstrap_indices = rng.choice(sample_indices,
size=sample_indices.shape[0],
replace=True)
# формируем бутстреп-выборку
return X[bootstrap_indices], y[bootstrap_indices]

Пишем функцию bias_variance_decomp(), вычисляющую усредненное ожидаемое значение функции потерь, усредненное смещение, усредненную дисперсию.

7.3. Разбираемся с дилеммой смещения–дисперсии...  223
# пишем функцию, вычисляющую усредненное ожидаемое значение функции
# потерь, усредненное смещение, усредненную дисперсию
def bias_variance_decomp(estimator, X_train, y_train, X_test, y_test,
num_rounds=200, random_seed=None):
"""
Автор: Sebastian Raschka
https://github.com/rasbt
Параметры
---------estimator: регрессор.
X_train: обучающий набор для извлечения бутстреп-выборок.
y_train: обучающий массив значений зависимой переменной.
X_test: тестовый набор для вычисления среднеквадратичной функции
потерь, смещения и дисперсии.
y_test: тестовый массив значений зависимой переменной.
num_rounds : количество итераций бутстрепа (целочисленное
значение, по умолчанию 200).
random_seed : стартовое значение генератора псевдослучайных
чисел для создания бутстреп-выборок (целочисленное
значение, по умолчанию None).
Возвращает
---------avg_expected_loss, avg_bias, avg_var : усредненное
ожидаемое значение функции потерь, усредненное смещение,
усредненную дисперсию (все значения с плавающей точкой),
усреднение происходит по наблюдениям тестового набора.
"""
# создаем контейнер для генератора
# псевдослучайных чисел
rng = np.random.RandomState(random_seed)
# создаем массив из нулей, количество строк равно num_rounds,
# а количество столбцов определяется количеством наблюдений
# тестового набора, в него мы будем записывать прогнозы
all_pred = np.zeros((num_rounds, y_test.shape[0]), dtype=int)
# на каждой итерации...
for i in range(num_rounds):
# формируем бутстреп-выборку на основе обучающего набора
X_boot, y_boot = _draw_bootstrap_sample(rng, X_train, y_train)
# обучаем регрессор на бутстреп-выборке и
# выдаем прогнозы для тестового набора
pred = estimator.fit(X_boot, y_boot).predict(X_test)
# записываем прогнозы в массив all_pred
all_pred[i] = pred
# вычисляем усредненное ожидаемое значение функции потерь,
# в нашем случае среднеквадратичную ошибку, усредненную
# по среднеквадратичным ошибкам, полученным на каждой
# итерации (количество итераций задается num_rounds)
avg_expected_loss = np.apply_along_axis(
lambda x:
((x - y_test) ** 2).mean(),
axis=1,
arr=all_pred).mean()

224



Инструменты

# вычисляем усредненный прогноз (берем среднее
# по оси 0 массива all_pred)
main_predictions = np.mean(all_pred, axis=0)
# вычисляем усредненное смещение, делим сумму квадратов
# разностей между усредненными прогнозами и фактическими
# значениями зависимой переменной в тестовом наборе на
# количество наблюдений тестового набора
avg_bias = np.sum((main_predictions - y_test) ** 2) / y_test.size
# вычисляем усредненную дисперсию, делим сумму квадратов
# разностей между усредненными прогнозами и прогнозами
# для тестового набора на количество прогнозов
avg_var = np.sum((main_predictions - all_pred) ** 2) / all_pred.size
return avg_expected_loss, avg_bias, avg_var

Мы сравним смещение и дисперсию дерева максимальной глубины (строится по умолчанию) со смещением и дисперсией дерева глубины 1 (т. е. дерево
имеет один уровень ниже корневого узла).
# строим дерево максимальной глубины
tree = DecisionTreeRegressor(random_state=123)
# вычисляем усредненное ожидаемое значение функции потерь,
# усредненное смещение, усредненную дисперсию
avg_expected_loss, avg_bias, avg_var = bias_variance_decomp(
tree, X_train, y_train, X_test, y_test,
random_seed=123)
# печатаем результаты
ttl = "Усредненное ожидаемое значение функции потерь: %.3f"
print(ttl % avg_expected_loss)
print("Усредненное смещение: %.3f" % avg_bias)
print("Усредненная дисперсия: %.3f" % avg_var)
Усредненное ожидаемое значение функции потерь: 31.917
Усредненное смещение: 13.814
Усредненная дисперсия: 18.102
# строим дерево глубины 1
tree2 = DecisionTreeRegressor(random_state=123, max_depth=1)
# вычисляем усредненное ожидаемое значение функции потерь,
# усредненное смещение, усредненную дисперсию
avg_expected_loss, avg_bias, avg_var = bias_variance_decomp(
tree2, X_train, y_train, X_test, y_test,
random_seed=123)
# печатаем результаты
print(ttl % avg_expected_loss)
print("Усредненное смещение: %.3f" % avg_bias)
print("Усредненная дисперсия: %.3f" % avg_var)
Усредненное ожидаемое значение функции потерь: 53.199
Усредненное смещение: 41.990
Усредненная дисперсия: 11.209

7.3. Разбираемся с дилеммой смещения–дисперсии...  225
Мы видим, что дерево максимальной глубины характеризуется меньшим
смещением и большей дисперсией. И наоборот, короткое дерево имеет высокое смещение и низкую дисперсию.
Для лучшего понимания давайте подробнее посмотрим, что происходит под
капотом функции bias_variance_decomp().
Создаем игрушечные обучающие и тестовые массивы признаков и значений
зависимой переменной и контейнер для генератора псевдослучайных чисел.
# создаем игрушечные обучающие и тестовые массивы
# признаков и значений зависимой переменной
X_train = np.array([[29.1, 19000.28, 15],
[67.3, 48800.81, 45],
[77.9, 89800.55, 188]])
X_test = np.array([[11.9, 89900.28, 199],
[37.8, 10600.82, 95],
[77.2, 99700.22, 87]])
y_train = np.array([22.6, 89.5, 17.3])
y_test = np.array([12.4, 96.9, 107.9])
# создаем контейнер для генератора
# псевдослучайных чисел
rng = np.random.RandomState(123)

Создаем массив all_pred из нулей, количество строк равно количеству итераций бутстрепа (у нас будет три итерации), а количество столбцов определяется количеством наблюдений тестового набора, в него мы будем записывать
прогнозы каждого построенного дерева в целочисленном виде.
# создаем массив из нулей, количество строк равно количеству итераций,
# а количество столбцов определяется количеством наблюдений тестового
# набора, в этот массив мы будем записывать прогнозы
all_pred = np.zeros((3, y_test.shape[0]), dtype=int)
all_pred
array([[0, 0, 0],
[0, 0, 0],
[0, 0, 0]])

Ось 1 (ось столбцов)
наблюдения тестового набора

Ось 0 (ось строк)
итерации

0
1
2

0

1

2

0
0
0

0
0
0

0
0
0

Теперь запускаем цикл for. Извлекаем бутстреп-выборку на основе обучающего набора, строим дерево регрессии по бутстреп-выборке, вычисляем
прогнозы для тестового массива и записываем их в массив all_pred. И так
три раза.

226



Инструменты

# на каждой итерации...
for i in range(3):
# формируем бутстреп-выборку на основе обучающего набора
X_boot, y_boot = _draw_bootstrap_sample(rng, X_train, y_train)
# обучаем регрессор на бутстреп-выборке и
# выдаем прогнозы для тестового набора
pred = tree.fit(X_boot, y_boot).predict(X_test)
# записываем прогнозы в массив all_pred
all_pred[i] = pred

Давайте вновь взглянем на массив all_pred с прогнозами и тестовый массив
значений зависимой переменной.
# смотрим массив с прогнозами
all_pred
array([[17, 89, 89],
[17, 22, 22],
[17, 89, 89]])
# смотрим тестовый массив значений
# зависимой переменной
y_test
array([ 12.4, 96.9, 107.9])

Давайте вычислим среднеквадратичную ошибку по прогнозам, полученным на первой итерации.
# вычислим среднеквадратичную ошибку по прогнозам,
# полученным на первой итерации
mse_first_iter = (((17 - 12.4)**2) + ((89 - 96.9)**2) +
((89 - 107.9)**2)) / 3
mse_first_iter
146.92666666666676

Разумеется, когда итераций будет много, удобнее применить функцию
np.apply_along_axis(). Она вычисляет среднеквадратичную ошибку для каждой
итерации. Для этого она каждый раз применяет ((x - y_test) ** 2).mean()
вдоль оси 1 (оси столбцов) массива all_pred.
# вычислим среднеквадратичные ошибки по прогнозам,
# полученным на каждой итерации
mse = np.apply_along_axis(
lambda x:
((x - y_test)**2).mean(),
axis=1,
arr=all_pred)
mse

array([ 146.92666667, 4336.66 , 146.92666667])

7.3. Разбираемся с дилеммой смещения–дисперсии...  227
Вычисляем усредненное ожидаемое значение функции потерь, фактически
среднеквадратичную ошибку, усредненную по среднеквадратичным ошибкам, полученным на каждой итерации.
# вычисляем усредненное ожидаемое значение функции потерь,
# в нашем случае среднеквадратичную ошибку, усредненную по
# среднеквадратичным ошибкам, полученным на каждой итерации
avg_expected_loss = np.apply_along_axis(
lambda x:
((x - y_test)**2).mean(),
axis=1,
arr=all_pred).mean()
avg_expected_loss
1543.504444444445

Теперь вычисляем усредненный прогноз по каждому наблюдению (берем
среднее по оси 0 массива all_pred).
# вычисляем усредненный прогноз по каждому наблюдению
# (берем среднее по оси 0 массива all_pred)
main_predictions = np.mean(all_pred, axis=0)
main_predictions
array([17. , 66.66666667, 66.66666667])

Вычисляем усредненное смещение. Для этого делим сумму квадратов разностей между усредненными прогнозами и фактическими значениями зависимой переменной в тестовом наборе на размер тестового набора (количество
наблюдений тестового набора).
# вычисляем усредненное смещение, делим сумму квадратов разностей
# между усредненными прогнозами и фактическими значениями
# зависимой переменной в тестовом наборе на
# количество наблюдений тестового набора
avg_bias = np.sum((main_predictions - y_test)**2) / y_test.size
avg_bias
878.4674074074074

228



Инструменты

Вычисляем усредненную дисперсию. Для этого делим сумму квадратов разностей между усредненными прогнозами и прогнозами для тестового набора
на количество прогнозов.
# вычисляем усредненную дисперсию, делим сумму квадратов разностей
# между усредненными прогнозами и прогнозами для тестового набора
# на количество прогнозов
avg_var = np.sum((main_predictions - all_pred)**2) / all_pred.size
avg_var
665.037037037037

Давайте проверим, совпадают ли результаты, вычисленные вручную, с результатами, вычисленными с помощью функции bias_variance_decomp().
# давайте проверим, совпадают ли результаты, вычисленные вручную, с
# результатами, вычисленными с помощью функции bias_variance_decomp()
# вычисляем усредненное ожидаемое значение функции потерь,
# усредненное смещение, усредненную дисперсию
avg_expected_loss, avg_bias, avg_var = bias_variance_decomp(
tree, X_train, y_train, X_test, y_test, num_rounds=3,
random_seed=123)
# печатаем результаты
print(ttl %avg_expected_loss)
print("Усредненное смещение: %.3f" % avg_bias)
print("Усредненная дисперсия: %.3f" % avg_var)
Усредненное ожидаемое значение функции потерь: 1543.504
Усредненное смещение: 878.467
Усредненная дисперсия: 665.037

7.4. Обработка пропусков с помощью классов
MissingIndicator и SimpleImputer
В прошлом примере данные, предоставленные компанией StateFarm, не
содержали пропущенных значений, однако на практике практически всегда наборы данных содержат пропуски. Давайте разберем полезные классы
MissingIndicator и SimpleImputer, помогающие обработать пропуски.
Класс MissingIndicator создает индикатор пропущенных значений. Пропуск
переменной заменяется значением True, а непропущенное значение – значением False.

7.4. Обработка пропусков с помощью классов MissingIndicator и...  229
Давайте загрузим данные, сформируем обучающий массив признаков, тес­
товый массив признаков, обучающий массив меток, тестовый массив меток.
# импортируем библиотеки pandas и numpy, функцию train_test_split()
# и классы MissingIndicator и SimpleImputer
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.impute import MissingIndicator, SimpleImputer
# записываем CSV-файл в объект DataFrame
data = pd.read_csv('Data/Verizon_missing.csv', sep=';')
# выведем первые 3 наблюдения
data.head(3)

Исходный набор содержит небольшую часть данных американского провайдера Verizon. Он представляет собой записи о 144 клиентах, классифицированных на два класса: 0 – оттока нет (78 клиентов) и 1 – отток есть (66 клиентов).
По каждому наблюдению (клиенту) фиксируются следующие переменные (характеристики):
 количественный признак Длительность междугородних звонков [longdist];
 количественный признак Длительность международных звонков [internat];
 количественный признак Длительность меcтных звонков [local];
 количественный признак Возраст [age];
 категориальный признак Ежемесячный доход [income];
 категориальный признак Тип тарифа [billtype];
 категориальный признак Способ оплаты [pay];
 бинарная зависимая переменная Факт оттока клиента [churn].
Давайте разобьем набор на обучающую и тестовую выборки и взглянем на них.
# разбиваем данные на обучающие и тестовые: получаем обучающий
# массив признаков, тестовый массив признаков, обучающий массив
# меток, тестовый массив меток
train, test, y_train, y_test = train_test_split(
data.drop('churn', axis=1),
data['churn'],
test_size=.3,
stratify=data['churn'],
random_state=100)

Посмотрим на обучающий массив признаков.
# взглянем на обучающий массив признаков
train.head(10)

230



Инструменты

Посмотрим на тестовый массив признаков.
# взглянем на тестовый массив признаков
test.head(10)

Видим, что массивы содержат пропуски. Также о пропусках можно судить,
воспользовавшись методом .info() объекта DataFrame или цепочкой методов
.isnull() и .sum(). Кроме того, если целочисленная переменная (возраст, количество просрочек) записана как вещественная, это указывает на наличие
пропусков (ведь для хранения количественной переменной с пропусками используется тип float).
# выясняем, есть ли пропуски
print(train.isnull().sum())
print('')
print(test.isnull().sum())
longdist
internat
local
age
income

5
23
12
8
18

7.4. Обработка пропусков с помощью классов MissingIndicator и  231
billtype
13
pay
13
dtype: int64
longdist
3
internat
11
local
2
age
0
income
11
billtype
11
pay
2
dtype: int64

Давайте создадим экземпляр класса MissingIndicator и обучим модель
импутации. В данном случае метод .fit() вычисляет единственный параметр
модели – булеву маску для пропусков переменной local в обучающем массиве
признаков.
# создаем экземпляр класса MissingIndicator
miss_ind = MissingIndicator()
# обучаем модель импутации – создаем индикатор пропусков
# переменной local в обучающем массиве
miss_ind.fit(train[['local']])

Большинство классов библиотеки scikit-learn работают с 2D-массивами, поэтому когда работаем с серией, не забываем ее преобразовать в соответствующий массив (используем двойные квадратные скобки).
Теперь применяем модель с помощью метода .transform(). Фактически метод .transform() создает индикатор пропусков переменной local в обучающем
и тестовом массивах признаков.
# применяем модель импутации к переменной local
# в обучающем массиве признаков:
# создаем индикатор пропусков переменной local
# в обучающем массиве
train['miss_ind_local'] = miss_ind.transform(train[['local']])
# применяем модель импутации к переменной local
# в тестовом массиве признаков:
# создаем индикатор пропусков переменной local
# в тестовом массиве
test['miss_ind_local'] = miss_ind.transform(test[['local']])
train .head()

232



Инструменты

Давайте вычислим медиану переменной local в обучающем массиве признаков. Мы ее применим для импутации пропусков переменной local, воспользовавшись удобным классом SimpleImputer.
# взглянем на медиану переменной local
# в обучающем массиве признаков
train['local'].median()
32.5

Класс SimpleImputer осуществляет замену пропущенных значений с помощью статистик – среднего, медианы, моды и константного значения. Среднее и медиану можно задать только для количественных переменных. Моду
и константное значение можно применить как к количественным, так и к
категориальным переменным (для количественных переменных константой
будет 0, для категориальных переменных константой будет строковое значение 'missing_value').

-

Давайте создадим экземпляр класса SimpleImputer и обучим модель импутации. В данном случае метод .fit() вычисляет единственный параметр модели – медиану переменной local в обучающем массиве признаков.
# создаем экземпляр класса SimpleImputer
simp = SimpleImputer(strategy='median')
# обучаем модель импутации – вычисляем медиану
# переменной local в обучающем массиве
simp.fit(train[['local']]);

Теперь применяем модель с помощью метода .transform(). Фактически
метод .transform() заменяет пропуски переменной local в обучающем и тес­
товом массивах признаков медианой переменной local в обучающем массиве
признаков.
#
#
#
#

применяем модель импутации к переменной local
в обучающем массиве признаков:
заменяем пропуски переменной в обучающем массиве признаков
медианой переменной, вычисленной по ОБУЧАЮЩЕМУ массиву признаков

7.4. Обработка пропусков с помощью классов MissingIndicator и  233
train['local'] = simp.transform(train[['local']])
# еще можно так
# train['local'] = simp.transform(train['local'].values.reshape(-1, 1))
# применяем модель импутации к переменной local
# в тестовом массиве признаков:
# заменяем пропуски переменной в тестовом массиве признаков
# медианой переменной, вычисленной по ОБУЧАЮЩЕМУ массиву признаков
test['local'] = simp.transform(test[['local']])

Взглянем на обучающий и тестовый массивы признаков.
# взглянем на обучающий массив признаков
train.head(10)

# взглянем на тестовый массив признаков
test.head(10)

Видим, что пропуски были заменены на медианы (выделены красными
рамками).

234



Инструменты

Теперь применим SimpleImputer к отдельным спискам переменных. Пропус­
ки в оставшихся количественных переменных заменим медианами, а пропус­
ки в категориальных переменных – модами.
# создаем список количественных переменных
numeric_cols = ['longdist', 'internat', 'age', 'income']
# обучаем модель импутации – вычисляем медианы переменных
# longdist, internat, age и income в обучающем массиве признаков
simp.fit(train[numeric_cols])
# применяем модель импутации к указанным переменным
# в обучающем массиве признаков:
# заменяем пропуски каждой переменной в обучающем
# массиве признаков медианой соответствующей переменной,
# вычисленной по ОБУЧАЮЩЕМУ массиву признаков
train[numeric_cols] = simp.transform(train[numeric_cols])
# применяем модель импутации к указанным переменным
# в тестовом массиве признаков:
# заменяем пропуски каждой переменной в тестовом
# массиве признаков медианой соответствующей переменной,
# вычисленной по ОБУЧАЮЩЕМУ массиву признаков
test[numeric_cols] = simp.transform(test[numeric_cols])
# создаем список категориальных переменных
cat_cols = ['billtype', 'pay']
# создаем экземпляр класса SimpleImputer
simp2 = SimpleImputer(strategy='most_frequent')
# обучаем модель импутации – вычисляем моды переменных
# billtype и pay в обучающем массиве признаков
simp2.fit(train[cat_cols])
# применяем модель импутации к указанным переменным
# в обучающем массиве признаков:
# заменяем пропуски каждой переменной в обучающем
# массиве признаков модой соответствующей переменной,
# вычисленной по ОБУЧАЮЩЕМУ массиву признаков
train[cat_cols] = simp2.transform(train[cat_cols])
# применяем модель импутации к указанным переменным
# в тестовом массиве признаков:
# заменяем пропуски каждой переменной в тестовом
# массиве признаков модой соответствующей переменной,
# вычисленной по ОБУЧАЮЩЕМУ массиву признаков
test[cat_cols] = simp2.transform(test[cat_cols])

Взглянем на результат и убедимся, что пропуски в указанных переменных
импутированы.
# взглянем на обучающий массив признаков
train.head(10)

7.5. Выполнение дамми-кодирования с помощью класса OneHotEncoder...  235

7.5. Выполнение дамми-кодирования с помощью класса
OneHotEncoder и функции get_dummies(), знакомство
с разреженными матрицами
Класс OneHotEncoder и функция get_dummies() осуществляют дамми-кодирование, необходимое для линейных моделей при обработке категориальных признаков. Линейные модели не умеют обрабатывать категориальные признаки
«как есть». Каждый уровень категориальной переменной становится отдельным бинарным столбцом со значениями 0 или 1.

Рис. 31 Дамми-кодирование

Начнем с класса OneHotEncoder. Метод .fit() определяет схему кодировки
категориального признака, а метод .transform() применяет эту схему, выполняет дамми-кодирование. Ниже разобраны основные параметры класса
OneHotEncoder.

236



Инструменты

Наиболее важным параметром класса OneHotEncoder является параметр
drop, который задает способ удаления одной из категорий признака. По умолчанию все категории сохраняются. Если для параметра drop задано значение
'first', удаляется первая категория. Это может быть полезно в ситуациях, когда идеально скоррелированные признаки вызывают проблемы, например при
вводе их в модель линейной регрессии без регуляризации. Однако отбрасывание одной категории нарушает симметрию исходного представления и, следовательно, может вызвать смещение в некоторых моделях, например в моделях
линейной классификации или регрессии с регуляризацией. По умолчанию параметр drop сохраняет все категории.
Параметр handle_unknown задает способ обработки новой категории в новых
данных: 'error' – выдает ошибку (по умолчанию), 'ignore' – игнорирует (дамми-переменные для новой категории будут закодированы нулями), 'infrequent_
if_exist' – приравнивает к редкой категории (дамми-переменные для новой категории будут закодированы так же, как для редкой категории), редкая категория
(infrequent category) определяется параметрами min_frequency и max_categories.
Давайте выполним дамми-кодирование переменной pay, при этом будем
игнорировать появление неизвестной категории в новых данных.
# импортируем класс OneHotEncoder
from sklearn.preprocessing import OneHotEncoder
# создаем экземпляр класса OneHotEncoder
ohe = OneHotEncoder(sparse=False, handle_unknown='ignore')
# копируем переменную pay в обучающем массиве признаков
ohe_train = train[['pay']].copy()
# обучаем модель дамми-кодирования – определяем дамми для переменной
# pay в обучающем массиве признаков
ohe.fit(ohe_train)
# выполняем дамми-кодирование переменной
# pay в обучающем массиве признаков
ohe_train_transformed = ohe.transform(ohe_train)
# смотрим первые 10 наблюдений
ohe_train_transformed[:10]

7.5. Выполнение дамми-кодирования с помощью класса OneHotEncoder...  237
array([[0.,
[0.,
[0.,
[0.,
[1.,
[0.,
[0.,
[0.,
[0.,
[0.,

0.,
1.,
1.,
1.,
0.,
1.,
1.,
0.,
0.,
1.,

0.,
0.,
0.,
0.,
0.,
0.,
0.,
0.,
1.,
0.,

1.],
0.],
0.],
0.],
0.],
0.],
0.],
1.],
0.],
0.]])

Теперь применим OneHotEncoder, когда в тестовом массиве признаков у переменной pay появилась новая категория.
# копируем переменную pay в тестовом массиве признаков
ohe_test = test[['pay']].copy()
# заменяем значение в переменной pay
ohe_test.iloc[0, 0] = 'new_category'
# выводим первые три наблюдения
ohe_test.head(3)

# выполняем дамми-кодирование переменной
# pay в тестовом массиве признаков
ohe_test_transformed = ohe.transform(ohe_test)
# смотрим первые три наблюдения
ohe_test_transformed[:3]
array([[0., 0., 0., 0.],
[0., 0., 0., 1.],
[0., 1., 0., 0.]])

Видим, что все дамми-переменные для новой категории закодированы нулями.
Теперь создадим обучающий датафрейм с одним столбцом City.
# создаем обучающий датафрейм c одним столбцом
train = pd.DataFrame(
{'City': ['MSK', 'MSK', 'MSK', 'SPB',
'EKB', 'EKB', 'EKB',
'EKB', 'EKB']})
train

238



Инструменты

Взглянем на частоты категорий переменной City.
# смотрим частоты категорий City
train['City'].value_counts()
EKB
MSK
SPB
Name:

5
3
1
City, dtype: int64

Создаем класс OneHotEncoder, задав с помощью параметра min_frequency
порог для редких категорий, равный 3, т. е. категории с абсолютными частотами менее 3 будут объявлены редкими, и обучаем. Дополнительно с помощью
атрибута infrequent_categories_ выведем обнаруженные редкие категории.
# создаем класс OneHotEncoder, задав порог
# для редких категорий, и обучаем
ohe = OneHotEncoder(
min_frequency=3,
sparse=False,
handle_unknown='infrequent_if_exist')
ohe.fit(train)
ohe.infrequent_categories_
[array(['SPB'], dtype=object)]

Видим, что редкой категорией является 'SPB', потому что ее частота меньше 3.
Теперь выполним дамми-кодирование переменной City в обучающем наборе.
# выполняем дамми-кодирование
# в обучающем датафрейме
ohe.transform(train)
array([[0.,
[0.,
[0.,
[0.,
[1.,

1.,
1.,
1.,
0.,
0.,

0.],
0.],
0.],
1.],
0.],

7.5. Выполнение дамми-кодирования с помощью класса OneHotEncoder...  239
[1.,
[1.,
[1.,
[1.,

0.,
0.,
0.,
0.,

0.],
0.],
0.],
0.]])

Таким образом, для новой категории в новых данных к дамми-переменным
будет применена та же схема кодировки, что и для категории 'SPB' (выделена
красной рамкой).
Теперь создадим тестовый датафрейм с одним столбцом City, у которого помимо знакомых категорий 'SPB', 'MSK' и 'EKB' будет одна новая категория
'NSK'.
# создаем обучающий датафрейм c одним столбцом,
# в котором будет новая категория 'NSK'
test = pd.DataFrame(
{'City': ['NSK', 'MSK', 'NSK', 'MSK',
'SPB', 'EKB', 'SPB',
'EKB', 'SPB']})
test

Теперь выполним дамми-кодирование переменной City в тестовом наборе.
# выполняем дамми-кодирование
# в тестовом датафрейме
ohe.transform(test)
array([[0.,
[0.,
[0.,
[0.,
[0.,
[1.,
[0.,
[1.,
[0.,

0.,
1.,
0.,
1.,
0.,
0.,
0.,
0.,
0.,

1.],
0.],
1.],
0.],
1.],
0.],
1.],
0.],
1.]])

Видим, что дамми-переменные для новой категории 'NSK' в новых данных
(тестовом датафрейме) кодируются так же, как для категории 'SPB'.

240



Инструменты

Типичное применение класса OneHotEncoder – в качестве звена в цепочке
преобразований внутри конвейера в связке с классами FunctionTransformer,
ColumnTransformer и Pipeline. Класс OneHotEncoder редко используется для непосредственного дамми-кодирования объектов Series и DataFrame библиотеки
pandas. Обычно для этих целей применяется функция get_dummies() библиотеки pandas.
Функция get_dummies() автоматически преобразует заданную категориальную переменную в набор дамми-переменных.
Параметр drop_first этой функции задает тип дамми-кодирования. По умолчанию для этого параметра задано значение False и выполняется дамми-кодирование по методу неполного ранга, в противном случае будет выполнено
дамми-кодирование по методу полного ранга (первая категория объявляется
опорной, это необходимо для устранения линейной зависимости столбцов при
применении линейных моделей без регуляризации). Пропуски в переменной
кодируются нулями во всех дамми-переменных, созданных для данной переменной. С помощью параметра columns можно задать список имен столбцов, которые нужно подвергнуть дамми-кодированию. С помощью параметра
dummy_na можно добавить столбец – индикатор значений NaN.
# применяем функцию get_dummies()
train = pd.get_dummies(train)
train.head()

Вовремя избавляйтесь от редких категорий, потому что они могут стать
причиной несовпадения количества дамми-переменных в обучающей и тес­
товой выборках.
У класса OneHotEncoder есть параметр sparse, возвращающий разреженную
матрицу, у функции get_dummies() тоже есть параметр sparse, возвращающий
массив, который используется для хранения разреженных данных. Давайте
выясним, что из себя представляет разреженная матрица.
Разреженной называют матрицу, состоящую преимущественно из нулевых
значений. Плотной матрицей называют матрицу, состоящую преимущественно из ненулевых значений. Разреженность можно вычислить, поделив количество нулевых значений на общее количество элементов. В прикладном
машинном обучении часто приходится работать с большими разреженными
матрицами. Здесь можно привести примеры получения таких матриц, когда:
 мы представляем категориальную переменную в виде бинарных признаков, строя линейную модель;
 в ходе построения рекомендательной системы фиксируем факт просмот­
ра зрителем конкретного фильма в каталоге фильмов, факт покупки
клиентом товара в каталоге товаров;

7.5. Выполнение дамми-кодирования с помощью класса OneHotEncoder...  241
 подсчитываем встречаемость слов в корпусе документов при естественной обработке языка.
Правильная обработка разреженных матриц может привести к сокращению
вычислительных затрат.
Для хранения разреженных матриц можно использовать следующие структуры данных.
Словарь по ключам (dictionary of keys или DOK) – словарь, в котором
ключами являются кортежи индексов строк и столбцов (row, column), а значениями – соответствующие значения. Элементы, отсутствующие в словаре,
принимаются за нулевые.
Список списков (list of lists или LIL) – каждая строка матрицы хранится
как список, каждый подсписок содержит индекс столбца и значение.
Список координат (coordinate list или COO) – список кортежей, в котором
каждый кортеж хранит индекс строки, индекс столбца и значение (row, column,
value).
Кроме того, существуют еще две структуры данных, которые на практике
чаще используются для эффективной работы с разреженными матрицами:
 сжатое хранение строкой (CSR – compressed sparse row, CRS –
compressed row storage, Йельский формат);
 cжатое хранение столбцом (CSС – compressed sparse column, CСS –
compressed column storage).
При сжатом хранении строкой мы представляем матрицу M n×m, содержащую
NNONZERO ненулевых значений, в виде трех одномерных массивов:
 массив значений – массив размера NNONZERO, в котором хранятся ненулевые значения, взятые подряд из первой непустой строки, затем идут
значения из следующей непустой строки и т. д.;
 массив индексов столбцов – массив размера NNONZERO, который хранит индексы столбцов для соответствующих элементов из массива значений;
 массив индексации строк – массив размера n + 1 (количество строк + 1),
который для индекса i хранит количество ненулевых элементов в строках до i – 1 включительно, при этом последний элемент массива совпадает с NNONZERO, а первый всегда равен 0, выполняя роль запирающего
элемента.
1 2 0


Пусть M   0 4 0  , тогда массив значений = (1, 2, 4, 2, 6), массив индексов
0 2 6


столбцов = (0, 1, 1, 1, 2), массив индексов строк = (0, 2, 3, 5).
Как был сформирован массив значений? Мы переписали ненулевые значения,
взятые подряд из первой непустой строки, затем ненулевые значения из второй
непустой строки, потом ненулевые значения из третьей непустой строки.
Как был сформирован массив индексов столбцов? Мы записали индексы
столбцов для ненулевых значений.
Как был сформирован массив индексов строк? Записываем ноль и подсчитываем накопленное количество ненулевых значений в каждой строке. В пер-

242

Инструменты



вой строке только два ненулевых значения, поэтому записываем 2, во второй
строке – одно ненулевое значение, поэтому записываем 2 + 1 = 3, в третьей
строке – два ненулевых значения, поэтому записываем 3 + 2 = 5.
Сжатое хранение столбцом похоже на CRS, только строки и столбцы меняются ролями – значения храним по столбцам, по второму массиву можем
определить строку, после подсчётов с третьим массивом узнаём столбцы.
Разумеется, могут быть модификации данных форматов.
Сейчас мы определим разреженную матрицу 4×4 как плотный массив
NumPy, преобразуем ее в формат CRS и потом обратно преобразуем в плотный массив с помощью функции todense().
# импортируем функцию csr_matrix()
from scipy.sparse import csr_matrix
# создаем массив из 16 элементов, 4 строки и 4 столбца
A = np.array([[0, 0, 0, 0],
[5, 8, 0, 0],
[0, 0, 3, 0],
[0, 6, 0, 0]])
# печатаем массив
print(A)
# преобразовываем в CRS-представление
print('')
S = csr_matrix(A)
# печатаем CRS-представление
print(S)
# преобразовываем обратно в плотный массив
D = S.todense()
# печатаем плотный массив
print('')
print(D)
[[0
[5
[0
[0

0
8
0
6

(1,
(1,
(2,
(3,
[[0
[5
[0
[0

0
8
0
6

0
0
3
0

0]
0]
0]
0]]
→5
→8
→3
→6

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

0]
0]
0]
0]]

В заключение проиллюстрируем, как можно ускорить вычисления за счет
использования формата для разреженных данных. Загружаем данные с соревнования Categorical Feature Encoding Challenge II на Kaggle https://www.kaggle.
com/competitions/cat-in-the-dat-ii/overview. Он содержит 600 000 наблюдений и 25
переменных. Метрика качества – AUC-ROC.

7.5. Выполнение дамми-кодирования с помощью класса OneHotEncoder...  243
# загружаем и смотрим данные
data = pd.read_csv('Data/catfeatures_challenge_II_train.csv')
data.head()

Зависимой переменной является переменная target, бинарные признаки
помечены префиксом bin_, номинальные – префиксом nom_, порядковые –
префиксом ord_, также есть два циклических признака времени day и month.
Избавляемся от редких категорий в некоторых переменных. Все категории
с частотой 100 наблюдений и менее запишем в отдельную категорию OTHER.
# избавляемся от редких категорий
for col in ['nom_5', 'nom_6', 'nom_7', 'nom_8', 'nom_9']:
abs_freq = data[col].value_counts(dropna=False)
data[col] = np.where(
data[col].isin(abs_freq[abs_freq >= 100].index.tolist()),
data[col], 'Other')

Удаляем идентификатор, формируем массив меток и массив признаков,
смотрим типы переменных и количество пропусков.
# удаляем идентификатор
data.drop('id', axis=1, inplace=True)
# формируем массив меток и массив признаков
labels = data.pop('target').values
# смотрим типы переменных и количество пропусков
data.info()

RangeIndex: 600000 entries, 0 to 599999
Data columns (total 23 columns):
# Column Non-Null Count Dtype
--- ------ -------------- ----0 bin_0 582106 non-null float64
1 bin_1 581997 non-null float64
2 bin_2 582070 non-null float64
3 bin_3 581986 non-null object
4 bin_4 581953 non-null object
5 nom_0 581748 non-null object
6 nom_1 581844 non-null object
7 nom_2 581965 non-null object
8 nom_3 581879 non-null object
9 nom_4 581965 non-null object
10 nom_5 582222 non-null object
11 nom_6 581869 non-null object
12 nom_7 581997 non-null object
13 nom_8 582245 non-null object
14 nom_9 581927 non-null object

244



Инструменты

15 ord_0 581712 non-null
16 ord_1 581959 non-null
17 ord_2 581925 non-null
18 ord_3 582084 non-null
19 ord_4 582070 non-null
20 ord_5 582287 non-null
21 day
582048 non-null
22 month 582012 non-null
dtypes: float64(6), object(17)
memory usage: 105.3+ MB

float64
object
object
object
object
object
float64
float64

Видим, что каждая переменная имеет пропуск.
Создаем индикаторы пропусков для каждой переменной.
# для всех столбцов создаем индикаторы пропусков
for col in data.columns:
data[col + '_isnan'] = np.where(data[col].isnull(), 'T', 'F')

Теперь создаем две новые переменные на основе переменной ord5, просто
извлекая первый и второй символы в ее строковых значениях.
# создаем две новые переменные на основе переменной ord5,
# просто извлекая первый и второй символы
data['ord_5a'] = data['ord_5'].str[0]
data['ord_5b'] = data['ord_5'].str[1]

Формируем список столбцов.
# формируем список столбцов
columns = [col for col in data.columns]

Разбиваем набор на обучающую и тестовую выборки.
# разбиваем набор на обучающую и тестовую выборки
X_train, X_test, y_train, y_test = train_test_split(
data,
labels,
test_size=0.3,
stratify=labels,
random_state=42)

С помощью функции get_dummies() получим дамми-переменные. Мы попробуем создать массивы с использованием и без использования параметра
sparse. С помощью значения True параметра drop_first первую категорию каж­
дой переменной объявляем опорной.
# создаем дамми-переменные, не используем
# параметр sparse
X_tr_non_sparse = pd.get_dummies(
X_train,
columns=columns,
drop_first=True,
sparse=False)
X_tst_non_sparse = pd.get_dummies(

7.5. Выполнение дамми-кодирования с помощью класса OneHotEncoder...  245
X_test,
columns=columns,
drop_first=True,
sparse=False)
# создаем дамми-переменные, используем
# параметр sparse
X_tr_sparse = pd.get_dummies(
X_train,
columns=columns,
drop_first=True,
sparse=True)
X_tst_sparse = pd.get_dummies(
X_test,
columns=columns,
drop_first=True,
sparse=True)
# смотрим формы массивов
print('non_sparse:', X_tr_non_sparse.shape, X_tst_non_sparse.shape)
print('sparse:', X_tr_sparse.shape, X_tst_sparse.shape)
non_sparse: (420000, 5026) (180000, 5026)
sparse: (420000, 5026) (180000, 5026)

Итак, у нас каждый массив содержит по 5026 признаков.
Теперь импортируем функцию roc_auc_score() для вычисления AUC-ROC
и класс LogisticRegression.
# импортируем функцию roc_auc_score() для
# вычисления AUC-ROC
from sklearn.metrics import roc_auc_score
# импортируем класс LogisticRegression
from sklearn.linear_model import LogisticRegression

Теперь обучаем модель логистической регрессии на массивах, созданных
с использованием и без использования параметра sparse.
%%time
# обучаем модель логистической регрессии
# на массиве признаков, созданном с
# с помощью параметра sparse
logreg = LogisticRegression(solver='liblinear').fit(
X_tr_sparse, y_train)
print("AUC на обучающей выборке: {:.3f}".format(
roc_auc_score(y_train, logreg.predict_proba(
X_tr_sparse)[:, 1])))
print("AUC на тестовой выборке: {:.3f}".format(
roc_auc_score(y_test, logreg.predict_proba(
X_tst_sparse)[:, 1])))
AUC на обучающей выборке: 0.801
AUC на тестовой выборке: 0.787
CPU times: user 11.9 s, sys: 125 ms, total: 12 s

246



Инструменты

Wall time: 12.1 s
%%time
# обучаем модель логистической регрессии
# на массиве признаков, созданном с
# без помощи параметра sparse
logreg = LogisticRegression(solver='liblinear').fit(
X_tr_non_sparse, y_train)
print("AUC на обучающей выборке: {:.3f}".format(
roc_auc_score(y_train, logreg.predict_proba(
X_tr_non_sparse)[:, 1])))
print("AUC на тестовой выборке: {:.3f}".format(
roc_auc_score(y_test, logreg.predict_proba(
X_tst_non_sparse)[:, 1])))
AUC на обучающей выборке: 0.801
AUC на тестовой выборке: 0.787
CPU times: user 1min 39s, sys: 24.7 s, total: 2min 4s
Wall time: 1min 55s

Обучение на данных в формате разреженных матриц заняло 12 секунд,
а обучение на обычных данных заняло уже 2 минуты.

7.6. Автоматическое построение конвейеров моделей
с помощью класса Pipeline
Ранее мы говорили, что на практике строим не одну модель, а конвейер нескольких моделей. Класс Pipeline позволяет автоматизировать построение
конвейеров.
Он позволяет объединить модели предварительной обработки (например,
модели импутации и стандартизации данных) с моделью машинного обучения типа логистической регрессии. Класс Pipeline предусматривает методы
.fit(), .predict() и .score() и имеет все те же свойства, что и любая модель
машинного обучения scikit-learn. Конвейер представляет собой список этапов – двухэлементных кортежей. Первый элемент кортежа – имя этапа (любая
строка на ваш выбор, за одним исключением: имя не должно содержать символ двойного подчеркивания __). Второй элемент кортежа – экземпляр класса.

Рис. 32 Схема конвейера

Здесь мы создали два этапа: первый этап, названный 'imputer', является
экземпляром класса SimpleImputer, а второй, названный 'forest', является экземпляром класса RandomForestClassifier.

7.6. Автоматическое построение конвейеров моделей...  247
Класс Pipeline не ограничивается предварительной обработкой и классификацией, с его помощью можно объединить любое количество моделей. Например, можно создать конвейер, включающий в себя выделение признаков,
отбор признаков, масштабирование и классификацию, в общей сложности четыре этапа. Кроме того, последним этапом вместо классификации может быть
регрессия или кластеризация.
Единственное требование, предъявляемое к моделям в конвейере, заключается в том, что все этапы, кроме последнего, должны использовать метод
.transform(), таким образом, они позволяют сгенерировать новое представление данных, которое можно использовать на следующем этапе. Последний
этап должен использовать метод .fit().
Допустим, у нас есть конвейер, включающий две модели предварительной
подготовки (трансформера) T1 и T2 и модель машинного обучения E.
Во время вызова Pipeline.fit конвейер поочередно вызывает метод .fit(),
а затем метод .transform() каждого этапа, вводная информация представляет
собой вывод метода .transform() для предыдущего этапа. Для последнего этапа конвейера просто вызывается метод .fit().
Во время вызова Pipeline.predict конвейер поочередно вызывает метод
.transform() каждого этапа, вводная информация представляет собой вывод
метода .transform() для предыдущего этапа. Для последнего этапа конвейера
просто вызывается метод .predict().

pipe.fit(X, y)
X

T1.fit(X, y)

T1.transform(X, y)

T1

X1

T2.fit(X1, y)

T2.transform(X1, y)

T2

X2

E.fit(X2, y)

E

pipe.predict(X’)
X

T1.transform(X’)

X’1

T2.transform(X’1)

X’2

E.predict(X’2)

y’

Рис. 33 Схема работы методов Pipeline.fit() и Pipeline.predict()

Давайте попробуем воспользоваться классом Pipeline, для этого импортируем необходимые библиотеки, классы и загрузим данные, которые мы уже
использовали ранее для построения логистической регрессии.
# импортируем необходимые библиотеки, функцию train_test_split()
# и классы StandardScaler, LogisticRegression, Pipeline
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
# записываем CSV-файл в объект DataFrame
data = pd.read_csv('Data/StateFarm.csv', sep=';')
data.head(3)

248



Инструменты

Выполним случайное разбиение данных на обучающую и тестовую выборки:
сформируем обучающий массив признаков, тестовый массив признаков, обучающий массив меток, тестовый массив меток.
# разбиваем данные на обучающие и тестовые:
# получаем обучающий массив признаков, тестовый
# массив признаков, обучающий массив меток,
# тестовый массив меток
X_train, X_test, y_train, y_test = train_test_split(
data.drop('Response', axis=1),
data['Response'],
test_size=0.3,
stratify=data['Response'],
random_state=42)

Теперь создаем конвейер.
# создаем конвейер – экземпляр класса Pipeline
pipe = Pipeline([('scaler', StandardScaler()),
('logreg', LogisticRegression(
solver='lbfgs', max_iter=200))])

Здесь мы создали два этапа: первый этап, названный 'scaler', является экземпляром класса StandardScaler, а второй, названный 'logreg', является экземпляром класса LogisticRegression.
Существует удобная функция make_pipeline(), которая позволяет создать
конвейер и автоматически присвоить имя каждому этапу, исходя из его класса
(напомним, что каждый этап представляет собой двухэлементный кортеж, содержащий имя и экземпляр класса).
# импортируем функцию make_pipeline()
from sklearn.pipeline import make_pipeline
# создаем с ее помощью конвейер
pipe_quick = make_pipeline(
StandardScaler(),
LogisticRegression(solver='lbfgs', max_iter=200))

Объекты-конвейеры pipe и pipe_quick выполняют одну и ту же последовательность операций, но в случае с pipe_quick имена этапов присваиваются автоматически. Мы можем взглянуть на имена этапов с помощью свойства steps.
# смотрим этапы конвейера
pipe_quick.steps
[('standardscaler', StandardScaler()),
('logisticregression', LogisticRegression(max_iter=200))]

7.6. Автоматическое построение конвейеров моделей...  249
Этапам присвоены имена standardscaler и logisticregression. В общем,
имена этапов – это просто названия классов, написанные строчными буквами.
Теперь мы можем обучить конвейер точно так же, как и любую другую модель scikit-learn.
# обучаем конвейер
pipe.fit(X_train, y_train)

В данном случае pipe.fit сначала вызывает метод .fit() объекта scaler
(вычисляет среднее и стандартное отклонение для каждого признака), затем
метод .transform() объекта scaler (вычитает из исходного значения каждого признака среднее значение и делит полученный результат на стандартное
отклонение, т. е. стандартизирует данные), и, наконец, метод .fit() объекта
logreg (строит модель логистической регрессии на основе стандартизированных данных). Чтобы оценить правильность модели на обучающих и тестовых
данных, мы просто вызываем pipe.score. Теперь pipe.score сначала вызывает метод .transform() объекта scaler (стандартизирует данные), затем метод
.score() объекта logreg (вычисляет правильность).
# оцениваем качество конвейера на обучающих данных
print("Правильность на обучающей выборке: {:.3f}".format(
pipe.score(X_train, y_train)))
# оцениваем качество конвейера на тестовых данных
print("Правильность на тестовой выборке: {:.3f}".format(
pipe.score(X_test, y_test)))
Правильность на обучающей выборке: 0.900
Правильность на тестовой выборке: 0.900

Видно, что приведенный вывод идентичен результату, который мы получили, используя программный код в разделе 7.1, когда выполняли преобразования вручную. С помощью конвейера мы сократили программный код, необходимый для нашего процесса «предварительная обработка + классификация».
Теперь извлекаем константу и коэффициенты логистической регрессии –
последнего этапа нашего конвейера. Для этого необходимо воспользоваться
атрибутом named_steps, позволяющим получить доступ к любому этапу конвейера по заданному имени.
# извлекаем константу
intercept = np.round(pipe.named_steps['logreg'].intercept_[0], 3)
intercept
-2.205
# извлекаем коэффициенты
coef = np.round(pipe.named_steps['logreg'].coef_, 3)
coef
array([[-0.022, 0.042, 0.079, -0.056, -0.004, -0.017, -0.109]])

Затем задаем список названий признаков и с помощью функции zip()
«сшиваем» константу и коэффициенты с названиями признаков.

250



Инструменты

# записываем названия признаков
feat_labels = X_train.columns
# печатаем название "Константа"
print("Константа:", intercept)
# печатаем название "Регрессионные коэффициенты"
print("Регрессионные коэффициенты:")
# для удобства сопоставим каждому названию
# признака соответствующий коэффициент
for c, feature in zip(coef[0], feat_labels):
print(feature, c)
Константа: -2.205
Регрессионные коэффициенты:
Customer Lifetime Value -0.022
Income 0.042
Monthly Premium Auto 0.079
Months Since Last Claim -0.056
Months Since Policy Inception -0.004
Number of Open Complaints -0.017
Number of Policies -0.109

7.7. Знакомство с классом ColumnTransformer
Часто наши данные содержат как количественные, так и категориальные переменные. Для каждого типа переменных требуется своя предварительная
подготовка. Допустим, пропуски в категориальных переменных мы хотим
заменить самой часто встречающейся категорией и потом применить к категориальным переменным дамми-кодирование. А пропуски в количественных
переменных мы хотим заменить медианами и потом применить к количест­
венным переменным стандартизацию. Можно пойти еще дальше: пропуски
в каких-то категориальных переменных заменить самой часто встречающейся
категорией, а пропуски в других категориальных переменных записать в отдельную категорию.
Класс ColumnTransformer (входящий в новый модуль compose) позволяет задать конкретные столбцы или наборы столбцов, которые нужно преобразовать
отдельно, и затем признаки, полученные с помощью каждого трансформера,
будут объединены вместе и сформируют единое пространство трансформированных признаков.

Класс ColumnTransformer принимает на вход список трехэлементных
кортежей. Первое значение в кортеже является именем самого кортежа, второе – экземпляр класса, третье – список столбцов, которые нужно преобразовать. Кортеж будет выглядеть следующим образом: ('name',
SomeTransformer(parameters), columns).

7.7. Знакомство с классом ColumnTransformer  251
Столбцы необязательно должны быть именами столбцов, вы можете использовать целочисленные индексы столбцов, массив булевых значений и даже
функцию, которая принимает в качестве аргумента весь DataFrame и возвращает набор столбцов.
Необходимо помнить, что порядок столбцов в выходном пространстве
трансформированных признаков соответствует тому порядку, в котором
столбцы указаны в списке трансформеров. Столбцы исходного пространства признаков, не указанные в списке трансформеров, исключаются из получаемого пространства трансформированных признаков. С этой целью для
параметра remainder по умолчанию задано значение 'drop'. Если же указать
remainder='passthrough', то оставшиеся столбцы добавляются справа к трансформированным столбцам.
Давайте загрузим данные и потренируемся использовать класс
ColumnTransformer.
# импортируем необходимые библиотеки, функцию train_test_split()
# и классы SimpleImputer, StandardScaler, OneHotEncoder,
# ColumnTransformer, LogisticRegression, Pipeline
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import (StandardScaler,
OneHotEncoder)
from sklearn.compose import ColumnTransformer
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
# записываем CSV-файл в объект DataFrame
data = pd.read_csv('Data/StateFarm_missing.csv', sep=';')
data.head(5)

Исходный набор содержит расширенные данные американской автостраховой компании StateFarm (добавлены категориальные признаки). Он представляет собой записи о 8293 клиентах, классифицированных на два класса: 0 – отклика нет на предложение автостраховки (7462 клиента) и 1 – отклик есть на
предложение автостраховки (831 клиент). По каждому наблюдению (клиенту)
фиксируются следующие переменные (характеристики):
 количественный признак Пожизненная ценность клиента [Customer
Lifetime Value];
 категориальный признак Вид страхового покрытия [Coverage];
 категориальный признак Образование [Education];
 категориальный признак Тип занятости [EmploymentStatus];

252



Инструменты

 категориальный признак Пол [Gender];
 количественный признак Доход клиента [Income];
 количественный признак Размер ежемесячной автостраховки [Monthly
Premium Auto];
 количественный признак Количество месяцев со дня подачи последнего
страхового требования [Months Since Last Claim];
 количественный признак Количество месяцев с момента заключения
страхового договора [Months Since Policy Inception];
 количественный признак Количество открытых страховых обращений
[Number of Open Complaints];
 количественный признак Количество полисов [Number of Policies];
 бинарная зависимая переменная Отклик на предложение автостраховки
[Response].
Видим, что наши данные содержат как количественные, так и категориальные переменные. Кроме того, у нас есть пропуски.
Выполним случайное разбиение данных на обучающую и тестовую выборки: сформируем обучающий массив признаков, тестовый массив признаков,
обучающий массив меток, тестовый массив меток.
# разбиваем данные на обучающие и тестовые: получаем обучающий
# массив признаков, тестовый массив признаков, обучающий массив
# меток, тестовый массив меток
X_train, X_test, y_train, y_test = train_test_split(
data.drop('Response', axis=1),
data['Response'],
test_size=0.3,
stratify=data['Response'],
random_state=42)

Давайте создадим списки категориальных и количественных переменных.
# создаем списки категориальных
# и количественных столбцов
cat_columns = X_train.select_dtypes(
include='object').columns.tolist()
num_columns = X_train.select_dtypes(
exclude='object').columns.tolist()

Теперь создаем конвейер для количественных переменных и конвейер для
категориальных переменных. Для количественных переменных будем применять импутацию медианами и стандартизацию, а для категориальных переменных будем применять импутацию самой часто встречающейся категорией
и дамми-кодирование. Конвейеры, ответственные за операции предварительной подготовки (трансформации переменных), так и называют – трансформерами (transformers).
# создаем конвейер для количественных переменных
num_pipe = Pipeline([
('imputer', SimpleImputer(strategy='median')),
('scaler', StandardScaler())
])

7.7. Знакомство с классом ColumnTransformer  253
# создаем конвейер для категориальных переменных
cat_pipe = Pipeline([
('imputer', SimpleImputer(strategy='most_frequent')),
('ohe', OneHotEncoder(sparse=False, handle_unknown='ignore'))
])

Теперь создаем список трансформеров – трехэлементных кортежей. Первый
элемент кортежа – название конвейера с преобразованиями для определенного списка переменных (столбцов), второй элемент – собственно конвейер,
и третий элемент – соответствующий список переменных (столбцов).
# создаем список трехэлементных кортежей, в котором
# первый элемент кортежа – название конвейера с
# преобразованиями для определенного типа признаков
transformers = [('num', num_pipe, num_columns),
('cat', cat_pipe, cat_columns)]

Теперь создаем экземпляр класса ColumnTransformer, передав список трансформеров.
# передаем список трансформеров в ColumnTransformer
transformer = ColumnTransformer(transformers=transformers)

Давайте взглянем на созданный нами объект. Здесь нас интересует порядок столбцов, который задает экземпляр класса ColumnTransformer. Это нам
понадобится для правильного сопоставления весов логистической регрессии
названиям признаков.
# смотрим трансформер
transformer

Вывод представляет собой схему предварительной обработки данных, заданную экземпляром класса ColumnTransformer.
Щелкнем по стрелке слева от ColumnTransformer и получим следующий
вывод.

254



Инструменты

ColumnTransformer(transformers=[('num',
Pipeline(steps=[('imputer',
SimpleImputer(strategy='median')),
('scaler', StandardScaler())]),
['Customer Lifetime Value', 'Income',
'Monthly Premium Auto',
'Months Since Last Claim',
'Months Since Policy Inception',
'Number of Open Complaints',
'Number of Policies']),
('cat',
Pipeline(steps=[('imputer',
SimpleImputer(strategy='most_frequent')),
('ohe',
OneHotEncoder(handle_unknown='ignore',
sparse=False))]),
['Coverage', 'Education', 'EmploymentStatus',
'Gender'])])

Обращаем внимание на порядок столбцов. Сначала у нас перечисляются
имена количественных переменных, а затем имена категориальных переменных, потому что в нашем списке трехэлементных кортежей, который мы
передали в ColumnTransformer, сначала указан конвейер для количественных
переменных, а затем конвейер для категориальных переменных. Этот порядок
столбцов отличается от порядка столбцов в исходном датафрейме.
Таким образом, класс ColumnTransformer поменял порядок переменных,
в новом пространстве признаков сначала будут располагаться количественные переменные, затем категориальные переменные, преобразованные с помощью класса OneHotEncoder в дамми-переменные.
Создаем и обучаем итоговый конвейер. Итоговый конвейер содержит список из двух этапов, этап 'transformer' – список трансформеров, выполняющих
преобразования для переменных разного типа, и этап 'logreg', строящий модель логистической регрессии.
# задаем итоговый конвейер
ml_pipe = Pipeline(
[('transform', transformer),
('logreg', LogisticRegression(solver='lbfgs',
max_iter=200))])
# обучаем итоговый конвейер
ml_pipe.fit(X_train, y_train)
# оцениваем качество конвейера на обучающих данных
print("Правильность на обучающей выборке: {:.3f}".format(
ml_pipe.score(X_train, y_train)))
# оцениваем качество конвейера на тестовых данных
print("Правильность на тестовой выборке: {:.3f}".format(
ml_pipe.score(X_test, y_test)))
Правильность на обучающей выборке: 0.900
Правильность на тестовой выборке: 0.899

Теперь извлекаем дамми-переменные, созданные классом OneHotEncoder.
Вспомним об атрибуте named_steps класса Pipeline, позволяющем получить

7.7. Знакомство с классом ColumnTransformer  255
доступ к любому этапу конвейера по заданному имени. Итак, с помощью
атрибута named_steps класса Pipeline обращаемся к этапу 'transform' итогового конвейера ml_pipe, затем с помощью свойства named_transformers_ класса ColumnTransformer обращаемся к названию конвейера для категориальных
признаков 'cat', находящемуся внутри этапа 'transform'. С помощью атрибута named_steps класса Pipeline обращаемся к этапу 'ohe', находящемуся внут­
ри конвейера для категориальных признаков 'cat', затем с помощью метода
.get_feature_names_out() класса ColumnTransformer получаем массив c именами признаков после трансформации и создаем из него список.
# извлекаем дамми-переменные, созданные
# классом OneHotEncoder
cat = ml_pipe.named_steps['transform'].named_transformers_['cat']
onehot_columns = list(cat.named_steps['ohe'].get_feature_names_out(
input_features=cat_columns))
onehot_columns
['Coverage_Basic',
'Coverage_Extended',
'Coverage_Premium',
'Education_Bachelor',
'Education_College',
'Education_Doctor',
'Education_High School or Below',
'Education_Master',
'EmploymentStatus_Disabled',
'EmploymentStatus_Employed',
'EmploymentStatus_Medical Leave',
'EmploymentStatus_Retired',
'EmploymentStatus_Unemployed',
'Gender_F',
'Gender_M']
# еще можно применить такой стиль
cat = ml_pipe.named_steps.transform.named_transformers_.cat
onehot_columns = list(cat.named_steps.ohe.get_feature_names_out(
input_features=cat_columns))
onehot_columns
['Coverage_Basic',
'Coverage_Extended',
'Coverage_Premium',
'Education_Bachelor',
'Education_College',
'Education_Doctor',
'Education_High School or Below',
'Education_Master',
'EmploymentStatus_Disabled',
'EmploymentStatus_Employed',
'EmploymentStatus_Medical Leave',
'EmploymentStatus_Retired',
'EmploymentStatus_Unemployed',
'Gender_F',
'Gender_M']

256



Инструменты

Добавляем в конец списка количественных переменных список дамми-переменных, созданных классомOneHotEncoder, т. е. сохраняем тот же порядок
столбцов, что задал класс ColumnTransformer.
# добавляем в конец списка количественных переменных
# дамми-переменные, созданные OneHotEncoder, т. е.
# сохраняем тот же порядок столбцов, что задал
# ColumnTransformer
all_columns_lst = num_columns + onehot_columns
all_columns_lst
['Customer Lifetime Value',
'Income',
'Monthly Premium Auto',
'Months Since Last Claim',
'Months Since Policy Inception',
'Number of Open Complaints',
'Number of Policies',
'Coverage_Basic',
'Coverage_Extended',
'Coverage_Premium',
'Education_Bachelor',
'Education_College',
'Education_Doctor',
'Education_High School or Below',
'Education_Master',
'EmploymentStatus_Disabled',
'EmploymentStatus_Employed',
'EmploymentStatus_Medical Leave',
'EmploymentStatus_Retired',
'EmploymentStatus_Unemployed',
'Gender_F',
'Gender_M']

Теперь извлекаем константу и коэффициенты логистической регрессии –
последнего этапа нашего конвейера. Для этого вновь необходимо воспользоваться атрибутом named_steps класса Pipeline.
# извлекаем константу
intercept = np.round(ml_pipe.named_steps['logreg'].intercept_[0], 3)
intercept
-1.697
# извлекаем коэффициенты
coef = np.round(ml_pipe.named_steps['logreg'].coef_, 3)
coef
array([[ 7.000e-03,
-4.000e-02,
-1.640e-01,
1.000e-03,
-7.000e-03,

2.300e-02, 1.240e-01,
-5.900e-02, -3.600e-02,
-2.100e-02, 4.120e-01,
-5.240e-01, -1.000e-01,
6.000e-03]])

-4.700e-02,
1.290e-01,
-1.710e-01,
1.648e+00,

-2.900e-02,
-9.400e-02,
-5.700e-02,
-1.026e+00,

7.7. Знакомство с классом ColumnTransformer  257
C помощью функции zip() «сшиваем» константу и коэффициенты с названиями признаков.
# печатаем название "Константа"
print("Константа:", intercept)
# печатаем название "Регрессионные коэффициенты"
print("Регрессионные коэффициенты:")
# для удобства сопоставим каждому названию
# признака соответствующий коэффициент
for c, feature in zip(coef[0], all_columns_lst):
print(feature, c)
Константа: -1.697
Регрессионные коэффициенты:
Customer Lifetime Value 0.007
Income 0.023
Monthly Premium Auto 0.124
Months Since Last Claim -0.047
Months Since Policy Inception -0.029
Number of Open Complaints -0.04
Number of Policies -0.059
Coverage_Basic -0.036
Coverage_Extended 0.129
Coverage_Premium -0.094
Education_Bachelor -0.164
Education_College -0.021
Education_Doctor 0.412
Education_High School or Below -0.171
Education_Master -0.057
EmploymentStatus_Disabled 0.001
EmploymentStatus_Employed -0.524
EmploymentStatus_Medical Leave -0.1
EmploymentStatus_Retired 1.648
EmploymentStatus_Unemployed -1.026
Gender_F -0.007
Gender_M 0.006

С помощью функций make_pipeline() и make_column_transformer(), класса
make_column_selector можно сократить объем программного кода, требуемого
для создания экземпляра класса ColumnTransformer.
Существует удобная функция make_column_transformer(), которая позволяет
создать экземпляр класса ColumnTransformer и автоматически присвоить имя
каждому конвейеру, исходя из номера списка столбцов, к которому он должен
быть применен. Например, конвейер, предназначенный для обработки первого
списка – списка количественных признаков, будет назван 'pipeline-1', а конвейер, предназначенный для обработки второго списка – списка категориальных признаков, будет назван 'pipeline-2'. Списки признаков определенного
типа можно создавать автоматически с помощью класса make_column_selector.
У класса make_column_selector будут два параметра: параметр dtype_include
задает тип признаков для включения, а параметр dtype_exclude – тип признаков для исключения. В итоге мы можем передать в функцию make_column_
transformer() двухэлементные кортежи, в которых первый элемент – конвей-

258



Инструменты

ер, создаваемый с помощью функции make_pipeline(), а второй элемент – список признаков, создаваемый с помощью класса make_column_selector.
Давайте импортируем функции make_pipeline() и make_column_
transformer(), класс make_column_selector.
# импортируем функции make_pipeline() и make_column_transformer(),
# класс make_column_selector
from sklearn.compose import (make_column_transformer,
make_column_selector)
from sklearn.pipeline import make_pipeline

Теперь с помощью функции make_column_transformer() автоматически создаем экземпляр класса ColumnTransformer, при этом автоматически создав
трансформеры с помощью функции make_pipeline() и списки признаков с помощью класса make_column_selector.
# автоматически создаем экземпляр класса ColumnTransformer,
# при этом автоматически создав трансформеры и списки
# признаков
column_transformer_quick = make_column_transformer(
(make_pipeline(SimpleImputer(strategy='median'),
StandardScaler()),
make_column_selector(dtype_include=np.number)),
(make_pipeline(SimpleImputer(strategy='most_frequent'),
OneHotEncoder()),
make_column_selector(dtype_include=object)))

Давайте посмотрим получившийся результат (вывод для удобства восприятия изменен).
column_transformer_quick
ColumnTransformer(
transformers=[('pipeline-1', Pipeline(
steps=[('simpleimputer', SimpleImputer(strategy='median')),
('standardscaler', StandardScaler())]),