/
Author: Гинько А.Ю.
Tags: данные языки программирования трансляторы программирование анализ данных компьютерные технологии язык программирования python
ISBN: 978-5-93700-381-2
Year: 2025
Text
Инженерия данных
в Python
Data Engineering
Foundations
Core Techniques for Data Analysis
with Pandas, NumPy, and Scikit-Learn
Инженерия данных
в Python
Основы анализа данных
с помощью Pandas, NumPy и Scikit-learn
Москва, 2025
УДК 004.6:004.438Python
ББК 16.35+32.973.2
И62
И62 Инженерия данных в Python: Основы анализа данных с помощью Pandas,
NumPy и Scikit-learn / пер. с англ. А. Ю. Гинько. – М.: ДМК Пресс, 2025. –
528 с.: ил.
ISBN 978-5 -93700-381-2
Цель этой книги – научить вас подготавливать и преобразовывать сырые
данные, конструировать новые признаки и придавать исходным данным фор-
му, пригодную для будущего интеллектуального анализа при помощи методов
машинного и глубокого обучения. Предложенные в книге эффективные техники
и приемы будут полезны для обработки любого объема данных. Примеры кода
опираются на наиболее популярные библиотеки Python для работы с данными,
такие как Pandas, NumPy и Scikit-learn.
Издание предназначено как делающим первые шаги в освоении науки о данных,
так и практикующим специалистам, желающим улучшить свои навыки.
УДК 004.6:004.438Python
ББК 16.35+32.973.2
All rights reserved. No part of this book may be reproduced, stored in a retrieval system, or
transmitted in any form or by any means, without the prior written permission of the publisher,
except in the case of brief quotations embedded in critical articles or reviews.
Все права защищены. Любая часть этой книги не может быть воспроизведена в ка-
кой бы то ни было форме и какими бы то ни было средствами без письменного разрешения
владельцев авторских прав.
© 2024 Cuantum Technologies
ISBN 979-8 -89587-355-7 (англ.)
ISBN 978-5 -93700-381-2 (рус.)
© Перевод, оформление, издание,
ДМК Пресс, 2025
«Искусственный интеллект – это новое электричество».
– Эндрю Ын (Andrew Ng), сооснователь Coursera,
профессор и внештатный преподаватель
Стэнфордского университета
Содержание
От издательства ......................................................................................................13
Кто мы? ......................................................................................................................14
О переводчике ........................................................................................................16
Введение ...................................................................................................................17
Часть I. ПОДГОТОВКА К РАСШИРЕННОМУ АНАЛИЗУ
ДАННЫХ ..................................................................................................................21
Глава 1. Введение. Основы и не только ..........................................................22
1.1. Введение в промежуточный анализ данных.......................................................23
1.1.1. Ключевые концепции промежуточного анализа данных...........................26
1.1.2. Пример: промежуточный анализ данных с помощью Pandas и NumPy ....32
1.1.3. Заполнение пропущенных значений ...........................................................36
1.1.4. Вычисление скользящих средних .................................................................40
1.1.5. Оптимизация типов данных .........................................................................42
1.1.6. Ключевые выводы..........................................................................................46
1.2. Путь от простого к сложному ...............................................................................47
1.2.1. От простых техник манипулирования данными к более сложным ...........47
1.2.2. Промежуточный уровень манипулирования данными ..............................49
1.2.3. Построение эффективных рабочих процессов ............................................50
1.2.4. Использование библиотеки NumPy для повышения
производительности ...............................................................................................53
1.3. Pandas, NumPy и Scikit-learn в действии .............................................................55
1.3.1. Pandas: манипулирование табличными данными на экспертном
уровне.......................................................................................................................56
1.3.2. NumPy: высокоэффективные числовые вычисления..................................60
1.3.3 . Использование инструментов NumPy для преобразований ......................64
1.3.4 . Scikit-learn: эксперт в области машинного обучения .................................66
1.3.5. Почему Scikit-learn? .......................................................................................68
1.3.6. Собираем все вместе: полный рабочий процесс .........................................69
1.3 .7. Ключевые выводы ..........................................................................................71
1.4. Практические упражнения ...................................................................................72
1.5. Возможные проблемы ..........................................................................................76
1.5 .1. Неэффективное манипулирование данными в Pandas...............................76
Содержание 7
1.5.2. Неправильная обработка пропущенных значений .....................................77
1.5.3. Неправильное применение масштабирования и преобразования
признаков ................................................................................................................78
1.5.4. Неправильное использование конвейеров Scikit-learn ..............................78
1.5.5. Неправильная интерпретация результатов модели в Scikit-learn .............79
1.5.6. Узкие места в операциях NumPy ..................................................................79
1.5.7. Избыточное конструирование признаков....................................................80
Заключение ..................................................................................................................80
Глава 2. Оптимизация потоков данных ..........................................................82
2.1. Расширенное манипулирование данными с Pandas ..........................................82
2.1.1. Сложная фильтрация и извлечение подмножеств ......................................85
2.1.2. Многоуровневая группировка с агрегацией ................................................93
2.1.3. Сводные таблицы и изменение структуры данных ....................................96
2.1.4. Эффективный анализ временных рядов.................................................... 101
2.1.5 . Оптимизация производительности и использования памяти................. 105
2.2. Повышение производительности при помощи массивов NumPy...................109
2.2.1 . Работа с массивами в NumPy ......................................................................109
2.2.2 . Векторизованные операции: скорость и простота.................................... 113
2.2.3. Транслирование: гибкие операции с массивами ...................................... 114
2.2 .4. Работа с памятью: типы данных в NumPy .................................................116
2.2 .5 . Многомерные массивы: работа со сложными структурами данных .......... 118
2.3. Комбинирование инструментов для выполнения эффективного анализа
данных ........................................................................................................................ 121
2.3 .1. Шаг 1: предварительная обработка данных с помощью Pandas
и NumPy .................................................................................................................121
2.3 .2. Шаг 2: конструирование признаков с помощью Pandas и NumPy ...........125
2.3 .3. Шаг 3: построение модели машинного обучения с помощью
Scikit-learn .............................................................................................................. 127
2.3 .4. Шаг 4: оптимизация рабочих процессов с помощью конвейеров
Scikit-learn .............................................................................................................. 129
2.4. Практические упражнения ................................................................................. 132
2.5. Возможные проблемы ........................................................................................ 137
2.5 .1. Большие накладные расходы при работе с большими наборами
данных в Pandas .................................................................................................... 137
2.5 .2. Игнорирование или неправильное использование векторизации
в NumPy ..................................................................................................................138
2.5 .3. Утечка информации в конвейерах Scikit-learn .......................................... 138
2.5 .4. Чрезмерная надежда на значения гиперпараметров модели
по умолчанию ........................................................................................................ 139
2.5 .5. Излишняя сложность конвейеров............................................................... 139
Заключение ................................................................................................................ 140
Контрольный опрос. Часть I. Подготовка данных
для дальнейшего анализа................................................................................. 141
8 Содержание
Часть II. КОНСТРУИРОВАНИЕ ПРИЗНАКОВ
ДЛЯ МОДЕЛЕЙ МАШИННОГО ОБУЧЕНИЯ ......................................... 145
Проект 1. Предсказание стоимости домов с помощью
конструирования признаков ............................................................................ 146
Исследование переменных и очистка данных ........................................................ 146
Конструирование признаков .................................................................................... 151
Построение и оценка предсказательной модели .................................................... 157
Итоги проекта ............................................................................................................ 161
Дальнейшие улучшения ............................................................................................ 162
Глава 3. Роль конструирования признаков в машинном обучении ... 163
3.1. Почему так важно конструировать признаки? ................................................. 163
3.1.1. Области влияния признаков на качество моделей ................................... 164
3.2. Примеры эффективного конструирования признаков .................................... 172
3.2.1. Создание переменных взаимодействия .................................................... 173
3.2.2. Создание признаков на основе временных рядов .................................... 176
3.2.3. Разбиение числовых переменных на интервалы ...................................... 185
3.2.4. Кодирование на основе целевой переменной ...........................................189
3.3. Практические упражнения ................................................................................. 193
3.4. Возможные проблемы ........................................................................................ 197
3.4.1 . Переобучение из-за слишком большого количества признаков.............. 197
3.4.2 . Мультиколлинеарность ............................................................................... 198
3.4.3 . Утечка информации..................................................................................... 198
3.4.4 . Неправильная интерпретация признаков на основе времени ................ 199
3.4.5 . Неподобающее масштабирование признаков ........................................... 199
3.4.6 . Недооценка знаний о предметной области ............................................... 200
Заключение ................................................................................................................ 201
Глава 4. Сложные техники заполнения пропусков в данных ............... 202
4.1. Использование продвинутых техник заполнения пропущенных значений . . 202
4.1.1. Подстановка с помощью метода k-ближайших соседей ........................... 203
4.1.2. Метод множественной подстановки с помощью цепных уравнений
(MICE) ..................................................................................................................... 208
4.1.3. Использование моделей машинного обучения для подстановки ............ 212
4.2. Обработка пропущенных значений в больших наборах данных .................... 216
4.2.1. Оптимизация техник подстановки с целью обеспечения
масштабируемости................................................................................................ 217
4.2 .2 . Обработка столбцов с большим количеством пропущенных значений .... 224
4.2.3. Использование распределенных вычислительных систем для
заполнения пропусков .......................................................................................... 229
4.2.4. Ключевые выводы........................................................................................ 238
4.3. Практические упражнения ................................................................................. 240
4.4. Возможные проблемы ........................................................................................ 245
4.4.1. Погрешность модели при неправильной подстановке пропусков .......... 245
4.4.2. Переобучение модели вследствие замены пропусков в тестовой
выборке .................................................................................................................. 246
Содержание 9
4.4.3. Удаление слишком большого количества данных ..................................... 246
4.4.4. Неправильное интерпретирование данных о временных рядах ............. 247
4.4 .5 . Вычислительная сложность при работе с большими наборами данных .... 247
4.4.6. Сложности с нахождением шаблонов в пропущенных значениях .......... 248
Заключение ................................................................................................................ 248
Глава 5. Преобразование и масштабирование признаков .................... 250
5.1. Масштабирование и нормализация: оптимальное применение .................... 250
5.1.1. Почему так важны масштабирование и нормализация............................ 251
5.1.2. Масштабирование и нормализация: в чем разница? ...............................252
5.1.3. Минимаксное масштабирование (нормализация)....................................253
5.1.4. Стандартизация (z-нормализация) ............................................................256
5.1.5 . Когда использовать минимаксное масштабирование, а когда
стандартизацию .................................................................................................... 259
5.1.6 . Робастное масштабирование, устойчивое к выбросам ............................. 260
5.1.7. Винсоризация ............................................................................................... 264
5.2. Логарифм, квадратный корень и другие нелинейные преобразования
признаков................................................................................................................... 267
5.2.1 . Логарифмическое преобразование ............................................................ 268
5.2.2. Преобразование квадратного корня .......................................................... 270
5.2 .3. Преобразование кубического корня........................................................... 273
5.2 .4. Преобразования Бокса−Кокса и Йео−Джонсона ........................................ 276
5.3. Практические упражнения ................................................................................. 282
5.4. Возможные проблемы ........................................................................................ 286
5.4 .1. Неправильный выбор метода преобразования ......................................... 286
5.4.2. Неправильное масштабирование тестовых данных ................................. 287
5.4.3. Излишнее преобразование признаков....................................................... 288
5.4.4. Неправильная интерпретация результатов логарифмического
преобразования ..................................................................................................... 288
5.4.5. Игнорирование природы нелинейных зависимостей ..............................289
5.4.6. Неправильное обращение с выбросами..................................................... 289
Заключение ................................................................................................................ 290
Глава 6. Кодирование категориальных переменных.............................. 291
6.1. Кодирование с одним активным состоянием: углубленное изучение............ 291
6.1 .1 . Совет 1: избегайте ловушки, связанной с фиктивными переменными .... 294
6.1.2. Совет 2: правильно кодируйте значения в столбцах с высокой
кардинальностью .................................................................................................. 296
6.1.3. Совет 3: используйте разреженные матрицы для повышения
эффективности ...................................................................................................... 305
6.1.4. Выводы и рекомендации............................................................................. 308
6.2. Более сложные примеры применения кодирования на основе целевой
переменной, частоты и порядкового кодирования ................................................ 309
6.2.1. Кодирование на основе целевой переменной с регуляризацией и без ... 310
6.2.2. Пример использования кодирования на основе частоты......................... 315
6.2.3. Порядковое кодирование ............................................................................ 318
6.2.4. Выводы и рекомендации............................................................................. 322
10 Содержание
6.3. Практические упражнения ................................................................................. 323
6.4. Возможные проблемы ........................................................................................ 326
6.4.1. Переобучение при использовании кодирования на основе целевой
переменной ........................................................................................................... 327
6.4.2. Неправильное использование порядкового кодирования ....................... 327
6.4.3. Использование кодирования с одним активным состоянием
для столбцов с высокой кардинальностью .......................................................... 328
6.4.4. Пренебрежение разреженностью матрицы при кодировании
с одним активным состоянием ............................................................................ 328
6.4.5. Утечка информации при использовании кодирования на основе
целевой переменной .............................................................................................329
6.4.6. Ошибочная интерпретация результатов кодирования на основе
частоты ................................................................................................................... 329
Заключение ................................................................................................................ 330
Глава 7. Конструирование признаков и переменных
взаимодействия .................................................................................................... 331
7.1. Создание признаков на основе существующих переменных .......................... 331
7.1.1. Математические преобразования переменных ........................................ 331
7.1.2. Извлечение компонентов из дат................................................................. 337
7.1.3. Комбинирование признаков ....................................................................... 340
7.2. Переменные взаимодействия и значимость признаков для моделей ............ 342
7.2.1. Полиномиальные признаки ........................................................................ 343
7.2.2. Перекрестные признаки .............................................................................. 345
7.2.3. Переменные взаимодействия и нелинейные зависимости ...................... 351
7.2.4. Комбинирование полиномиальных и перекрестных признаков ............. 354
7.3. Практические упражнения ................................................................................. 358
7.4. Возможные проблемы......................................................................................... 362
7.4.1. Переобучение модели при использовании избыточного количества
признаков .............................................................................................................. 362
7.4.2. Возникновение мультиколлинеарности..................................................... 362
7.4.3. Добавление в модель избыточных признаков ........................................... 363
7.4.4 . Ошибки при интерпретации перекрестных признаков............................ 364
7.4.5 . Проблемы с производительностью при использовании
полиномиальных признаков в больших наборах данных.................................. 364
Заключение ................................................................................................................ 365
Контрольный опрос. Часть II. Конструирование признаков
для сложных моделей........................................................................................ 366
Часть III. Очистка и предварительная обработка данных ..................... 371
Проект 2. Прогнозирование временных рядов
с конструированием признаков ...................................................................... 372
Введение в прогнозирование временных рядов с использованием
конструирования признаков .................................................................................... 373
Содержание 11
Признаки на основе временного лага в прогнозировании временных
рядов ...................................................................................................................... 373
Признаки на основе скользящего окна для обнаружения трендов
и сезонности .............................................................................................................. 377
Циклические признаки на основе гармонических функций ................................. 381
Часовые пояса и пропущенные значения во временных рядах ............................ 382
Детрендирование и работа с сезонностью во временных рядах ........................... 386
Что такое детрендирование? ................................................................................ 386
Методы детрендирования временных рядов...................................................... 387
Работа с сезонностью во временных рядах ......................................................... 394
Как детрендирование и выделение сезонности влияют на качество
моделей ..................................................................................................................397
Применение методов из семейства ARIMA и алгоритмов машинного
обучения для прогнозирования временных рядов................................................. 397
Шаг 1. Подготовка данных для алгоритма машинного обучения ...................... 398
Шаг 2. Применение методов прогнозирования временных рядов ................... 403
Шаг 2(б). Применение методов машинного обучения
для прогнозирования временных рядов ............................................................. 424
Подбор гиперпараметров для методов машинного обучения ............................... 434
Что такое гиперпараметры? .................................................................................435
Использование поиска по сетке для подбора гиперпараметров ....................... 436
Использование случайного поиска для подбора гиперпараметров .................. 438
Итоги проекта ............................................................................................................ 440
Особенности развертывания моделей прогнозирования временных
рядов........................................................................................................................... 441
Практические упражнения ....................................................................................... 442
Возможные проблемы ............................................................................................... 444
Утечка информации в результате неправильно созданных признаков............ 444
Неправильно выбранный размер окна при создании скользящих
признаков .............................................................................................................. 445
Пропуски, возникающие в результате создания новых признаков .................. 445
Неправильная интерпретация циклических переменных ................................ 446
Разреженность данных при создании скользящих признаков .......................... 446
Неправильный учет часовых поясов в данных ................................................... 447
Глава 8. Корректировка аномалий в данных при помощи Pandas .... 448
8.1. Обработка некорректных форматов данных .................................................... 449
8.2. Поиск и удаление дубликатов ............................................................................ 455
8.3. Исправление неконсистентных категориальных данных ............................... 459
8.4. Обработка значений, выходящих за допустимые границы ............................. 463
8.5. Обработка пропущенных значений, образовавшихся в результате
коррекции аномалий................................................................................................. 467
8.6. Практические упражнения ................................................................................. 469
8.7. Возможные проблемы......................................................................................... 473
8.7.1. Удаление важных наблюдений вместе с выбросами ................................. 473
8.7.2. Чрезмерная стандартизация категориальных данных ............................. 474
8.7.3. Ошибочное интерпретирование дубликатов ............................................. 474
12 Содержание
8.7.4. Ошибочное удаление значений, выходящих за границы диапазона....... 475
8.7.5. Ошибки, появляющиеся в результате автоматической
стандартизации ..................................................................................................... 475
8.7.6. Ошибки в результате подстановки пропущенных значений ................... 476
Заключение ................................................................................................................ 476
Глава 9. Методы снижения размерности ..................................................... 477
9.1. Анализ главных компонент (PCA)...................................................................... 477
9.1.1. Суть анализа главных компонент ............................................................... 479
9.1.2. Реализация анализа главных компонент при помощи Scikit-learn ......... 483
9.1.3. Объясненная дисперсия и анализ главных компонент ............................ 487
9.1.4. Когда стоит применять анализ главных компонент ................................. 491
9.1.5. Ключевые выводы об анализе главных компонент .................................. 492
9.2. Техники отбора признаков ................................................................................. 493
9.2.1. Методы фильтрации .................................................................................... 494
9.2.2. Оберточные методы .................................................................................... 501
9.2.3. Встроенные методы ..................................................................................... 506
9.2.4. Ключевые выводы о техниках отбора признаков...................................... 511
9.3. Практические упражнения ................................................................................. 512
9.4. Возможные проблемы ........................................................................................ 516
9.4.1. Удаление слишком большого количества признаков................................ 516
9.4.2. Опасности использования только методов фильтрации .......................... 516
9.4.3 . Утечка информации при использовании оберточных методов ............... 517
9.4.4 . Чрезмерные штрафы при использовании встроенных методов ............. 517
9.4.5 . Ошибки при интерпретации главных компонент .................................... 518
9.4.6 . Избыточность данных при использовании техник отбора признаков .... 519
Заключение ................................................................................................................ 519
Контрольный опрос. Часть III. Очистка и предобработка данных ..... 520
Заключение ............................................................................................................ 523
Предметный указатель....................................................................................... 524
От издательства
Отзывы и пожелания
Мы всегда рады отзывам наших читателей. Расскажите нам, что вы думаете
об этой книге – что понравилось или, может быть, не понравилось. Отзывы
важны для нас, чтобы выпускать книги, которые будут для вас максимально
полезны.
Вы можете написать отзыв на нашем сайте www.dmkpress.com, зайдя на
страницу книги и оставив комментарий в разделе «Отзывы и рецензии».
Также можно послать письмо главному редактору по адресу dmkpress@gmail.
com; при этом укажите название книги в теме письма.
Если вы являетесь экспертом в какой-либо области и заинтересованы в на-
писании новой книги, заполните форму на нашем сайте по адресу http://
dmkpress.com/authors/publish_book/ или напишите в издательство по адресу
dmkpress@gmail.com.
Список опечаток
Хотя мы приняли все возможные меры для того, чтобы обеспечить высо-
кое качество наших текстов, ошибки все равно случаются. Если вы найдете
ошибку в одной из наших книг, мы будем очень благодарны, если вы сооб-
щите о ней главному редактору по адресу dmkpress@gmail.com. Сделав это,
вы избавите других читателей от недопонимания и поможете нам улучшить
последующие издания этой книги.
Нарушение авторских прав
Пиратство в интернете по-прежнему остается насущной проблемой. Издатель-
ство «ДМК Пресс» очень серьезно относится к вопросам защиты авторских прав
и лицензирования. Если вы столкнетесь в интернете с незаконной публикацией
какой-либо из наших книг, пожалуйста, пришлите нам ссылку на интернет-ре-
сурс, чтобы мы могли применить санкции.
Ссылку на подозрительные материалы можно прислать по адресу элект-
ронной почты dmkpress@gmail.com.
Мы высоко ценим любую помощь по защите наших авторов, благодаря
которой мы можем предоставлять вам качественные материалы.
Кто мы?
Вы держите в руках книгу, ставшую плодом совместных усилий разработ-
чиков из компании Cuantum Technologies. Мы – команда людей, предан-
ных идее создания программного обеспечения, наделенного творческим
началом и помогающего решать реальные задачи. Мы стремимся создавать
высококачественные и дружественные веб-приложения, отвечающие всем
требованиям наших заказчиков.
Мы в нашей команде верим, что программирование не ограничивается
исключительно написанием кода. Задача программирования состоит в ре-
шении насущных проблем и облегчении жизни людей. С завидным посто-
янством исследуем новые технологии и науки, чтобы оставаться в авангар-
де индустрии, и с удовольствием делимся накопленными знаниями с вами,
наши читатели.
Наш подход к созданию приложений базируется на совместной работе
и творческом начале. Мы работаем в тесном контакте с заказчиками, что-
бы досконально понять их нужды и создать продукт, отвечающий всем их
требованиям. Верим в то, что выходящие из-под пера программиста при-
ложения должны быть интуитивно понятными, легкими в обращении и при-
влекательными визуально, и делаем все от нас зависящее для воплощения
этой веры.
В этой книге мы постарались изложить практические рекомендации по
подготовке данных для их интеллектуального анализа. Делаете ли вы свои
первые шаги в программировании или уже имеете определенный опыт
в написании приложений, эта книга поможет вам освоить приемы работы
с данными, которые позволят вам в будущем погрузиться в мир машинного
и глубокого обучения.
Наша философия
В Cuantum Technologies мы свято верим, что учиться программировать и раз-
вивать свои навыки в написании приложений можно на протяжении всей
жизни. В связи с этим мы всячески поощряем стремление наших разра-
ботчиков осваивать новые технологии и техники и обеспечиваем их всеми
необходимыми инструментами, для того чтобы не терять позиций в быстро
развивающейся отрасли. Кроме того, мы полагаем, что программирование
должно приносить радость и удовлетворение, и стараемся создавать рабочее
окружение, поощряющее творческий подход и внедрение инноваций.
Ктомы? 15
Наш опыт
Компания Cuantum Technologies специализируется на создании веб-
приложений, помогающих решать заказчикам их насущные проблемы твор-
чески и эффективно. Наши разработчики обладают богатым опытом напи-
сания приложений на разных языках программирования и использования
различных фреймворков и технологий, включая Python, AI, ChatGPT, Django,
React, Three.js, Vue.js и пр. Мы постоянно осваиваем новые технологии и ста-
раемся реализовывать на практике все пожелания клиентов.
Также наши разработчики имеют все необходимые навыки в области ана-
лиза данных, визуализации, машинного обучения и искусственного интел-
лекта. Мы убеждены, что именно за этими технологиями будущее индустрии,
и делаем все возможное, чтобы оставаться на переднем крае этой революции.
О переводчике
Александр Гинько, обладающий богатым опытом
работы в сфере ИТ и более десяти лет посвятивший
переводам книг и статей на самые разные темы,
в последние годы специализируется на переводе
книг в области бизнес-аналитики и программиро-
вания для издательства «ДМК Пресс» по направле-
ниям Python, SQL, Power BI, DAX, Excel, Power Query,
Tableau, R... На данный момент в активе Александра
уже более 25 книг, включая одну авторскую, и он про-
должает плодотворно работать над переводом и на-
писанием новых книг.
Возможно, вам также будут интересны книги «Сверхбыстрый Python»
(https://dmkpress.com/catalog/computer/programming/python/978-5 -93700-
226-6), «Python: практическое руководство по Pandas (200 упражнений)»
(https://dmkpress.com/catalog/computer/programming/python/978-5 -93700-
227-3) и «Введение в статистическое обучение с примерами на Python»
(https://dmkpress.com/catalog/computer/statistics/978-5-93700-217-4) в пере-
воде Александра.
Помимо перевода книг, Александр ведет свой канал в Telegram (https://t.me/
alexanderginko_books), на котором вы можете из первых уст получить ответы
на все интересующие вас вопросы об уже переведенных книгах, находящих-
ся в работе и запланированных на будущее. Также на канале можно найти
промокоды на все книги Александра для покупки книг на сайте издательства
«ДМК Пресс» с большими скидками. Купить книги Александра и следить за
переводом новых книг в режиме реального времени можно также с помощью
его бота в Telegram по адресу https://t.me/alexanderginko_books_bot.
Введение
Данные – это один из наиболее ценных активов в современном цифровом
мире, и они лежат в основе всего: от принятия бизнес-решений до осущест-
вления технического прогресса. В то же время сырые необработанные дан-
ные зачастую содержатся в разрозненном, беспорядочном и неструктури-
рованном виде. Истинная ценность данных кроется в их сути, добраться до
которой можно только путем их преобразований. Но для выполнения этих
преобразований недостаточно просто знать нужные алгоритмы. Нужно хоро-
шо понимать приемы и способы эффективной очистки, подготовки и мани-
пулирования данными. В этой книге мы поговорим о ключевых концепци-
ях и техниках, связанных с анализом данных, а также с конструированием
и отбором признаков, лежащих в основе машинного и глубокого обучения.
Цель этой книги – научить вас подготавливать и преобразовывать сырые
данные, конструировать новые признаки и в целом придавать исходным
данным форму, пригодную для будущего интеллектуального анализа при
помощи методов машинного и глубокого обучения. Работаете ли вы с не-
большими объемами данных или оперируете сложными наборами данных
высокой размерности, эта книга познакомит вас с эффективными техниками
и приемами предварительной обработки и подготовки данных к дальнейше-
му анализу. По большей части мы будем пользоваться богатыми средствами
и инструментами наиболее популярных библиотек Python для работы с дан-
ными, таких как Pandas, NumPy и Scikit-learn.
Почему так важно подготавливать данные
и заниматься построением признаков?
В машинном обучении часто можно услышать фразу «Данные – наше всё».
Хотя выбор модели и тщательная настройка используемых алгоритмов
играют важную роль, качество исходных данных оказывает гораздо боль-
шее влияние на эффективность итоговой модели. Этап подготовки данных
и конструирования признаков зачастую занимает наиболее продолжитель-
ное время в проекте, и на то есть веские причины. Искусно подготовленные
данные позволяют модели выявлять значимые шаблоны и делать точные
предсказания, а также улучшают ее обобщающую способность при приме-
нении к новым данным.
Конструирование признаков (feature engineering) относится к области соз-
дания новых информативных признаков на основе сырых данных и играет
важнейшую роль в процессе интеллектуального анализа данных. Именно
признакам (feature) – преобразованным, комбинированным или созданным
18 Введение
на основе существующих данных – модели обязаны своим высоким предска-
зательным потенциалом. В процессе чтения книги вы увидите, что хорошо
сконструированные признаки способны определять зависимости и шаблоны,
которые невозможно выявить на основе одних только исходных данных.
Такие тщательно отобранные признаки лежат в основе моделей, характери-
зующихся высоким качеством предсказаний, устойчивостью и интерпрети-
руемостью результатов.
Инструменты, которые мы будем использовать
В последнее время практическим стандартом для специалистов по работе
с данными стал язык программирования Python, и в этой книге мы будем
в основном использовать три наиболее популярные библиотеки этого языка
для преобразования данных: Pandas, NumPy и Scikit-learn.
Pandas – мощная библиотека для анализа и манипулирования данными.
Pandas представляет собой интуитивно понятный фреймворк для управле-
ния исходными данными, представленными в виде строк и столбцов. Он
особенно полезен в задачах первичной обработки данных, таких как очистка,
фильтрация, агрегация и объединение наборов данных. Pandas поможет вам
значительно облегчить процедуру обработки сырых данных и извлечения из
них важных сведений.
NumPy – высокоэффективная библиотека для быстрой работы с массивами
и применения математических операций к большим наборам данных. Пре-
имущества NumPy в работе с данными делают эту библиотеку незаменимой
при выполнении ресурсоемких операций с большими массивами информа-
ции, таких как масштабирование, нормализация и математические преоб-
разования.
Scikit-learn – будучи одной из наиболее популярных библиотек в области
машинного обучения, Scikit-learn не ограничивается одними лишь инстру-
ментами для построения моделей, но также предлагает богатый арсенал
средств для манипулирования данными. В модулях фреймворка, связанных
с предварительной обработкой данных, присутствуют средства для кодиро-
вания категориальных переменных, масштабирования непрерывных пере-
менных, создания конвейеров обработки данных и многого другого. Библио-
тека Scikit-learn играет ключевую роль на этапе предварительной обработки
данных, обеспечивая вашему анализу согласованность и воспроизводство.
В совокупности перечисленные библиотеки дают вам полный набор
средств для управления процессом подготовки данных и конструирования
признаков, включая очистку и преобразование данных, а также отбор и ко-
дирование переменных.
Чему вы научитесь
Главы этой книги разбиты на три части, каждая из которых посвящена от-
дельному этапу подготовки данных и конструирования признаков.
Введение 19
Часть I. Подготовка к расширенному анализу данных. В первой части
книги мы познакомимся с базовыми принципами промежуточного анали-
за данных и закроем пробелы в знаниях, касающихся обработки данных
средствами Python. Мы научимся подходить к обработке данных системно
и обеспечивать полную пригодность и структурированность данных перед
их дальнейшим анализом. В процессе мы пройдемся по основным возмож-
ностям библиотек Pandas и NumPy и научимся выполнять требуемые опера-
ции над данными максимально эффективно. Таким образом, в этой части
мы заложим основы для знакомства с более сложными техниками обработки
данных, о которых будем говорить далее.
Часть II. Конструирование и отбор признаков для повышения качест-
ва моделей. Во второй части книги мы с головой погрузимся в область
конструирования и отбора признаков. Мы познакомимся с эффективны-
ми способами управления пропущенными значениями, масштабирования
и преобразования признаков, кодирования категориальных переменных
и создания новых признаков. Область конструирования признаков требует
творческого подхода и глубокого понимания поставленной задачи, и в этой
части книги мы научимся выстраивать мыслительный процесс и исполь-
зовать наиболее подходящие техники для повышения предсказательной
способности будущей модели. Мы обсудим множество полезных методов
для создания полиномиальных признаков, комбинирования существующих
переменных с целью отслеживания эффектов их взаимодействий и коди-
рования категориальных переменных с использованием разных стратегий.
Завершив чтение этой части книги, вы будете вооружены полным спектром
техник и приемов для конструирования и отбора признаков, которые смо-
жете полноценно использовать в собственных проектах.
Часть III. Очистка и предварительная обработка данных. В заключи-
тельной части книги мы сосредоточимся на критически важных задачах,
связанных с очисткой данных и их предварительной обработкой. Здесь вы
узнаете о передовых техниках обработки выбросов в данных, корректировки
аномалий и подготовки данных для анализа временных рядов. Мы также
обсудим способы снижения размерности данных, такие как метод главных
компонент, незаменимые при работе с многомерными данными. Вы узнаете,
как можно уменьшить сложность пространства признаков без существенного
вреда для качества модели. Это позволит повысить эффективность итоговой
модели и ее интерпретируемость. Освоив эти техники предварительной об-
работки данных, вы сможете строить хорошо структурированные наборы
данных, что существенно повысит качество создаваемых моделей.
Практические примеры и реальные задачи
Каждая глава книги сопровождается примерами и упражнениями, которые
позволят вам проверить полученные знания на практике в самых разных об-
ластях – от финансов до здравоохранения и розничных продаж. Столь разные
примеры помогут вам понять, какие преобразования, способы кодирования
20 Введение
и масштабирования могут успешно применяться в тех или иных областях.
Кроме того, эти примеры помогут вам критически взглянуть на конструи-
рование признаков и научиться применять только нужные преобразования
в зависимости от набора данных и итоговой модели.
Также в конце каждой главы вы встретите раздел с названием «Что может
пойти не так?», в котором мы будем обсуждать распространенные ловушки
и трудности, поджидающие вас в процессе конструирования и отбора при-
знаков. Эти разделы призваны помочь вам развить критическое мышление
и всегда находиться на шаг впереди, предвосхищая возможные трудности.
Важность воспроизводимости
В науке о данных воспроизводимость является одним их ключевых аспектов
в построении надежных и качественных моделей. В книге мы часто будем
обращаться к теме воспроизводимости, в частности при обсуждении исполь-
зования конвейеров в Scikit-learn. Автоматизация шагов по преобразованию
данных внутри конвейеров позволит вам применять одни и те же действия
к обучающему и тестовому наборам данных и тем самым избежать утечки
информации и повысить надежность результатов.
Заключение
Книга, которую вы держите в руках, является подробным руководством по
освоению ключевых навыков, необходимых для подготовки данных и кон-
струирования и отбора признаков. Эти аспекты составляют основу любого
успешного проекта с применением машинного и глубокого обучения, и у вас
есть прекрасная возможность овладеть ими в полной мере.
Делаете ли вы свои первые шаги в освоении науки о данных или являетесь
практикующим специалистом в этой области, желающим улучшить свои на-
выки, эта книга поможет вам обрести уверенность при работе с большими
объемами данных.
Что ж, давайте вместе отправимся в это увлекательное путешествие и на-
учимся из сырых данных готовить восхитительные полуфабрикаты для по-
строения успешных моделей машинного обучения!
ЧАСТЬ I
Подготовка
к расширенному
анализу данных
Глава 1
Введение.
Основы и не только
А вот и ваша первая остановка в путешествии по миру анализа данных. Здесь
мы быстро пройдемся по основным концепциям, которые вы должны легко
усвоить, если обладаете базой, накопленной на курсах начального уровня
по Python. Прочитав эту главу, вы познакомитесь со всеми основными ин-
струментами для работы и будете готовы к погружению в более сложный
материал, связанный с подготовкой данных и конструированием и отбором
признаков.
В процессе изучения этого материала важно понять, что промежуточный
анализ данных не ограничивается одним лишь расширением вашего тех-
нического арсенала, а требует также досконального понимания того, когда,
как и каким инструментом нужно воспользоваться для извлечения ценных
сведений из разрозненных исходных данных. Вы научитесь оптимизировать
аналитические рабочие процессы, конструировать полезные признаки и, что
более важно, применять передовые техники и приемы для построения на-
дежных предсказательных моделей, способных эффективно решать сложные
задачи из реального мира.
Этот промежуточный шаг поможет вам перейти от использования прос-
тых техник для манипулирования данными к изощренным конвейерам для
обнаружения глубоко запрятанных шаблонов, от базовых визуализаций
к сложным многомерным представлениям данных и от применения эле-
ментарных статистических тестов к использованию продвинутых алгорит-
мов машинного обучения. Вы научитесь выявлять скрытые зависимости
и тенденции в данных, строить более точные прогнозы и делать аналитиче-
ские выводы, на основе которых вы и ваши заказчики сможете принимать
важные бизнес-решения в самых разных областях – от финансов до здраво-
охранения.
Введение в промежуточный анализ данных 23
1.1. Введение в промежуточный анализ
данных
Промежуточный анализ данных представляет собой важную переходную
фазу в аналитике, позволяющую преодолеть разрыв между применением ба-
зовых фундаментальных операций и использованием передовых аналитиче-
ских техник. Эта фаза знаменует следующий шаг после выполнения простых
действий над данными, включающих их загрузку, элементарные преобразо-
вания и визуализацию. Промежуточный анализ подразумевает использова-
ние более сложных и изощренных техник для исследования и интерпретации
данных. Он включает в себя широкий спектр методик, позволяющих:
1. Глубже погрузиться в данные
Техники, относящиеся к промежуточному анализу данных, позволяют более
тщательно исследовать данные и выявлять скрытые шаблоны и зависимости,
недоступные при использовании базовых операций над исходными данны-
ми. Это погружение включает в себя использование передовых статистиче-
ских методов, алгоритмов машинного обучения и техник визуализации.
К примеру, аналитик может воспользоваться методами кластеризации для
обнаружения естественных групп в исходных данных, применить техники
снижения размерности вроде анализа главных компонент с целью выяв-
ления скрытой структуры данных или использовать анализ ассоциативных
правил для поиска интересных зависимостей между переменными. Эти про-
двинутые методы позволяют аналитику извлечь гораздо больше глубоко за-
прятанной в данных ценной информации, что способствует принятию более
осмысленных решений на ее основе.
Кроме того, промежуточный анализ часто включает использование техник
конструирования и отбора признаков для создания более информативных
переменных на основе существующих, что также дает возможность обнару-
жить скрытые шаблоны и взаимодействия.
2. Оптимизировать производительность
При работе с объемными наборами данных этап промежуточного анализа
должен отвечать всем требованиям эффективной обработки данных для повы-
шения общей скорости выполнения операций. Ключевую роль здесь играют:
векторизация, подразумевающая применение векторизованных опе-
раций, свойственных NumPy и Pandas, для выполнения действий при-
менительно к целым столбцам или массивам одновременно вместо
использования традиционных медленных циклов;
управление памятью, заключающееся в использовании оптималь-
ных типов данных, файлов, отображаемых в память, и вычислений
с использованием внешней памяти для обработки наборов данных, не
помещающихся в оперативную память;
24 Введение. Основы и не только
параллельные вычисления, выражающиеся в использовании много-
ядерных процессоров или распределенных вычислительных систем
с целью ускорения обработки больших наборов данных;
эффективные алгоритмы, способные масштабироваться примени-
тельно к растущим объемам данных. В качестве примера можно при-
вести использование приблизительных методов вычислений опреде-
ленных статистических показателей.
С использованием всех перечисленных выше техник и методов анали-
тик может гораздо эффективнее обрабатывать огромные массивы данных
с применением сложных аналитических инструментов и опробовать разные
модели. Это позволит ему не только повысить производительность анали-
тических механизмов, но и проверить куда больше статистических гипотез,
а также использовать в работе потоки данных в реальном или почти реаль-
ном времени.
3. Обрабатывать сложные наборы данных
Промежуточный анализ данных предполагает работу с достаточно большими
и сложными наборами данных, которые могут включать множество предик-
торов, различные типы данных и сложные зависимости между наблюдения-
ми. На передний план при работе с такими данными выходят следующие
аспекты:
интеграция данных: аналитик может и часто объединяет данные для
всеобъемлющего анализа из массы источников, таких как базы данных,
API и плоские файлы;
обработка неструктурированных данных: аналитикам регулярно
приходится взаимодействовать с так называемыми неструктурирован-
ными данными, такими как текст, изображения или аудио, и зачастую
для их анализа они используют обработку естественного языка и тех-
ники компьютерного зрения;
анализ временных рядов: промежуточный анализ включает в себя
работу с данными, зависящими от времени, с применением таких тех-
ник, как сезонная декомпозиция, анализ трендов и прогнозирование;
многомерный анализ: часто аналитику бывает необходимо иссле-
довать переменные на наличие зависимостей между ними, для чего
используются корреляционный и факторный анализ, а также метод
главных компонент.
Досконально освоив все перечисленные выше методики, аналитик сможет
извлекать из исходных данных скрытые ценные сведения, которые позволят
делать более точные предсказания и принимать на их основе осмысленные
решения.
4. Использовать передовые статистические методы
Этап промежуточного анализа включает в себя применение сложных статис-
тических методов и алгоритмов машинного обучения, позволяющих повы-
Введение в промежуточный анализ данных 25
сить точность предсказаний. На этой стадии аналитик обычно использует
следующие методы:
регрессионный анализ: речь здесь может идти не только об обыч-
ной линейной регрессии, но также о множественной, логистической
и полиномиальной, позволяющих исследовать скрытые зависимости
между переменными;
анализ временных рядов: сюда включается использование модели
ARIMA (интегрированная модель авторегрессии скользящего средне-
го), метода экспоненциального сглаживания и сезонной декомпози-
ции для определения тенденций и шаблонов в данных, зависящих от
времени;
байесовская статистика: применение байесовского вывода для об-
новления вероятностей при появлении новой информации, что часто
используется в области A/B-тестирования и анализа рисков;
алгоритмы машинного обучения: использование методов обучения
с учителем (деревья решений, случайные леса, метод опорных векто-
ров и т. д .) и без учителя (кластеризация методом k-средних, иерар-
хическая кластеризация и т. д .) для выявления шаблонов в данных
и выполнения предсказаний.
Перечисленные и подобные им техники и приемы позволяют аналитику
извлекать неочевидную на первый взгляд информацию из данных в виде
любопытных зависимостей между переменными и строить качественные
и устойчивые прогностические модели. Освоив эти методы, аналитик сможет
при помощи моделей решать более специфические задачи в самых разных
областях.
5. Улучшить способы визуализации данных
Промежуточный анализ выводит область визуализации данных на новый
качественный уровень. Традиционные графики и диаграммы остаются
позади, а впереди – изысканные визуализации, способные эффективно
отображать многомерные данные и сложные зависимости внутри них. Не-
которые виды и техники визуализации, используемые на этом этапе, пере-
числены ниже:
интерактивные дашборды: с помощью фреймворков Plotly или Bokeh
вы можете создавать очень мощные, динамичные и интерактивные
дашборды, позволяющие анализировать данные в реальном времени;
сетевые диаграммы: визуализация сложных зависимостей между на-
блюдениями бывает очень удобна при анализе социальных сетей или
поиске взаимосвязей в больших наборах данных;
геопространственные визуализации: относятся к включению гео-
графических данных для формирования информативных карт, позво-
ляющих определять пространственные шаблоны и тенденции;
трехмерные визуализации: представление трехмерных структур
данных или использование трехмерных техник для добавления до-
26 Введение. Основы и не только
полнительных слоев информации на традиционных двумерных диа-
граммах.
Перечисленные техники позволяют не только сделать данные визуально
более привлекательными, но и помогают аналитикам обнаружить в них шаб-
лоны, выбросы и иные важные особенности, незаметные при первичном
анализе. Освоив продвинутые визуальные методы, аналитик сможет доно-
сить свои изыскания в понятном виде как до технических специалистов,
так и до рядовой аудитории, что позволит повысить качество принимаемых
решений.
1.1.1. Ключевые концепции промежуточного
анализа данных
Для эффективного выполнения промежуточного анализа данных аналитик
должен уверенно владеть всеми передовыми методами работы с данными.
Ниже мы перечислим и немного углубимся в техники, с которыми должен
быть знаком аналитик и которые вы освоите, прочитав эту книгу.
Манипулирование данными с помощью Pandas
Pandas является одной из наиболее популярных библиотек языка Python для
работы с табличными данными, и вы должны в совершенстве владеть всеми
имеющимися в ней техниками и инструментами, такими как:
изменение формы сложных табличных данных с помощью функций
создания и разворачивания сводных таблиц. Эти функции позволяют
кардинальным образом изменять структуру данных для их анализа
и визуализации. Посредством сводных таблиц можно агрегировать
данные по разным измерениям, а функция melt() позволит преобра-
зовать данные из широкого формата в длинный, что бывает удобно для
выполнения дальнейшего их анализа;
применение функций группировки и работа с объектами GroupBy. Опе-
рации группировки позволяют разбивать исходные данные на группы
по некоторым критериям, применять преобразования к каждой груп-
пе по отдельности и объединять полученные результаты. Это бывает
очень полезно при выполнении сложных вычислений применительно
к подмножествам данных;
управление данными временных рядов путем их передискретиза-
ции и вычисления скользящих показателей. Данные, представленные
в виде временных рядов, зачастую требуют изменения периодичности
их следования и выполнения вычислений, применяющихся не к кон-
кретным наблюдениям, а к так называемым плавающим окнам. Эти
техники бывают очень полезны для выявления тенденций, связанных
с сезонностью, и других шаблонов на основе времени;
Введение в промежуточный анализ данных 27
объединение и добавление наборов данных с применением разных ме-
тодов и параметров. Поскольку разрозненные данные часто поступают
из различных источников, важно уметь правильно задавать много-
численные критерии для их корректного объединения. Сюда вклю-
чаются как разные типы объединения (внутреннее, внешнее, левое,
правое и т. д .), так и способы обращения с дублирующимися значения-
ми в ключах или несовпадающими именами столбцов.
Помимо этого, вы должны умело обращаться со следующими механизма-
ми, представленными в библиотеке Pandas:
множественные индексы и передовые техники индексации, позволя-
ющие эффективно работать с многомерными данными;
категориальный тип данных, с помощью которого можно существенно
повысить эффективность использования памяти при работе со столб-
цами с ограниченным количеством уникальных значений;
строковые методы и функции для работы с текстом, включая регуляр-
ные выражения.
Числовые вычисления с помощью NumPy
NumPy представляет собой библиотеку для эффективной работы с числовы-
ми и многомерными данными. Эта библиотека предлагает богатый набор
инструментов для обработки объемных матриц и многомерных массивов
и применения к ним различных математических функций и преобразова-
ний. Ниже перечислены ключевые особенности библиотеки NumPy:
использование техники транслирования (broadcasting) для выполнения
операций с массивами разной формы. Транслирование представляет
собой мощнейший механизм, позволяющий автоматически изменять
размеры массивов таким образом, чтобы они совпадали, что дает воз-
можность применять математические операторы к разным массивам
(при соблюдении определенных условий) без необходимости вручную
воспроизводить недостающие значения при помощи дублирования.
Это бывает очень полезно при работе с наборами данных разной раз-
мерности или при применении скалярных операций к целым массивам;
использование богатых возможностей индексации для выбора нужных
данных из массивов. NumPy предлагает много дополнительных спо-
собов индексации данных, помимо традиционных срезов. К примеру,
булева индексация позволяет отбирать значения по условию, а с по-
мощью так называемой причудливой индексации можно достучать-
ся до нужных элементов посредством целочисленных массивов. Эти
техники очень активно используются для фильтрации и манипулиро-
вания большими наборами данных, особенно при наличии большого
количества сложных критериев отбора;
применение универсальных функций (ufunc) для выполнения поэле-
ментных операций. Универсальные функции представляют собой век-
28 Введение. Основы и не только
торизованные обертки для скалярных функций, позволяющие опе-
рировать с массивами поэлементно. Эти функции являются хорошо
оптимизированными и демонстрируют очень высокую скорость вы-
числений в сравнении с традиционными циклами Python. Универсаль-
ные функции можно применять к массивам разной формы, к тому же
они поддерживают механизм транслирования, что делает их весьма
универсальными в отношении использования математических опера-
ций применительно к массивам;
использование модуля линейной алгебры, входящего в состав библио-
теки NumPy. В этом модуле (linalg) содержится множество функций,
реализующих операции из линейной алгебры, такие как матричное
и векторное произведение, декомпозиция, нахождение собственных
векторов и значений, решение линейных уравнений и т. д . Эти функ-
ции бывают очень полезны при создании научных и инженерных при-
ложений, а также при реализации алгоритмов машинного обучения,
многие из которых базируются на вычислениях из линейной алгебры.
Кроме того, скорость вычислений и оптимальное использование памяти,
присущие библиотеке NumPy, делают ее незаменимой при работе с больши-
ми объемами данных. Возможность выполнять векторизованные операции
применительно к целым массивам вместо их поэлементной обработки по-
зволяет значительно ускорить процессы вычислений.
Конструирование и отбор признаков
Процедура конструирования и отбора признаков включает в себя создание
новых переменных, способных существенно повысить качество модели. Кон-
струирование признаков является краеугольным камнем промежуточного
анализа данных и позволяет аналитику извлекать гораздо больше ценной
информации из сырых данных и улучшать качество моделей. Некоторые
ключевые аспекты конструирования и отбора признаков:
кодирование категориальных переменных. Под кодированием по-
нимается преобразование нечисловых данных в формат, понятный
алгоритмам машинного обучения. Техники, подобные кодированию
с одним активным состоянием, позволяют создавать колонки с дво-
ичными значениями для категорий, а приемы кодирования на основе
целевой переменной – заменять их на средние значения из целевого
атрибута. С помощью этих методов можно эффективно использовать
информацию, заложенную в номинальных предикторах;
создание признаков взаимодействий. Комбинируя различные пере-
менные, аналитик может отлавливать сложные зависимости, которые
невозможно обнаружить, оперируя только исходными предикторами.
К примеру, если умножить цену на количество, можно получить весь-
ма информативный признак с совокупным доходом. С помощью при-
знаков взаимодействий вы можете обнаружить нелинейные шаблоны
в данных и улучшить качество будущей модели;
Введение в промежуточный анализ данных 29
применение преобразований, характерных для предметной области.
Использование сокровенных знаний о конкретной предметной обла-
сти для конструирования особенных и очень эффективных признаков
является вершиной анализа данных и граничит с творчеством. К при-
меру, в области финансов вы могли бы добавить в модель признаки
вроде отношения заемных средств к собственному капиталу или от-
ношения цены к доходам, которые несут в себе ценную информацию,
недоступную в сырых исходных данных;
реализация автоматической процедуры конструирования признаков.
При увеличении объемов и сложности наборов данных генерировать
новые признаки вручную становится достаточно затратно по време-
ни. Тогда на помощь приходят специальные техники вроде глубокого
синтеза признаков (deep feature synthesis) или генетических алго-
ритмов (genetic algorithm), позволяющие исследовать существую-
щие признаки и создавать на их основе новые. Применение подобных
методов может помочь обнаружить скрытые зависимости в данных
и существенно сэкономить время, затрачиваемое на процесс создания
признаков.
Но процедура конструирования и отбора признаков не ограничивается
одним лишь созданием новых переменных. Она также включает в себя ис-
следование шаблонов в данных и представление их в виде, понятном модели.
Позже, когда вы наберетесь опыта в этой области, вы узнаете, что нередко
хорошую модель от потрясающей отделяет именно грамотное конструиро-
вание признаков.
Эффективная работа с данными
При росте объемов и сложности исходных данных на первый план в отно-
шении эффективности и быстродействия выходит оптимизация рабочих
процессов.
Ниже приведены некоторые важные аспекты управления большими объ-
емами данных:
использование оптимальных с точки зрения экономии памяти типов
и структур данных. К примеру, вы всегда должны выбирать мини-
мально допустимые для конкретной задачи типы данных (допустим,
int8 вместо int64 для хранения небольших целочисленных значений)
и использовать структуры вроде разреженных матриц для хранения
наборов данных с большим количеством нулевых значений. Это по-
может существенно снизить объем используемой памяти и ускорить
вычисления;
реализация вычислений с использованием внешней памяти для об-
работки наборов данных, не помещающихся в оперативную память.
При работе с большими данными вы можете воспользоваться техникой
разбиения на блоки или файлами, отображаемыми в память, для об-
работки данных небольшими фрагментами. А библиотеки наподобие
30 Введение. Основы и не только
Dask или Vaex помогут вам организовать распределенные вычисления
применительно к объемным наборам данных;
использование техник параллельного вычисления для ускорения про-
цесса обработки данных. Сюда включается задействование много-
ядерных процессоров и/или систем распределенных вычислений. Это
можно реализовать путем использования библиотеки multiprocessing
в Python и обращения к фреймворкам распределенных вычислений,
таким как Apache Spark, при работе с большими данными;
оптимизация операций ввода-вывода для ускорения процедуры за-
грузки и сохранения данных. Здесь вам могут прийти на помощь фор-
маты эффективного хранения данных, такие как Parquet или HDF5,
оптимизированные для выполнения аналитических операций. Также
вы можете воспользоваться асинхронными операциями ввода-вывода
и техниками буферизации для минимизации влияния медленных опе-
раций, связанных с работой с диском, на ваш аналитический конвейер;
использование техник сжатия данных. Для уменьшения размера ваших
наборов данных вы можете воспользоваться алгоритмами компрессии
как при хранении данных, так и во время их обработки. Это поможет
значительно ускорить операции ввода-вывода и сократить расходы на
хранение;
использование индексов и техник по оптимизации запросов. Если
вашим хранилищем являются реляционные базы данных, вам стоит
задуматься над тем, чтобы должным образом организовать индексы
и оптимизировать запросы, что поможет сократить время, затрачивае-
мое на извлечение и обработку данных.
Приняв во внимание все перечисленные выше аспекты, вы сможете рабо-
тать с большими объемами данных гораздо более эффективно и выполнять
сложные аналитические операции с перебором различных моделей с мень-
шими временными затратами. Это позволит не только повысить произво-
дительность, но также проверить более сложные гипотезы и поработать с по-
токами данных в реальном времени, что откроет вам новые аналитические
возможности.
Использование конвейеров данных
Автоматизация рабочих процессов – ключ к воспроизводимости и эффек-
тивности в аналитике. Конвейеры данных (data pipeline) представляют собой
один из главных компонентов промежуточного анализа данных и позволяют
сделать обработку данных более последовательной и упорядоченной. Ниже
приведены ключевые аспекты конвейеров данных:
разработка модульных шагов обработки данных, доступных для по-
вторного использования. Эта процедура заключается в разбиении
ваших процессов на отдельные автономные модули. Каждый модуль
должен отвечать за свою часть обработки данных, будь то очистка дан-
Введение в промежуточный анализ данных 31
ных, извлечение признаков или их нормализация. Сделав эти модули
повторно используемыми, вы сможете беспрепятственно применять
их в других проектах с другими наборами данных, что позволит вам
сэкономить массу времени и внести единообразие в анализ;
реализация проверки качества данных внутри конвейеров. Качество
данных является первостепенной составляющей любого анализа дан-
ных. Внедрение различных проверок на разных этапах выполнения
конвейера поможет идентифицировать и отловить проблемы на ран-
них стадиях анализа. Проверки могут включать в себя поиск пропущен-
ных значений, определение наличия выбросов, сверку типов данных,
определение того, что значения созданных признаков укладываются
в заданный диапазон, и т. д . Такие автоматические проверки качества
данных позволяют поддерживать целостность ваших данных на про-
тяжении всего процесса анализа;
интеграция процедуры отбора признаков и обучения модели в конвей-
ер. При повышении сложности анализа добавление отбора признаков
и обучения модели непосредственно в конвейер может значительно
упростить рабочий процесс. К примеру, вы можете воспользоваться ал-
горитмом рекурсивного исключения признаков или анализом главных
компонент в качестве метода отбора признаков, следом за которым
будет автоматически запускаться процедура обучения модели с под-
боров гиперпараметров. Это позволит сделать процедуру отбора при-
знаков и обучения модели легко воспроизводимой;
использование объектов конвейеров для упрощения экспериментов
и выполнения перекрестной проверки. Многие библиотеки машинного
обучения, включая Scikit-learn, предлагают свои объекты для рабо-
ты с конвейерами, позволяющие сцеплять вместе шаги по обработке
и анализу данных. Эти конвейеры бывает очень удобно использовать
для экспериментов, поскольку они позволяют легко переключаться
между разными шагами и моделями. Кроме того, с их помощью мож-
но легко использовать технику перекрестной проверки, или кросс-
валидации, для полноценной и эффективной проверки всего рабочего
процесса, начиная с предварительной обработки данных и заканчивая
предсказанием.
Также использование конвейеров данных позволяет облегчить работу над
проектом в команде за счет упрощения процесса развертывания моделей
в рабочих окружениях и осуществления контроля над всеми шагами. В про-
цессе приобретения опыта в анализе данных вы начнете особенно ценить
умение строить эффективные конвейеры данных, позволяющие работать
одновременно с несколькими сложными проектами и задачами.
Освоив в должной мере все перечисленные концепции и приемы про-
межуточного анализа данных, вы сможете без труда работать со сложными
наборами данных. Вы научитесь обрабатывать большие объемы данных, на-
ходить в них скрытые шаблоны и разрабатывать качественные модели. Этот
32 Введение. Основы и не только
набор навыков позволит вам успешно решать задачи из различных областей.
Кроме того, вы сможете эффективно доносить полученные результаты до
заказчиков в виде понятных им выводов.
1.1.2. Пример: промежуточный анализ данных
с помощью Pandas и NumPy
Теперь давайте погрузимся в несложный пример промежуточного анализа
данных на Python с использованием библиотек Pandas и NumPy. В нем мы
будем работать с набором данных, посвященным розничным продажам то-
варов разных категорий в различных магазинах.
Хотя базовый анализ обычно состоит только из элементарных операций
над данными вроде фильтрации и вычисления простых итогов, промежуточ-
ный анализ может добавлять более сложные расчеты.
На этом этапе мы сосредоточимся на некоторых выводах, которые могли
бы заинтересовать нас как аналитиков. К примеру, мы могли бы сформули-
ровать следующие задачи.
1. Выполнить анализ временных рядов. Рассчитать скользящее сред-
нее по продажам для окон разной дискретности с целью обнаружения
тенденций и сезонности. Для этого можно воспользоваться средствами
Pandas и функциями для работы со скользящими окнами.
2. Поработать с пропущенными значениями и неконсистентны-
ми данными. В реальных наборах данных часто содержатся ошибки
и пропуски. Для работы с пропущенными значениями можно было бы
воспользоваться разными техниками подстановки, от простой интер-
поляции на основе существующих данных до алгоритмов машинного
обучения.
3. Оптимизировать хранение и обработку данных. При росте объ-
емов наборов данных, с которыми приходится работать, на первый
план выходит эффективность. Реализовать оптимизацию можно при
помощи использования подходящих типов данных и использования
векторизованных операций посредством библиотеки NumPy.
4. Создать сложные признаки. Мы могли бы попробовать объединить
разные переменные с целью создания новых, более информативных
признаков. К примеру, одним из таких признаков мог бы стать пока-
затель рентабельности продаж, зависящий от прибыли и затраченных
ресурсов.
5. Выполнить операции группировки. При помощи соответствующих
функций библиотеки Pandas мы могли бы проанализировать шаблоны
продаж в разрезе категорий товаров или географии магазинов.
6.
Применить статистические тесты. Можно было бы выполнить про-
верку гипотез и вычислить доверительные интервалы для проверки
сделанных выводов на статистическую значимость.
Введение в промежуточный анализ данных 33
Все эти действия позволят не только провести качественный анализ, но
и заложить основу для применения методов машинного обучения и соз-
дания моделей данных. Таким образом, воспользовавшись приобретенны-
ми навыками, аналитик способен превратить сырые разрозненные данные
в полезную информацию, на основе которой можно принимать взвешенные
решения.
Пример кода: вычисление скользящих средних и обработка
пропущенных значений
Предположим, у нас есть следующий набор данных по розничным продажам:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# Простые данные: ежедневные продажи в розничных магазинах
data={
'Date': pd.date_range(start='2023-01-01', periods=30, freq='D'),
'Sales': [200, 220, np.nan, 250, 260, 240, np.nan, 300, 280, 290,
310, 305, 315, np.nan, 330, 340, 335, 345, 350, 360,
355, np.nan, 370, 375, 380, 385, 390, 395, 400, 410],
'Category': ['A', 'B', 'A', 'C', 'B', 'A', 'C', 'B', 'A', 'C',
'A', 'B', 'C', 'A', 'B', 'C', 'A', 'B', 'C', 'A',
'B', 'C', 'A', 'B', 'C', 'A', 'B', 'C', 'A', 'B']
}
df = pd.DataFrame(data)
# Выводим первые несколько строк датафрейма
print("Исходный датафрейм:")
print(df.head())
# Базовая статистика по столбцу Sales
print("\nБазовая статистика по столбцу Sales:")
print(df['Sales'].describe())
# Обработка пропущенных значений
df['Sales_Filled'] = df['Sales'].fillna(method='ffill')
# Расчет скользящего среднего
df['Rolling_Avg_7d'] = df['Sales_Filled'].rolling(window=7).mean()
# Группировка по столбцу Category с вычислением средних продаж
category_avg = df.groupby('Category')['Sales_Filled'].mean()
print("\nСредние продажи по категориям:")
print(category_avg)
# Оптимизация типов данных
df['Sales'] = pd.to_numeric(df['Sales'], downcast='float')
df['Sales_Filled'] = pd.to_numeric(df['Sales_Filled'], downcast='float')
df['Rolling_Avg_7d'] = pd.to_numeric(df['Rolling_Avg_7d'], downcast='float')
34 Введение. Основы и не только
print("\nИспользование памяти после оптимизации:")
print(df.memory_usage(deep=True))
# Визуализация данных
plt.figure(figsize=(12, 6))
plt.plot(df['Date'], df['Sales_Filled'], label='Продажи (заполненные)')
plt.plot(df['Date'], df['Rolling_Avg_7d'], label='7-дневное скользящее среднее')
plt.title('Ежедневные продажи и семидневные скользящие средние')
plt.xlabel('Дата')
plt.ylabel('Продажи')
plt.legend()
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
Вывод:
Исходный датафрейм:
Date Sales Category
0 2023-01-01 200.0
A
1 2023-01-02 220.0
B
2 2023-01-03 NaN
A
3 2023-01-04 250.0
C
4 2023-01-05 260.0
B
Базовая статистика по столбцу Sales:
count
26.000000
mean
326.538462
std
58.271249
min
200.000000
25%
292.500000
50%
337.500000
75%
373.750000
max
410.000000
Name: Sales, dtype: float64
Средние продажи по категориям:
Category
A 310.454545
B 329.000000
C 323.888889
Name: Sales_Filled, dtype: float64
Использование памяти после оптимизации:
Index
128
Date
240
Sales
120
Category
1740
Sales_Filled
120
Rolling_Avg_7d
120
dtype: int64
Введение в промежуточный анализ данных 35
Рис. 1.1 Продажи и семидневное скользящее среднее
В этом коде демонстрируется сразу несколько техник промежуточного
анализа данных с использованием библиотек Pandas и NumPy. Давайте раз-
беремся, что в нем происходит.
1. Создание данных и их базовое исследование:
• создаем набор данных, состоящий из 30 записей, соответствующих
дневным продажам, со столбцом Category, в котором указана кате-
гория проданного товара;
• метод head() используется для отображения первых нескольких
строк датафрейма, что позволяет понять структуру данных.
2. Базовая статистика:
• метод describe() позволяет вывести основную статистическую ин-
формацию по столбцу Sales, включая количество элементов, среднее
значение, стандартное отклонение и квартили.
3. Заполнение пропущенных значений:
• здесь мы воспользовались методом fillna(), передав ему в качестве
параметра method значение 'ffill', что позволило заменить в столб-
це Sales пустые значения на последние валидные с созданием нового
столбца Sales_Filled
1
.
1
Вызов метода fillna() может вызывать предупреждение о том, что он является
устаревшим. Вместо него рекомендуется использовать метод ffill(). – Прим. перев.
36 Введение. Основы и не только
4. Анализ временных рядов:
• с помощью функции rolling() рассчитываем скользящее среднее по
созданному столбцу Sales_Filled за семь дней, что позволяет нивели-
ровать ежедневные колебания продаж и подчеркнуть долгосрочные
тенденции.
5. Группировка и агрегация:
• группируем наши данные по столбцу Category и рассчитываем сред-
ние продажи для всех уникальных категорий с помощью метода
groupby().
6. Оптимизация типов данных:
• здесь мы вызываем функцию to_numeric() с параметром downcast=
'float' с целью возможной оптимизации типов данных в числовых
столбцах.
7. Анализ используемой памяти:
• при помощи метода memory_usage() выводим информацию о том,
сколько памяти задействуется для хранения нашего датафрейма.
8. Визуализация данных:
• с помощью библиотеки Matplotlib создаем линейный график, на ко-
торый выводим столбец с продажами с заполненными пропущен-
ными значениями и столбец со скользящими средними продажами;
• с помощью графика можно обнаружить тренды и шаблоны в данных
о продажах.
В этом примере мы показали сразу несколько техник, которыми можно
воспользоваться в процессе выполнения промежуточного анализа данных.
1.1.3. Заполнение пропущенных значений
На промежуточном этапе анализа данных работа с пропусками в данных ве-
дется довольно активно. Вместо удаления строк с пропущенными значения-
ми или произвольного заполнения пропусков аналитики используют более
изощренные техники, призванные сохранить целостность набора данных
и восполнить пробелы на основе имеющейся информации.
Один из подходов состоит в прямом заполнении (forward filling). Он под-
разумевает использование последних валидных значений в столбце для за-
полнения пропусков. Этот подход особенно активно используется при работе
с временными рядами, где значения в столбцах имеют тенденцию к плавно-
му изменению. Противоположный прием именуется обратным заполнением
(backward filling), и в нем пропуски заполняются следующими в столбце ва-
лидными значениями.
Кроме того, зачастую для нивелирования пропусков в данных использу-
ется техника, называемая интерполяцией (interpolation). В этом случае зна-
чения для заполнения рассчитываются на основе шаблона окружающих на-
блюдений. При этом может использоваться линейная, полиномиальная или,
Введение в промежуточный анализ данных 37
к примеру, сплайновая интерполяция в зависимости от природы данных.
Этот метод обычно применяется при наличии определенных тенденций или
шаблона в данных.
Часто пропущенные значения в столбце заменяются на среднее, медиану
или моду (наиболее часто встречающееся значение) по столбцу. Этот подход
можно применять как ко всему столбцу, так и в разрезе групп. Он является
довольно простым, но при этом весьма эффективным.
В сложных сценариях можно воспользоваться так называемым методом
множественного восстановления пропущенных данных (multiple imputation),
который предполагает вычисление сразу нескольких вариантов для подста-
новки и дальнейшего их комбинирования для получения более устойчивой
оценки. Этот прием может оказаться особенно полезным при работе с дан-
ными, в которых пропуски распределены неслучайным образом.
Выбор способа подстановки пропущенных значений во многом зависит
от характера исходных данных, природы пропущенных значений и специ-
фики требований к анализу. При правильном выборе техники подстановки
можно добиться минимальной погрешности и поддержания статистической
мощности набора данных, что позволит повысить качество будущей модели.
Пример с более сложной агрегацией и двумя показателями
скользящего среднего
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# Создаем простой набор данных, на этот раз с помощью списков
dates = pd.date_range(start='2023-01-01', periods=30, freq='D')
sales = [100, 120, np.nan, 140, 160, 150, np.nan, 200, 180, 190,
210, 205, 215, np.nan, 230, 240, 235, 245, 250, 260,
255, np.nan, 270, 275, 280, 285, 290, 295, 300, 310]
categories = ['A', 'B', 'A', 'C', 'B', 'A', 'C', 'B', 'A', 'C',
'A', 'B', 'C', 'A', 'B', 'C', 'A', 'B', 'C', 'A',
'B', 'C', 'A', 'B', 'C', 'A', 'B', 'C', 'A', 'B']
df = pd.DataFrame({'Date': dates, 'Sales': sales, 'Category': categories})
# Выводим первые строки
print("Исходный датафрейм:")
print(df.head())
print("\nИнформация о датафрейме:")
print(df.info())
# Подставляем пропущенные значения с помощью прямого заполнения
df['Sales_Filled'] = df['Sales'].fillna(method='ffill')
# Рассчитываем скользящие средние с разными интервалами
df['Rolling_Avg_3d'] = df['Sales_Filled'].rolling(window=3).mean()
df['Rolling_Avg_7d'] = df['Sales_Filled'].rolling(window=7).mean()
38 Введение. Основы и не только
# Группируем по столбцу Category и вычисляем разные статистики
category_stats = df.groupby('Category')['Sales_Filled'].agg(['mean', 'median', 'std'])
print("\nСтатистика по категориям:")
print(category_stats)
# Оптимизируем типы данных
df['Sales'] = pd.to_numeric(df['Sales'], downcast='float')
df['Sales_Filled'] = pd.to_numeric(df['Sales_Filled'], downcast='float')
df['Rolling_Avg_3d'] = pd.to_numeric(df['Rolling_Avg_3d'], downcast='float')
df['Rolling_Avg_7d'] = pd.to_numeric(df['Rolling_Avg_7d'], downcast='float')
print("\nИспользование памяти до оптимизации:")
print(df.memory_usage(deep=True))
# Визуализация данных
plt.figure(figsize=(12, 6))
plt.plot(df['Date'], df['Sales'], label='Исходные продажи', alpha=0.7)
plt.plot(df['Date'], df['Sales_Filled'], label='Продажи (заполненные)')
plt.plot(df['Date'], df['Rolling_Avg_3d'], label='3-дневное скользящее среднее')
plt.plot(df['Date'], df['Rolling_Avg_7d'], label='7-дневное скользящее среднее')
plt.title('Ежедневные продажи и скользящие средние')
plt.xlabel('Дата')
plt.ylabel('Продажи')
plt.legend()
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
# Выводим итоговый датафрейм
print("\nИтоговый датафрейм:")
print(df)
Вывод:
Исходный датафрейм:
Date Sales Category
0 2023-01-01 100.0
A
1 2023-01-02 120.0
B
2 2023-01-03 NaN
A
3 2023-01-04 140.0
C
4 2023-01-05 160.0
B
Информация о датафрейме:
<class 'pandas.core .frame.DataFrame'>
RangeIndex: 30 entries, 0 to 29
Data columns (total 3 columns):
# Column
Non-Null Count Dtype
---
------
--------------
-----
0 Date
30 non-null
datetime64[ns]
1 Sales
26 non-null
float64
2 Category 30 non-null
object
dtypes: datetime64[ns](1), float64(1), object(1)
memory usage: 848.0+ bytes
None
Введение в промежуточный анализ данных 39
Статистика по категориям:
mean median
std
Category
A
211.363636 215.0 67.197132
B
229.000000 237.5 59 .057411
C
223.888889 240.0 54.588104
Использование памяти до оптимизации:
Index
128
Date
240
Sales
120
Category
1740
Sales_Filled
120
Rolling_Avg_3d
120
Rolling_Avg_7d
120
dtype: int64
Итоговый датафрейм:
Date Sales Category Sales_Filled Rolling_Avg_3d Rolling_Avg_7d
0 2023-01-01 100.0
A
100.0
NaN
NaN
1 2023-01-02 120.0
B
120.0
NaN
NaN
2 2023-01-03 NaN
A
120.0
113.333336
NaN
3 2023-01-04 140.0
C
140.0
126.666664
NaN
4 2023-01-05 160.0
B
160.0
140.000000
NaN
...
Рис. 1.2 Продажи и скользящие средние
40 Введение. Основы и не только
Кратко разберем этот код. От предыдущего примера он отличается тем,
что мы создали датафрейм не на основе словаря, а на основе отдельных
списков, задав имена столбцов непосредственно в функции pd.DataFrame().
Пропущенные значения мы также заменили с помощью метода прямого за-
полнения. Скользящие средние на этот раз мы рассчитали за два периода:
семидневный и трехдневный. Далее сгруппировали данные по полю Category
и для каждой категории рассчитали сразу три статистики: среднее значе-
ние, медиану и стандартное отклонение. На графике мы вывели исходные
данные, заполненные и оба скользящих средних. В заключение мы вывели
фрагмент нашего измененного датафрейма.
1.1.4. Вычисление скользящих средних
Вычисление скользящих средних (rolling average) представляет собой одну из
базовых техник промежуточного анализа данных и отвечает сразу несколь-
ким целям. Этот метод заключается в группировке определенного количества
наблюдений в так называемое временное окно, вычислении на основании
этого окна необходимых показателей и движении этого окна с фиксирован-
ным количеством строк по набору данных. Это позволяет эффективно сгла-
дить ежедневные колебания и шумы в данных и подчеркнуть долгосрочные
тенденции, которые иначе бывает заметить очень сложно.
Эффективность вычисления скользящих средних основывается на воз-
можности сохранения баланса между идентификацией важных трендов
и снижением влияния выбросов, или разовых всплесков. Подобного рода
анализ очень часто выполняется в сфере финансов для предсказания цен
на акции, в розничных и оптовых продажах и даже в научных областях при
анализе трендов. Выбор размера окна (три дня, семь дней, 30 дней и т. д .) ока-
зывает влияние на степень сглаживания и обнаружение тенденций и должен
подбираться очень тщательно на основе целей анализа и природы исходных
данных.
Более того, скользящие средние могут комбинироваться с другими статис-
тическими показателями, такими как стандартное отклонение, для реали-
зации более сложных статистических показателей, таких как линии, или по-
лосы, Боллинджера в финансовом анализе. Позже мы научимся эффективно
вычислять скользящие средние и интегрировать их в более сложные рабочие
процессы анализа данных.
Пример расчета процентных изменений и накопительных сумм
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# Создаем простой набор данных только с продажами
dates = pd.date_range(start='2023-01-01', periods=30, freq='D')
sales = [100, 120, np.nan, 140, 160, 150, np.nan, 200, 180, 190,
Введение в промежуточный анализ данных 41
210, 205, 215, np.nan, 230, 240, 235, 245, 250, 260,
255, np.nan, 270, 275, 280, 285, 290, 295, 300, 310]
df = pd.DataFrame({'Date': dates, 'Sales': sales})
# Подставляем пропущенные значения с помощью прямого заполнения
df['Sales_Filled'] = df['Sales'].fillna(method='ffill')
# Рассчитываем скользящие средние с разными интервалами
df['Rolling_Avg_3d'] = df['Sales_Filled'].rolling(window=3).mean()
df['Rolling_Avg_7d'] = df['Sales_Filled'].rolling(window=7).mean()
df['Rolling_Avg_14d'] = df['Sales_Filled'].rolling(window=14).mean()
# Вычисляем процентные изменения
df['Pct_Change'] = df['Sales_Filled'].pct_change()
# Рассчитываем накопительную сумму
df['Cumulative_Sum'] = df['Sales_Filled'].cumsum()
# Отображаем результаты
print(df)
# Визуализируем данные
plt.figure(figsize=(12, 6))
plt.plot(df['Date'], df['Sales_Filled'], label='Filled Sales')
plt.plot(df['Date'], df['Rolling_Avg_3d'], label='3-дневное скользящее среднее')
plt.plot(df['Date'], df['Rolling_Avg_7d'], label='7-дневное скользящее среднее')
plt.plot(df['Date'], df['Rolling_Avg_14d'], label='14-дневное скользящее среднее')
plt.title('Ежедневные продажи и скользящие средние')
plt.xlabel('Дата')
plt.ylabel('Продажи')
plt.legend()
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
Вывод:
Date Sales Sales_Filled Rolling_Avg_3d Rolling_Avg_7d \
0 2023-01-01 100.0
100.0
NaN
NaN
1 2023-01-02 120.0
120.0
NaN
NaN
2 2023-01-03 NaN
120.0
113.333333
NaN
3 2023-01-04 140.0
140.0
126.666667
NaN
4 2023-01-05 160.0
160.0
140.000000
NaN
...
25 2023-01-26 285.0
285.0
280.000000
268.571429
26 2023-01-27 290.0
290.0
285.000000
272.857143
27 2023-01-28 295.0
295.0
290.000000
278.571429
28 2023-01-29 300.0
300.0
295.000000
285.000000
29 2023-01-30 310.0
310.0
301.666667
290.714286
Rolling_Avg_14d Pct_Change Cumulative_Sum
0
NaN
NaN
100.0
1
NaN 0.200000
220.0
2
NaN 0.000000
340.0
3
NaN 0.166667
480.0
42 Введение. Основы и не только
4
NaN 0.142857
640.0
...
25
250.714286 0.017857
5435.0
26
256.071429 0.017544
5725.0
27
261.785714 0.017241
6020.0
28
266.785714 0.016949
6320.0
29
271.785714 0.033333
6630.0
Рис. 1.3 Продажи и скользящие средние
Что здесь новенького? Набор данных мы создали так же, как и раньше.
Скользящие средние рассчитали сразу по трем интервалам: трехдневному,
недельному и двухнедельному. Далее мы при помощи функции pct_change()
вычислили процентные изменения в столбце Sales_Filled ото дня ко дню,
после чего воспользовались методом cumsum() для расчета по этому же столб-
цу накопительных итогов. С помощью накопительных итогов мы можем от-
слеживать промежуточные суммы по полю на любой момент времени, что
бывает полезно для анализа динамики. Визуализацию мы выполнили по
всем рассчитанным скользящим средним.
1.1.5. Оптимизация типов данных
При работе с большими наборами данных на первый план выходит произво-
дительность. Библиотеки Pandas и NumPy предлагают богатый функционал
Введение в промежуточный анализ данных 43
по оптимизации использования памяти за счет применения разных типов
данных. Этот аспект начинает играть особую роль при недостатке ресурсов на
рабочих машинах. Выбирая оптимальные типы данных, вы сможете значи-
тельно снизить объем используемой памяти и сократить время выполнения
вычислений.
К примеру, переход на целочисленные типы int8 и int16 вместо использую-
щегося по умолчанию int64 позволит существенно снизить объем памяти, за-
действованной столбцами с ограниченными диапазонами значений. Точно
так же использование типа данных float32 вместо f loat64 приведет к значи-
тельной экономии памяти без ощутимого влияния на точность. Pandas дает
возможность воспользоваться параметром downcast в функциях to_numeric()
и astype() для автоматического выбора оптимального типа данных с гаран-
тией отсутствия потери точности.
Кроме того, в этой библиотеке присутствует особый тип данных category,
который можно эффективно использовать со столбцами с низкой кардиналь-
ностью (количеством уникальных значений). Применительно к текстовым
столбцам использование этого типа данных, а также более продвинутых тех-
ник вроде отображения в память может позволить существенно сэкономить
используемую память и ускорить выполнение таких операций, как группи-
ровка, сортировка и агрегация.
Пример использования категориального типа данных
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# Создаем простой набор данных
dates = pd.date_range(start='2023-01-01', periods=30, freq='D')
sales = [100, 120, np.nan, 140, 160, 150, np.nan, 200, 180, 190,
210, 205, 215, np.nan, 230, 240, 235, 245, 250, 260,
255, np.nan, 270, 275, 280, 285, 290, 295, 300, 310]
categories = ['A', 'B', 'C'] * 10
df = pd.DataFrame({'Date': dates, 'Sales': sales, 'Category': categories})
# Отображение информации об исходном датафрейме
print("Информация об исходном датафрейме:")
print(df.info())
print("\nИспользованная память исходным датафреймом:")
print(df.memory_usage(deep=True))
# Подставляем пропущенные значения с помощью прямого заполнения
df['Sales_Filled'] = df['Sales'].fillna(method='ffill')
# Оптимизируем типы данных
df['Sales'] = pd.to_numeric(df['Sales'], downcast='float')
df['Sales_Filled'] = pd.to_numeric(df['Sales_Filled'], downcast='float')
df['Category'] = df['Category'].astype('category')
# Вычисляем различные метрики
df['Rolling_Avg_3d'] = df['Sales_Filled'].rolling(window=3).mean()
44 Введение. Основы и не только
df['Rolling_Avg_7d'] = df['Sales_Filled'].rolling(window=7).mean()
df['Pct_Change'] = df['Sales_Filled'].pct_change()
df['Cumulative_Sum'] = df['Sales_Filled'].cumsum()
# Отображаем информацию после оптимизации
print("\nИнформация об оптимизированном датафрейме:")
print(df.info())
print("\nИспользованная память после оптимизации:")
print(df.memory_usage(deep=True))
# Вычисляем показатели по категориям
category_stats = df.groupby('Category')['Sales_Filled'].agg(['mean', 'median', 'std'])
print("\nПоказатели по категориям:")
print(category_stats)
# Визуализируем данные
plt.figure(figsize=(12, 6))
plt.plot(df['Date'], df['Sales'], label='Исходные продажи', alpha=0.7)
plt.plot(df['Date'], df['Sales_Filled'], label='Продажи (заполненные)')
plt.plot(df['Date'], df['Rolling_Avg_3d'], label='3-дневное скользящее среднее')
plt.plot(df['Date'], df['Rolling_Avg_7d'], label='7-дневное скользящее среднее')
plt.title('Ежедневные продажи и скользящие средние')
plt.xlabel('Дата')
plt.ylabel('Продажи')
plt.legend()
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
# Выводим итоговый датафрейм
print("\nИтоговый датафрейм:")
print(df.head())
Вывод:
Информация об исходном датафрейме:
<class 'pandas.core .frame.DataFrame'>
RangeIndex: 30 entries, 0 to 29
Data columns (total 3 columns):
# Column
Non-Null Count Dtype
---
------
--------------
-----
0 Date
30 non-null
datetime64[ns]
1 Sales
26 non-null
float64
2 Category 30 non-null
object
dtypes: datetime64[ns](1), float64(1), object(1)
memory usage: 848.0+ bytes
None
Использованная память исходным датафреймом:
Index
128
Date
240
Sales
240
Category 1740
Введение в промежуточный анализ данных 45
dtype: int64
Информация об оптимизированном датафрейме:
<class 'pandas.core .frame.DataFrame'>
RangeIndex: 30 entries, 0 to 29
Data columns (total 8 columns):
# Column
Non-Null Count Dtype
---
------
--------------
-----
0 Date
30 non-null
datetime64[ns]
1 Sales
26 non-null
float32
2 Category
30 non-null
category
3 Sales_Filled 30 non-null
float32
4 Rolling_Avg_3d 28 non-null
float64
5 Rolling_Avg_7d 24 non-null
float64
6 Pct_Change
29 non-null
float32
7 Cumulative_Sum 30 non-null
float32
dtypes: category(1), datetime64[ns](1), float32(4), float64(2)
memory usage: 1.5 KB
None
Использованная память после оптимизации:
Index
128
Date
240
Sales
120
Category
312
Sales_Filled
120
Rolling_Avg_3d 240
Rolling_Avg_7d 240
Pct_Change
120
Cumulative_Sum
120
dtype: int64
Показатели по категориям:
mean median
std
Category
A
211.5 227.5 64.680840
B
225.5 225 .0 56.541527
C
226.0 237.5 61.770184
Итоговый датафрейм:
Date Sales Category Sales_Filled Rolling_Avg_3d Rolling_Avg_7d \
0 2023-01-01 100.0
A
100.0
NaN
NaN
1 2023-01-02 120.0
B
120.0
NaN
NaN
2 2023-01-03 NaN
C
120.0
113.333333
NaN
3 2023-01-04 140.0
A
140.0
126.666667
NaN
4 2023-01-05 160.0
B
160.0
140.000000
NaN
Pct_Change Cumulative_Sum
0
NaN
100.0
1 0.200000
220.0
2 0.000000
340.0
3 0.166667
480.0
4 0.142857
640.0
46 Введение. Основы и не только
Что мы здесь сделали нового? Для начала мы создали датафрейм с повто-
ряющимся десять раз набором категорий товаров A, B и C, воспользовавшись
удобной конструкцией повторения ['A', 'B', 'C'] * 10. Затем вывели на экран
информацию о датафрейме и использованной памяти. Как видим, столбец
Sales занимает 240 байт, а столбец Category – 1740 байт. Далее мы заполнили
пропущенные значения в столбце Sales, заменив их на последние валидные
значения в столбце. После этого мы применили к столбцу Sales функцию
pd.to_numeric() с параметром downcast='float' и результат сохранили в столб-
це Sales_Filled. Столбец Category мы привели к категориальному типу с по-
мощью инструкции df['Category'].astype('category'). В результате столбец
Sales стал занимать в памяти 120 байт вместо 240, а столбец Category – 312
вместо 1740 байт. В расчете агрегаций и скользящих средних мы ничего не
поменяли, так что график приводить не будем.
1.1.6. Ключевые выводы
Промежуточный анализ данных не ограничивается одним лишь применени-
ем техник и приемов обработки данных, а подразумевает изменение само-
го подхода к восприятию данных. Необходимо думать не только о том, что
рассчитывать, но и погружаться в вопросы, касающиеся того, как и зачем
это делать. Это требует тщательного анализа применяемых методов, изуче-
ния их эффективности и пригодности для конкретной задачи. При работе
с объемными и сложными наборами данных необходимо изначально про-
думывать полную стратегию – от хранения данных до их преобразования
и анализа.
Техники, которые мы уже успели рассмотреть в этой главе, – это лишь вер-
шина айсберга промежуточного анализа данных. Но вы можете использовать
их в качестве трамплина к более сложным концепциям преобразования дан-
ных. По мере чтения книги вы будете все больше развивать аналитическое
чутье, заключающееся в умении находить правильный баланс между сложно-
стью используемых методов и их вычислительной стоимостью. И этот навык
вам очень пригодится, когда мы перейдем к теме конструирования и отбора
признаков, где способность извлекать ценную информацию из сырых дан-
ных ценится на вес золота.
В следующих разделах мы погрузимся в вопросы оптимизации рабочих
процессов. Вы научитесь использовать сложные приемы преобразования
данных вкупе с инструментами Pandas и NumPy. Такая синергия позволит
вам не только повысить скорость анализа данных, но и внести ясность и по-
рядок в ваш код и полученные результаты. Освоив представленные техники,
вы сможете обрабатывать большие объемы данных без особого труда.
Путь от простого к сложному 47
1.2. Путь от простого к сложному
При переходе на промежуточный уровень анализа данных нельзя забывать
о том, с чего все начиналось и как имеющиеся у вас навыки могут помочь
сделать следующий шаг в развитии в плане применяемых концепций. Про-
межуточный уровень аналитики требует изменения философии и подхода
к манипулированию и интерпретированию данных.
Первые шаги в анализе данных требуют наличия базовых навыков в отно-
шении работы с сырыми данными, визуализации и использования статисти-
ческих методов. Вы наверняка уже работали с библиотекой Pandas для преоб-
разования данных, пользовались какими-то вычислениями из пакета NumPy
и выводили графики при помощи Matplotlib. Эти инструменты являются
основой анализа данных, и вы продолжите их использовать и в дальнейшем.
Но при переходе на следующий уровень от вас потребуется умение ра-
ботать не только с базовыми функциями и методами из этих библиотек, но
и со всем богатейшим арсеналом инструментов, которые они предлагают.
Вам придется уделять больше внимания оптимизации рабочих процессов,
связанных с анализом данных, и повышению эффективности используемых
инструментов при решении сложных задач из реального мира. Эта книга
призвана перекинуть мостик между имеющимися у вас базовыми навыками
в работе с данными и овладением передовыми аналитическими техниками,
которые вам потребуются для решения действительно сложных задач. В сле-
дующих разделах мы покажем, как вы можете воспользоваться уже имеющи-
мися у вас навыками и подготовить почву для развития.
1.2.1. От простых техник манипулирования данными
к более сложным
На начальном этапе обучения вы наверняка усвоили операции загрузки
и фильтрации данных, отбора столбцов и простой группировки при помо-
щи библиотеки Pandas. Этих фундаментальных навыков достаточно для ре-
шения базовых задач, связанных с очисткой данных, первичным анализом
и простыми преобразованиями. Но при переходе на новый уровень вы пой-
мете, что этих знаний вам абсолютно недостаточно.
Промежуточный уровень анализа данных требует более глубокого пони-
мания работы инструментов, предоставляемых Pandas. Вам понадобится ос-
воить техники для работы со сложными структурами данных, такими как да-
тафреймы с множественными индексами и иерархические структуры. Кроме
того, вам часто придется разворачивать и сворачивать таблицы, преобразуя
их из широкого формата в узкий и обратно, а также менять размерности для
извлечения скрытой информации из сложных наборов данных.
48 Введение. Основы и не только
Помимо этого, гораздо больше внимания вам нужно будет уделять про-
изводительности при работе с большими объемами данных. Для этого вы
будете вырабатывать собственные стратегии, позволяющие эффективно
обрабатывать миллионы строк без существенной потери быстродействия.
В этом вам могут помочь векторизованные операции, являющиеся основой
NumPy, или техники пакетной обработки данных.
Также промежуточный уровень анализа данных подразумевает использо-
вание более сложных преобразований. Вы научитесь применять собственные
функции к данным при помощи методов apply() и applymap(), что позволит
вам лаконично и гибко выполнять сложные трансформации. Вы также уз-
наете о передовых способах группировки и агрегации данных, с помощью
которых сможете эффективно управлять данными сразу в нескольких из-
мерениях.
В ходе обучения вам придется гораздо больше внимания уделять вопросам
целостности и качества данных. Сюда включается реализация более надеж-
ных способов обнаружения ошибок, использование различных техник ва-
лидации данных и стратегий обработки граничных значений. Вы научитесь
писать код, который будет не только эффективно манипулировать данными,
но и постоянно следить за сохранением целостности и консистентности ва-
ших данных в процессе выполнения анализа.
Рассмотрим следующий простой пример фильтрации и группировки дан-
ных с целью вычисления средних продаж в разрезе магазинов:
Пример кода: начальный уровень манипулирования данными
import pandas as pd
# Простой набор данных
data = {'Store': ['A', 'B', 'A', 'B', 'A', 'B'],
'Sales': [200, 220, 210, 250, 215, 240]}
df = pd.DataFrame(data)
# Группируем по Store и рассчитываем средние продажи по магазинам
avg_sales = df.groupby('Store')['Sales'].mean()
print(avg_sales)
Вывод:
Store
A 208.333333
B 236.666667
Name: Sales, dtype: float64
Разберем этот простой фрагмент кода:
импортируем библиотеку pandas;
создаем словарь с двумя ключами: Store и Sales. Каждому ключу по-
ставлен в соответствие список значений;
создаем датафрейм на основе словаря при помощи инструкции
pd.DataFrame(data);
Путь от простого к сложному 49
применяем метод groupby() для группировки данных по столбцу Store;
применяем метод mean() к столбцу Sales в объекте с группами, рассчи-
тывая средние продажи для каждого отдельного магазина;
сохраняем результат в переменную avg_sales и выводим ее.
Здесь мы вычисляем средние продажи в разрезе магазинов. Это важная,
но достаточно простая операция. А что, если у вас достаточно большой дата-
фрейм с миллионами строк и вам необходимо выполнить более сложную
группировку или отфильтровать данные с применением сложных критериев?
1.2.2. Промежуточный уровень манипулирования
данными
Давайте применим ту же концепцию, но сделаем код более надежным, эф-
фективным и гибким. Допустим, нам необходимо сгруппировать данные сра-
зу по нескольким столбцам и вычислить несколько агрегаций, предваритель-
но оставив данные только по одному магазину. Вот как можно это сделать:
# Чуть более сложные данные
data = {'Store': ['A', 'B', 'A', 'B', 'A', 'B'],
'Sales': [200, 220, 210, 250, 215, 240],
'Category': ['Electronics', 'Clothing', 'Clothing', 'Electronics', 'Electronics',
'Clothing']}
df = pd.DataFrame(data)
# Группировка по полям Store и Category с вычислением разных агрегаций и фильтром на
магазин A
agg_sales = df[df['Store'] == 'A'].groupby(['Store', 'Category']).agg(
avg_sales=('Sales', 'mean'),
total_sales=('Sales', 'sum')
).reset_index()
print(agg_sales)
Вывод:
Store
Category avg_sales total_sales
0
A
Clothing
210.0
210
1
A Electronics
207.5
415
Изменения, которые мы внесли:
теперь в нашем словаре есть уже три ключа: Store, Sales и Category;
мы также преобразовали словарь в датафрейм;
в данном случае мы воспользовались группировкой данных сразу по
двум столбцам Store и Category и применили метод agg() для расчета
сразу двух агрегаций: среднего и суммы. Но перед этим мы отфильт-
ровали датафрейм при помощи инструкции df[df['Store'] == 'A'], оста-
вив в нем только магазин A;
50 Введение. Основы и не только
метод reset_index() мы применили, чтобы получившийся в результате
группировки датафрейм с множественным индексом преобразовать
в обычный датафрейм с полями Store и Category в виде столбцов;
в завершение выводим на экран итоговый датафрейм.
Здесь мы продемонстрировали чуть более сложный подход к анализу дан-
ных с помощью библиотеки Pandas. Такой подход с объединением агрегаций
в одной операции является более гибким и эффективным, особенно при
работе с большими наборами данных. При выполнении промежуточного
анализа данных несколько операций зачастую объединяются вместе в так
называемый рабочий процесс, или конвейер, что позволяет повысить эф-
фективность обработки и облегчить чтение кода.
1.2.3. Построение эффективных рабочих процессов
Еще один акцент, который мы будем делать в этой книге, связан с оптими-
зацией рабочих процессов. При переходе от начального к промежуточному
уровню анализа данных вы научитесь воспринимать задачи не как отдель-
ные операции, а как эффективные и масштабируемые рабочие процессы. Это
очень важно в связи с ростом объемов наборов данных, с которыми вы будете
работать, и сложности обработки данных. В таких условиях рационализация
и упрощение рабочих процессов играют важнейшую роль.
Представьте, что вы выполняете предварительную обработку данных
в большом датасете. Новичок мог бы подойти к этой задаче как к ряду от-
дельных процедур, которые следует выполнять изолированно друг от друга.
И если для небольших наборов данных такой подход может оказаться при-
емлемым, то с ростом объемов выполнение операций по отдельности утра-
чивает свою актуальность, а ему на смену приходят продвинутые техники
автоматизации и оптимизации процессов.
Одной из ключевых концепций, с которыми вы познакомитесь на этом
пути, будут конвейеры (pipeline). Конвейеры позволяют связать воедино не-
сколько шагов с обработкой данных. Это не только делает код более легким
для восприятия, но также позволяет существенно повысить производитель-
ность. Заранее объявляя операции, которые будут применяться к данным
в последовательной манере, вы можете легко и без ручного вмешательства
обрабатывать большие объемы данных.
Кроме того, вы познакомитесь с техниками параллельного выполнения,
позволяющими распределять нагрузку при обработке данных по разным
ядрам процессора и даже разным машинам. Это может помочь существенно
сократить время обработки при наличии больших наборов данных. Вы так-
же освоите эффективные в плане потребления памяти техники для работы
с данными, не помещающимися в оперативную память.
Еще один аспект оптимизации рабочих процессов связан с созданием по-
вторно используемых модулей. Вместо написания отдельных блоков кода для
каждого проекта вы научитесь разрабатывать гибкие модульные функции
Путь от простого к сложному 51
и классы, которые можно легко адаптировать для разных наборов данных
и требований. Это позволит вам не только сэкономить время, но и снизить
количество ошибок за счет использования хорошо отлаженных модулей.
Пример кода: создание конвейера предварительной обработки
данных
Предположим, мы работаем с набором данных, в котором присутствуют
пропущенные значения и переменные, подлежащие масштабированию для
включения в модель. Начинающий аналитик мог бы написать отдельные
блоки кода для подстановки пропущенных значений и масштабирования
переменных.
Ниже показано, как эти операции можно объединить в конвейеры при
помощи библиотеки sklearn. Именно такой подход больше подходит для про-
межуточного анализа данных.
import pandas as pd
import numpy as np
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
# Простой набор данных с пропущенными значениями и категориальной переменной
data={
'Feature1': [1, 2, np.nan, 4, 5],
'Feature2': [10, np.nan, 12, 14, 15],
'Category': ['A', 'B', 'A', 'C', 'B']
}
df = pd.DataFrame(data)
# Предварительная обработка для числовых столбцов
numeric_features = ['Feature1', 'Feature2']
numeric_transformer = Pipeline(steps=[
('imputer', SimpleImputer(strategy='mean')),
('scaler', StandardScaler())
])
# Предварительная обработка для категориальных столбцов
categorical_features = ['Category']
categorical_transformer = Pipeline(steps=[
('imputer', SimpleImputer(strategy='constant', fill_value='missing')),
('onehot', OneHotEncoder(handle_unknown='ignore'))
])
# Объединяем шаги предварительной обработки
preprocessor = ColumnTransformer(
transformers=[
('num', numeric_transformer, numeric_features),
('cat', categorical_transformer, categorical_features)
])
52 Введение. Основы и не только
# Создаем и запускаем конвейер
pipeline = Pipeline(steps=[('preprocessor', preprocessor)])
transformed_data = pipeline.fit_transform(df)
# Преобразуем в датафрейм для лучшей визуализации
feature_names = (numeric_features +
pipeline.named_steps['preprocessor']
.n a m ed_transformers_['cat']
.n a m ed_steps['onehot']
.get_feature_names _out(categorical_features).tolist())
transformed_df = pd.DataFrame(transformed_data, columns=feature_names)
print("Исходные данные:")
print(df)
print("\nПреобразованные данные:")
print(transformed_df)
Вывод:
Исходные данные:
Feature1 Feature2 Category
0
1.0
10.0
A
1
2.0
NaN
B
2
NaN
12.0
A
3
4.0
14.0
C
4
5.0
15.0
B
Преобразованные данные:
Feature1 Feature2 Category_A Category_B Category_C
0 -1 .414214 -1 .601112
1.0
0.0
0.0
1 -0 .707107 0.000000
0.0
1.0
0.0
2 0.000000
- 0 .436667
1.0
0.0
0.0
3 0.707107 0.727778
0.0
0.0
1.0
4 1.414214 1.310001
0.0
1.0
0.0
Разберемся, что здесь происходит.
1. Импорт библиотек:
• для манипулирования данными мы импортируем библиотеку pan-
das, для выполнения числовых операций – numpy, а также подгружа-
ем нужные модули из библиотеки Scikit-learn для предварительной
обработки и создания конвейеров.
2. Создание простого набора данных:
• генерируем набор данных с двумя признаками, содержащими про-
пущенные значения (Feature1 и Feature2), и категориальной пере-
менной Category.
3. Определение конвейера для предварительной обработки числовых
столбцов:
• создаем конвейер для числовых признаков, включающий в себя: а)
SimpleImputer (для замены пропущенных значений на средние) и б)
StandardScaler (для стандартизации признаков с приведением к еди-
ничному стандартному отклонению).
Путь от простого к сложному 53
4. Определение конвейера для предварительной обработки категориаль-
ных столбцов:
• создаем конвейер для категориальных признаков, включающий
в себя: а) SimpleImputer (для замены пропущенных значений на стро-
ковую константу 'missing') и б) OneHotEncoder (для преобразования
категориальных признаков в фиктивные переменные с одним ак-
тивным состоянием).
5. Объединение шагов предварительной обработки:
• используем класс ColumnTransformer для применения разных шагов
предварительной обработки к разным типам признаков:
• трансформер 'num' применяется к числовым признакам;
• трансформер 'cat' применяется к категориальным признакам.
6. Создание и запуск конвейера:
• определяем основной конвейер, содержащий наш объект preproces-
sor;
• запускаем конвейер на наших данных с помощью метода fit_trans-
form().
7. Преобразование результатов в датафрейм:
• извлекаем имена признаков из преобразованных данных, включая
фиктивные переменные с одним активным состоянием;
• создаем датафрейм на основе преобразованных данных с соответ-
ствующими именами столбцов для лучшей визуализации.
8. Вывод результатов:
• выводим на экран исходный и преобразованный датафреймы для
демонстрации результатов преобразований.
Этот пример показывает общий подход к предварительной обработке
данных в числовых и категориальных столбцах. Мы продемонстрировали
вариант использования классов Pipeline и ColumnTransformer для создания
надежных рабочих процессов по замене пропущенных значений и масшта-
бированию данных, которые легко можно использовать повторно в своих
проектах.
1.2.4. Использование библиотеки NumPy
для повышения производительности
При переходе на новый качественный уровень аналитики данных вам при-
дется активно использовать библиотеку NumPy для реализации эффектив-
ных рабочих процессов, особенно когда дело касается числовых вычислений.
Новички зачастую стараются выполнять все операции силами Pandas, при-
том что инструменты из библиотеки NumPy способны справляться с чис-
ловыми операциями применительно к большим массивам данных гораздо
быстрее и эффективнее благодаря использованию оптимизированных струк-
тур данных.
54 Введение. Основы и не только
Производительность NumPy проистекает из использования непрерывных
блоков памяти и возможности применения векторизованных операций. Это
означает, что вместо традиционного прохождения в цикле по отдельным
элементам библиотека NumPy будет применять нужные операции сразу ко
всем массивам, что существенно повышает быстродействие. К примеру, при
работе с большими наборами данных использование векторизованных опе-
раций NumPy способно на порядок ускорить процессы в сравнении с при-
менением встроенных средств Python и даже Pandas.
Кроме того, библиотека NumPy имеет в своем арсенале богатейший набор
математических функций, оптимизированных для работы с большими мас-
сивами. Среди прочего средствами NumPy можно реализовывать все много-
образие операций линейной алгебры, преобразование Фурье, генерировать
псевдослучайные числа и многое другое.
Еще одним преимуществом библиотеки NumPy является ее оптимизация
в отношении работы с памятью. В массивах NumPy используются фикси-
рованные типы данных, что позволяет хранить данные гораздо более ком-
пактно в сравнении с теми же списками в Python. Это дает возможность не
только сэкономить место на диске и в памяти, но и существенно ускорить
выполнение операций, поскольку центральный процессор намного быстрее
обрабатывает данные, хранящиеся в консистентном виде.
Изучая аналитику данных, вы увидите, что библиотека NumPy способна
значительно облегчить выполнение задач вроде конструирования и отбора
признаков, внедрения пользовательских алгоритмов и оптимизации сущест-
вующего кода с целью повышения его эффективности. Объединив мощь
Pandas в отношении манипулирования табличными данными с гибкостью
NumPy в плане выполнения числовых расчетов, вы сможете создавать макси-
мально эффективные и масштабируемые аналитические рабочие процессы.
Давайте рассмотрим типичный вариант решения задачи создания нового
столбца в датафрейме с суммой значений из двух других столбцов:
# Начальный уровень с использованием Pandas
df['Total'] = df['Feature1'] + df['Feature2']
print(df)
Здесь мы создали новый столбец в датафрейме df с именем Total, который
заполнился суммой значений в столбцах Feature1 и Feature2, и вывели итого-
вый датафрейм на экран.
Это простой и прямолинейный подход, вполне подходящий для новичков
или для работы с небольшими наборами данных. При увеличении объемов
или использовании более сложных операций над данными гораздо лучше
будет воспользоваться библиотекой NumPy, как показано ниже:
import numpy as np
# Преобразуем датафрейм в массив NumPy для ускорения вычислительных операций
data_np = df.to_numpy()
Pandas, NumPy и Scikit-learn в действии 55
# Выполняем поэлементное суммирование по столбцам NumPy
total = np.nansum(data_np, axis=1) # Пропущенные значения воспринимаем как нулевые
print(total)
Вывод:
[11. 2 . 12. 18. 20.]
Этот код демонстрирует более зрелый подход к числовым вычислениям
с использованием библиотеки NumPy, который более применим к наборам
данных больших размеров.
Что здесь происходит:
сначала мы, как и всегда, импортируем библиотеку NumPy;
преобразовываем датафрейм в массив NumPy с помощью инструкции
метода to_numpy(). После этого мы можем обращаться с нашими дан-
ными как с массивом;
применяем функцию np.nansum() для вычисления суммы по столбцам
(axis=1), т. е . в каждой строке по всем столбцам, в массиве NumPy. При-
ставка nan в имени функции указывает на то, что при выполнении опе-
рации мы будем воспринимать пропущенные значения (NaN) как нули,
что бывает удобно при работе с неполными данными;
результат с вычислениями для каждой строки сохраняем в переменную
total, которая по своей сути может быть представлена в виде нового
столбца в массиве;
выводим на экран содержимое переменной total.
Этот подход является более эффективным по сравнению с использованием
библиотеки Pandas при работе с большими объемами, поскольку в нем при-
меняются оптимизированные вычисления NumPy, а пропущенные значения
обрабатываются автоматически.
1.3. Pandas, NumPy и Scikit-learn
в действии
Анализ данных и конструирование признаков требуют использования наи-
более эффективных и современных из всех доступных инструментов. Вы уже
знакомы с великолепной троицей библиотек Pandas, NumPy и Scikit-learn,
средствами которых можно эффективно решить подавляющее большинство
задач, связанных с анализом данных. В этом разделе мы продемонстрируем
синергетический потенциал этих мощных библиотек и посмотрим, как мож-
но комбинировать их инструменты при решении аналитических сценариев
из реальной жизни.
Каждая из этих библиотек имеет свою четкую направленность. Библиоте-
ка Pandas предназначена для манипулирования табличными данными и их
56 Введение. Основы и не только
преобразования, в NumPy основной упор сделан на высокоэффективных
числовых вычислениях, а Scikit-learn незаменима при создании и проверке
моделей машинного обучения. Любой квалифицированный аналитик дол-
жен уметь не только использовать эти библиотеки по отдельности, но и эф-
фективно сочетать их при решении поставленных задач.
Для демонстрации вариантов совместного использования этих библиотек
мы рассмотрим сразу несколько примеров из реальной жизни. Эти приме-
ры позволят вам понять, сколь мощную аналитическую экосистему можно
создать на основе этих трех библиотек за счет построения универсальных
и эффективных рабочих процессов.
1.3.1. Pandas: манипулирование табличными
данными на экспертном уровне
Мы уже не раз говорили о незаменимости библиотеки Pandas в деле манипу-
лирования и анализа данных. Но одного лишь знакомства с этой библиотекой
вам как аналитику будет недостаточно, нужно понимать, как устроены ее
внутренние механизмы. В какой-то момент вам непременно понадобится
обрабатывать массивы данных, не помещающиеся в оперативную память,
и тогда вам пригодятся техники разбиения данных на блоки и вычисления
с использованием внешней памяти. Также вам необходимо освоить множест-
венные условия и иерархические индексы для полноценной работы с дан-
ными в Pandas.
Кроме того, вы должны уметь применять по необходимости векторизован-
ные операции, свободно владеть методами из семейства apply и обращаться
к другим библиотекам, таким как NumPy, для эффективного выполнения чис-
ловых вычислений. Не будет лишним и умение пользоваться расширениями
Pandas в виде библиотек Dask и Vaex для реализации распределенных и от-
ложенных вычислений без задействования памяти и операций копирования.
Давайте для демонстрации рассмотрим практический пример с большим
набором данных с продажами. Нам необходимо произвести очистку данных,
чтобы привести их в консистентный вид, применить фильтры для выделения
интересующих нас срезов данных, а также рассчитать агрегации с целью из-
влечения выводов.
import pandas as pd
import numpy as np
from sklearn.preprocessing import StandardScaler
from sklearn.impute import SimpleImputer
# Сгенерированные данные: транзакции с продажами
data={
'TransactionID': [101, 102, 103, 104, 105, 106, 107, 108, 109, 110],
'Store': ['A', 'B', 'A', 'C', 'B', 'A', 'C', 'B', 'A', 'C'],
'SalesAmount': [250, 120, 340, 400, 200, np.nan, 180, 300, 220, 150],
Pandas, NumPy и Scikit-learn в действии 57
'Discount': [10, 15, 20, 25, 5, 12, np.nan, 18, 8, 22],
'Date': pd.to_datetime(['2023-01-01', '2023-01-02', '2023-01-03', '2023-01-04',
'2023-01-05', '2023-01-06', '2023-01-07', '2023-01-08',
'2023-01-09', '2023-01-10']),
'Category': ['Electronics', 'Clothing', 'Electronics', 'Home', 'Clothing',
'Home', 'Electronics', 'Home', 'Clothing', 'Electronics']
}
df = pd.DataFrame(data)
# 1. Очистка данных и замена пропущенных значений
imputer = SimpleImputer(strategy='mean')
df[['SalesAmount', 'Discount']] = imputer.fit_transform(df[['SalesAmount', 'Discount']])
# 2. Конструирование признаков
df['DayOfWeek'] = df['Date'].dt.dayofweek
df['NetSales'] = df['SalesAmount'] - df['Discount']
df['DiscountPercentage'] = (df['Discount'] / df['SalesAmount']) * 100
# 3. Расширенная фильтрация
high_value_sales = df[(df['SalesAmount'] > 200) & (df['Store'].isin(['A', 'B']))]
# 4. Агрегация и группировка
agg_sales = df.groupby(['Store', 'Category']).agg(
TotalSales=('NetSales', 'sum'),
AvgSales=('NetSales', 'mean'),
MaxDiscount=('Discount', 'max'),
SalesCount=('TransactionID', 'count')
).reset_index()
# 5. Анализ временных рядов
daily_sales = df.resample('D', on='Date')['NetSales'].sum().reset_index()
# 6. Нормализация
scaler = StandardScaler()
df['NormalizedSales'] = scaler.fit_transform(df[['SalesAmount']])
# 7. Создание сводной таблицы
category_store_pivot = pd.pivot_table(df, values='NetSales',
index='Category',
columns='Store',
aggfunc='sum',
fill_value=0)
# Вывод результатов
print("Исходные данные:")
print(df)
print("\nКрупные продажи в магазинах A и B:")
print(high_value_sales)
print("\nАгрегированные продажи:")
print(agg_sales)
print("\nДневные продажи:")
print(daily_sales)
58 Введение. Основы и не только
print("\nСводная таблица по категориям товаров и магазинам:")
print(category_store_pivot)
Вывод:
Исходные данные:
TransactionID Store SalesAmount Discount
Date
Category \
0
101
A
250.0
10.0 2023-01-01 Electronics
1
102
B
120.0
15.0 2023-01-02
Clothing
2
103
A
340.0
20.0 2023-01-03 Electronics
...
7
108
B
300.0
18.0 2023-01-08
Home
8
109
A
220.0
8.0 2023-01-09
Clothing
9
110
C
150.0
22.0 2023-01-10 Electronics
DayOfWeek NetSales DiscountPercentage NormalizedSales
0
6
240.0
4.000000
0.121806
1
0
105.0
12.500000
- 1 .461677
2
1
320.0
5.882353
1.218064
...
7
6
282.0
6.000000
0.730838
8
0
212.0
3.636364
- 0 .243613
9
1
128.0
14.666667
- 1 .096257
Крупные продажи в магазинах A и B:
TransactionID Store SalesAmount Discount
Date
Category \
0
101
A
250.0
10.0 2023-01-01 Electronics
2
103
A
340.0
20.0 2023-01-03 Electronics
5
106
A
240.0
12.0 2023-01-06
Home
7
108
B
300.0
18.0 2023-01-08
Home
8
109
A
220.0
8.0 2023-01-09
Clothing
DayOfWeek NetSales DiscountPercentage
0
6
240.0
4.000000
2
1
320.0
5.882353
5
4
228.0
5.000000
7
6
282.0
6.000000
8
0
212.0
3.636364
Агрегированные продажи:
Store
Category TotalSales AvgSales MaxDiscount SalesCount
0
A
Clothing
212.0
212.0
8.0
1
1
A Electronics
560.0
280.0
20.0
2
2
A
Home
228.0
228.0
12.0
1
3
B
Clothing
300.0
150.0
15.0
2
4
B
Home
282.0
282.0
18.0
1
5
C Electronics
293.0
146.5
22.0
2
6
C
Home
375.0
375.0
25.0
1
Дневные продажи:
Date NetSales
0 2023-01-01
240.0
1 2023-01-02
105.0
2 2023-01-03
320.0
Pandas, NumPy и Scikit-learn в действии 59
3 2023-01-04
375.0
4 2023-01-05
195.0
5 2023-01-06
228.0
6 2023-01-07
165.0
7 2023-01-08
282.0
8 2023-01-09
212.0
9 2023-01-10
128.0
Сводная таблица по категориям товаров и магазинам:
Store
A
B
C
Category
Clothing
212.0 300 .0 0 .0
Electronics 560.0 0 .0 293.0
Home
228.0 282.0 375.0
Что происходит в этом коде?
1. Загрузка и предварительная обработка данных:
• генерируем набор данных с суммами транзакций, магазинами, да-
тами продажи и категориями товаров;
• используем класс SimpleImputer для замены пропущенных значений
на средние в столбцах SalesAmount и Discount.
2. Конструирование признаков:
• извлекаем день недели из столбца с датами;
• создаем столбец NetSales, вычитая значения в столбце Discount из
значений в столбце SalesAmount;
• создаем столбец DiscountPercentage с рассчитанным процентом скид-
ки по транзакции.
3. Расширенная фильтрация:
• оставляем в наборе данных только транзакции с суммой, превы-
шающей $200, по магазинам A и B, применяя для этого метод isin().
4. Агрегация и группировка:
• группируем данные по столбцам Store и Category для получения
укрупненной информации о продажах;
• рассчитываем разные агрегации по столбцам: общие продажи, сред-
ние продажи, максимальную скидку и количество транзакций.
5. Анализ временных рядов:
• используем метод resample() для передискретизации временных
данных по дням, демонстрируя возможности анализа временных
рядов.
6. Нормализация:
• применяем класс StandardScaler для стандартизации данных в столб-
це SalesAmount – это зачастую требуется в процессе подготовки дан-
ных для моделей машинного обучения.
7. Создание сводной таблицы:
• строим сводную таблицу с укрупненными суммами продаж на пере-
сечении категорий товаров и магазинов для компактного вывода
информации.
60 Введение. Основы и не только
1.3.2. NumPy: высокоэффективные числовые
вычисления
Когда речь заходит о числовых вычислениях, одной из наиболее эффектив-
ных библиотек является NumPy. Если Pandas в основном используется для
работы с табличными данными, NumPy чувствует себя как рыба в воде при
выполнении матричных операций и работе с большими числовыми мас-
сивами любой размерности. Использовать эту библиотеку бывает удобно
при необходимости в процессе конструирования признаков применять
сложные и не очень математические преобразования к существующим
переменным.
Потенциал NumPy кроется в способности выполнять векторизованные
операции над данными, что позволяет применять вычисления не построч-
но, а сразу ко всем векторам или массивам. К примеру, средствами этой
библиотеки можно очень эффективно выполнить поэлементное перемноже-
ние, матричное перемножение и даже применить вычисления из линейной
алгебры, что делает ее абсолютно незаменимой в области анализа данных
и разработки моделей машинного и глубокого обучения.
В основе библиотеки NumPy лежат высокоэффективные и оптимизиро-
ванные инструкции на языке C. Особенно заметно это бывает при работе
с многомерными массивами, что часто используется в обработке изображе-
ний, анализе сигналов и финансовом моделировании.
Представим, что нам необходимо выполнить пакетное преобразование
данных о продажах. К примеру, мы хотим вычислить логарифм от продаж,
что часто требуется при работе с моделями, требующими поступления на
вход нормализованных данных.
import numpy as np
# Преобразуем столбец SalesAmount в вектор NumPy
sales_np = df['SalesAmount'].to_numpy()
# Применяем логарифмическое преобразование (бывает полезно для распределений со смещениями)
log_sales = np.log(sales_np)
print(log_sales)
Вывод:
[5.52146092 4.78749174 5.82894562 5.99146455 5.29831737 5.48063892
5.19295685 5.70378247 5.39362755 5.01063529]
Этот код демонстрирует применение библиотеки NumPy с целью выпол-
нения эффективных вычислений для преобразования данных. Вот что здесь
происходит:
импортируем библиотеку NumPy;
Pandas, NumPy и Scikit-learn в действии 61
преобразуем столбец SalesAmount в вектор NumPy с помощью метода
to_numpy(). Это преобразование позволит выполнять операции над дан-
ными в данном столбце более эффективно;
вызываем функцию np.log(), тем самым применяя логарифмическое
преобразование к столбцу с продажами. Эта операция бывает очень
уместна при работе с данными, распределенными неравномерно;
выводим на экран результат преобразования.
Эффективность этого подхода основывается на векторизованной природе
функций в NumPy, позволяющей выполнять вычисления сразу над целыми
массивами данных.
Логарифмическое преобразование часто используется в финансовом ана-
лизе и моделях машинного обучения, поскольку позволяет избавиться от
смещенности распределений и тем самым подготовить данные в более нор-
мализованном виде для некоторых типов анализа и моделирования.
Давайте рассмотрим чуть более сложный пример применения векторизо-
ванных операций к исходным данным:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats
# Данные о продажах
data={
'SalesAmount': [100, 150, 200, 250, 300, 350, 400, 450, 500, 1000],
'ProductCategory': ['A', 'B', 'A', 'C', 'B', 'A', 'C', 'B', 'A', 'C']
}
df = pd.DataFrame(data)
# Преобразуем столбец SalesAmount в вектор NumPy
sales_np = df['SalesAmount'].to_numpy()
# Применяем логарифмическое преобразование (бывает полезно для распределений со смещениями)
log_sales = np.log(sales_np)
# Рассчитываем базовые статистики
mean_sales = np.mean(sales_np)
median_sales = np.median(sales_np)
std_sales = np.std(sales_np)
# Вычисляем z-оценки
z_scores = stats.zscore(sales_np)
# Определяем выбросы (z-оценка > 3 или < -3)
outliers = np.abs(z_scores) > 3
# Выводим результаты
print("Исходные продажи:", sales_np)
62 Введение. Основы и не только
print("Результат логарифмического преобразования:", log_sales)
print("Средние продажи:", mean_sales)
print("Медианные продажи:", median_sales)
print("Стандартное отклонение:", std_sales)
print("Z-оценки:", z_scores)
print("Выбросы:", df[outliers])
# Визуализируем результаты
plt.figure(figsize=(12, 6))
plt.subplot(121)
plt.hist(sales_np, bins=10, edgecolor='black')
plt.title('Распределение исходных продаж')
plt.xlabel('SalesAmount')
plt.ylabel('Частота')
plt.subplot(122)
plt.hist(log_sales, bins=10, edgecolor='black')
plt.title('Распределение логарифмированных продаж')
plt.xlabel('Log(SalesAmount)')
plt.ylabel('Частота')
plt.tight_layout()
plt.show()
Вывод:
Исходные продажи: [ 100 150 200 250 300 350 400 450 500 1000]
Результат логарифмического преобразования: [4.60517019 5.01063529 5.29831737 5.52146092
5.70378247 5.85793315
5.99146455 6.10924758 6.2146081 6.90775528]
Средние продажи: 370.0
Медианные продажи: 325.0
Стандартное отклонение: 243.10491562286435
Z-оценки: [-1 .11063159 -0 .90495908 -0 .69928656 -0 .49361404 -0 .28794152 -0 .08226901
0.12340351 0.32907603 0.53474855 2.59147372]
Выбросы: Empty DataFrame
Произведем разбор кода.
1. Подготовка данных:
• начинаем с импорта необходимых библиотек. В нашем случае это
NumPy для числовых операций, Pandas для манипулирования данны-
ми, Matplotlib для визуализации и SciPy – для вычисления z-оценок;
• генерируем простые данные с категориями товаров и суммами про-
даж.
2. Преобразование данных:
• преобразуем столбец с именем SalesAmount в вектор NumPy. Это по-
зволит выполнять математические и статистические операции над
ним более эффективно.
3. Логарифмическое преобразование продаж:
• применяем логарифмическое преобразование к столбцу с продажа-
ми с помощью функции np.log().
Pandas, NumPy и Scikit-learn в действии 63
4. Статистический анализ:
• с помощью соответствующих функций NumPy вычисляем основные
статистические показатели продаж (среднее, медиану, стандартное
отклонение);
• с помощью функции stats.zscore() из библиотеки SciPy рассчиты-
ваем z-оценки (z-score). Этот показатель говорит о том, на сколько
стандартных отклонений значение отстоит от среднего;
• определяем выбросы в данных, полагая, что к ним относятся значе-
ния, для которых z-оценка превышает 3 по модулю.
5. Визуализация результатов:
• строим две гистограммы с помощью библиотеки Matplotlib: первая
показывает распределение исходных данных, а вторая – преобразо-
ванных.
Рис. 1.4 Распределение исходных и преобразованных данных
Этот простой пример демонстрирует всеобъемлющий подход к анализу
данных в миниатюре. Здесь мы выполнили преобразование исходных дан-
ных, вычислили необходимые статистические показатели, определили вы-
бросы и построили две диаграммы. Вы увидели, что инструменты из библио-
теки NumPy могут использоваться совместно с Pandas, SciPy и Matplotlib
с целью проведения исследовательского анализа данных.
64 Введение. Основы и не только
1.3.3. Использование инструментов NumPy
для преобразований
Применяя векторизованные операции, характерные для библиотеки NumPy,
аналитики данных могут не только ускорить в разы свой код, но и сделать его
более простым для восприятия и легким для чтения. Применяемый синтак-
сис для таких операций не содержит каких-то дополнительных функций или
методов, а все делает очень прозрачно, словно вы работаете со скалярными
переменными, а не векторами и массивами. Это позволяет значительно об-
легчить понимание этих конструкций и использование их в команде, состо-
ящей из специалистов в разных областях: от науки о данных до статистики
и разработки программного обеспечения.
Давайте посмотрим, как можно вычислить z-оценки при помощи одной
только библиотеки NumPy:
# Вычисление z-оценок для столбца SalesAmount
mean_sales = np.mean(sales_np)
std_sales = np.std(sales_np)
z_scores = (sales_np - mean _sales) / std_sales
print(z_scores)
Вот что здесь происходит:
с помощью функции np.mean() мы вычисляем среднее значение по
столбцу, преобразованному в вектор NumPy;
используем функцию np.std() для вычисления стандартного откло-
нения по этому же столбцу. Этот показатель говорит о том, насколько
сильно значения в целом отстоят от среднего;
рассчитываем z-оценки по формуле (sales_np - mean_sales) / std_sales.
Эта операция выполняется поэлементно в вектризованном стиле;
выводим результаты на экран.
Z-оценка показывает, на сколько стандартных отклонений значение от-
стоит от среднего значения в ряду данных, и используется для стандарти-
зации данных, которая бывает полезна при сравнении значений в разных
наборах данных или определении выбросов. В нашем случае с помощью
z-оценок можно было бы найти необычно высокие или низкие значения
продаж в сравнении с общим распределением данных. Давайте рассмотрим
полный пример и выведем z-оценки на графике:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
Pandas, NumPy и Scikit-learn в действии 65
# Данные о продажах
data={
'SalesAmount': [100, 150, 200, 250, 300, 350, 400, 450, 500, 1000],
'ProductCategory': ['A', 'B', 'A', 'C', 'B', 'A', 'C', 'B', 'A', 'C']
}
df = pd.DataFrame(data)
# Преобразуем столбец SalesAmount в вектор NumPy
sales_np = df['SalesAmount'].to_numpy()
# Рассчитываем z-оценки для столбца SalesAmount с помощью NumPy
mean_sales = np.mean(sales_np)
std_sales = np.std(sales_np)
z_scores = (sales_np - mean _sales) / std_sales
# Определяем выбросы (z-оценка > 3 или < -3)
outliers = np.abs(z_scores) > 3
# Выводим результаты
print("Исходные данные:", sales_np)
print("Среднее значение:", mean_sales)
print("Стандартное отклонение:", std_sales)
print("Z-оценки:", z_scores)
print("Выбросы:", df[outliers])
# Визуализируем результаты
plt.figure(figsize=(12, 6))
plt.subplot(121)
plt.hist(sales_np, bins=10, edgecolor='black')
plt.title('Распределение исходных продаж')
plt.xlabel('SalesAmount')
plt.ylabel('Частота')
plt.subplot(122)
plt.scatter(range(len(sales_np)), z_scores)
plt.axhline(y=3, color='r', linestyle='--')
plt.axhline(y=-3, color='r', linestyle='--')
plt.title('Z-оценки продаж')
plt.xlabel('Точка данных')
plt.ylabel('Z-оценка')
plt.tight_layout()
plt.show()
Вывод:
Исходные данные: [ 100 150 200 250 300 350 400 450 500 1000]
Среднее значение: 370.0
Стандартное отклонение: 243.10491562286435
Z-оценки: [-1 .11063159 -0 .90495908 -0 .69928656 -0 .49361404 -0 .28794152 -0 .08226901
0.12340351 0.32907603 0.53474855 2.59147372]
Выбросы: Empty DataFrame
66 Введение. Основы и не только
Рис. 1.5 Распределение исходных данных и z-оценки
Здесь мы вычисляем z-оценки с помощью функций NumPy np.mean() и np.
std(). В разделе с визуализацией данных мы строим гистограмму с распре-
делением исходных данных, а также диаграмму рассеяния с z-оценками для
каждого наблюдения с горизонтальными линиями на отметках +3 и –3 для
идентификации выбросов.
1.3.4. Scikit-learn: эксперт в области машинного
обучения
После очистки и предварительной подготовки исходных данных мы зачастую
переходим к этапу построения моделей машинного обучения для выполне-
ния поставленных задач. Библиотека Scikit-learn содержит все необходимые
инструменты для работы практически с любыми алгоритмами машинного
обучения. Популярность этой библиотеки объясняется беспрецедентным
охватом методов для классификации, регрессии, кластеризации и сниже-
ния размерности, а также наличием полноценного арсенала механизмов для
подбора модели, оценки качества моделей и предварительной обработки
данных.
Библиотеку Scikit-learn от конкурентов выгодно отличает удобство исполь-
зования и единообразный API. Это позволяет аналитику данных спокойно
переключаться между разными алгоритмами и моделями без необходимости
Pandas, NumPy и Scikit-learn в действии 67
изучать новый синтаксис. Такая философия дает возможность быстро про-
верять гипотезы и ставить эксперименты в поисках нужной модели и гипер-
параметров для оптимального решения конкретной задачи.
Для иллюстрации мощи и гибкости библиотеки Scikit-learn воспользу-
емся ей в нашем сценарии с продажами. Для этого построим модель, пред-
сказывающую преодоление транзакциями заданного порога, а в качестве
предикторов будем использовать сумму продажи и скидку. Этот пример де-
монстрирует легкость, с которой библиотека Scikit-learn справляется с пре-
образованием сырых данных в осмысленные выводы.
Пример создания модели классификации с помощью библиотеки Scikit-
learn:
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
# Сгенерированные данные: транзакции с продажами
data={
'TransactionID': [101, 102, 103, 104, 105, 106, 107, 108, 109, 110],
'Store': ['A', 'B', 'A', 'C', 'B', 'A', 'C', 'B', 'A', 'C'],
'SalesAmount': [250, 120, 340, 400, 200, np.nan, 180, 300, 220, 150],
'Discount': [10, 15, 20, 25, 5, 12, np.nan, 18, 8, 22],
'Date': pd.to_datetime(['2023-01-01', '2023-01-02', '2023-01-03', '2023-01-04',
'2023-01-05', '2023-01-06', '2023-01-07', '2023-01-08',
'2023-01-09', '2023-01-10']),
'Category': ['Electronics', 'Clothing', 'Electronics', 'Home', 'Clothing',
'Home', 'Electronics', 'Home', 'Clothing', 'Electronics']
}
df = pd.DataFrame(data)
# Создаем столбец для целевой переменной со значением 1, если SalesAmount > 250, иначе 0
df['HighSales'] = (df['SalesAmount'] > 250).astype(int)
# Определяем предикторы и целевую переменную
X = df[['SalesAmount', 'Discount']]
y = df['HighSales']
# Разделяем данные на обучающую и тестовую выборки
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# Строим модель классификации методом случайного леса
clf = RandomForestClassifier(random_state=42)
clf.fit(X_train, y_train)
# Предсказываем исходы на тестовой выборке
y_pred = clf.predict(X_test)
# Выводим предсказания
print(y_pred)
Вывод:
[000]
68 Введение. Основы и не только
Что происходит в этом коде?
1. Импорт необходимых модулей и функций:
• нам понадобится функция train_test_split() из модуля sklearn.
model_selection для разделения данных на обучающую и тестовую
выборки;
• также нам нужен класс RandomForestClassifier из модуля sklearn.en-
semble с ансамблевыми моделями.
2. Создание столбца для целевой переменной:
• здесь мы создаем в датафрейме новый столбец с именем HighSales,
в котором будет стоять либо единица, если значение SalesAmount пре-
вышает 250, либо ноль.
3. Определение предикторов и целевой переменной:
• в датафрейм с предикторами мы отряжаем столбцы SalesAmount
и Discount;
• в качестве целевой переменной будем использовать столбец High-
Sales в виде объекта Series.
4. Разделение данных:
• здесь мы разбиваем исходные данные на обучающую выборку, со-
держащую 70 % наблюдений, и тестовую (30 % наблюдений).
5. Создание и обучение модели:
• создаем экземпляр класса RandomForestClassifier и выполняем обуче-
ние на первой группе наблюдений.
6. Предсказание:
• используем обученную модель для предсказания исходов на тесто-
вой выборке.
7. Вывод результатов:
• выводим на экран полученную информацию.
1.3.5. Почему Scikit-learn?
Как мы уже сказали, библиотека Scikit-learn предлагает очень богатый
и удобный API для работы, что позволяет легко экспериментировать с раз-
ными моделями и алгоритмами. Строите ли вы модель классификации, как
в показанном выше примере, или используете для решения поставленной
задачи регрессию, Scikit-learn значительно облегчит вам процесс разбиения
данных, создания и обучения модели, предсказания и проверки качества
полученной модели. Простота использования позволяет специалистам по
работе с данными сосредоточиться на более важных аспектах анализа и углу-
биться в детали настройки и реализации конкретной модели.
Одним из преимуществ библиотеки Scikit-learn является использование
универсального подхода к разным алгоритмам. Это значит, что вам доста-
точно научиться использовать и настраивать какой-то один метод модели-
Pandas, NumPy и Scikit-learn в действии 69
рования, и вы сможете применить полученные знания и навыки при при-
менении любого другого алгоритма. К примеру, для перехода от случайного
леса к методу опорных векторов или градиентному бустингу вам придется
внести лишь незначительные изменения в свой код, самое большое из кото-
рых будет касаться использования другого класса.
Кроме того, библиотека Scikit-learn имеет в своем арсенале широкий
спектр инструментов для выбора моделей и оценки их качества. В их число
входит метод перекрестной проверки (кросс-валидация), способ подбора
гиперпараметров по сетке значений, а также множество метрик для оценки
качества моделей. Такой богатый набор инструментов и механизмов по-
зволит с легкостью выбрать наиболее подходящую модель для решения по-
ставленной задачи.
Еще одним достоинством пакета Scikit-learn является его полная инте-
грация с библиотеками Pandas и NumPy. Таким образом, вы можете осу-
ществлять легкие и плавные переходы между этапами очистки данных, их
предварительной обработки и моделирования. Это позволит создавать эф-
фективные рабочие процессы и допускать меньше ошибок при переходах
между различными форматами.
1.3.6. Собираем все вместе: полный рабочий
процесс
Теперь, когда мы исследовали работу каждой библиотеки по отдельности,
пришло время объединить их в единый рабочий процесс. Представьте, что
перед вами стоит задача построить модель для предсказания транзакций
с высокими продажами, но вместе с тем вам нужно предварительно изба-
вить исходные данные от пропущенных значений, преобразовать признаки
и оценить качество итоговой модели. Этот сценарий вполне похож на реаль-
ный, ведь обычно нам требуется совмещать разные техники при решении
обычных рутинных задач.
На практике вы могли бы начать с использования библиотеки Pandas
для загрузки исходных данных и их очистки и предварительной обработ-
ки, включая замену пропущенных значений. Далее вы могли бы воспользо-
ваться библиотекой NumPy для выполнения расширенных математических
операций вроде расчета скользящих средних или обнаружения эффектов
взаимодействия между предикторами. В завершение вы могли бы прибег-
нуть к помощи библиотеки Scikit-learn с целью дополнительной обработки
данных (например, масштабирования числовых переменных), разбиения их
на обучающую и тестовую выборки, построения предсказательной модели
и проверки ее качества.
Интегрированный подход позволит по максимуму использовать сильные
стороны каждой библиотеки: Pandas – для манипулирования табличными
70 Введение. Основы и не только
данными, NumPy – для выполнения эффективных числовых преобразований,
а Scikit-learn – для реализации алгоритмов машинного обучения. Объединив
эти инструменты, вы сможете создать полноценный проект, который по-
зволит не только предсказывать большие продажи, но и выделять ключевые
факторы, лежащие в основе этих предсказаний.
Ниже показан пример, объединяющий в себе использование всех трех
библиотек:
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler
# Сгенерированные данные: транзакции с пропущенными значениями
data = {'TransactionID': [101, 102, 103, 104, 105],
'SalesAmount': [250, np.nan, 340, 400, 200],
'Discount': [10, 15, 20, np.nan, 5],
'Store': ['A', 'B', 'A', 'C', 'B']}
df = pd.DataFrame(data)
# Шаг 1: Обрабатываем пропущенные значения с помощью библиотек Pandas и Scikit-learn
imputer = SimpleImputer(strategy='mean')
df[['SalesAmount', 'Discount']] = imputer.fit_transform(df[['SalesAmount', 'Discount']])
# Шаг 2: Преобразовываем признаки с помощью библиотеки NumPy
df['LogSales'] = np.log(df['SalesAmount'])
# Шаг 3: Определяем целевую переменную модели
df['HighSales'] = (df['SalesAmount'] > 250).astype(int)
# Шаг 4: Разделяем данные на обучающую и тестовую выборки
X = df[['SalesAmount', 'Discount', 'LogSales']]
y = df['HighSales']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# Шаг 5: Строим модель и предсказываем исходы на тестовой выборке с помощью Scikit-learn
clf = RandomForestClassifier(random_state=42)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
print("Предсказания:", y_pred)
Вывод:
Предсказания: [1 0]
Этот код демонстрирует объединенный подход к построению рабочих
процессов с использованием библиотек Pandas, NumPy и Scikit-learn. Все
показанные здесь шаги мы уже разбирали по отдельности, так что не будем
останавливаться на этом подробно.
Pandas, NumPy и Scikit-learn в действии 71
1.3.7. Ключевые выводы
В этом разделе мы обсудили роли, отводящиеся библиотекам Pandas, NumPy
и Scikit-learn в анализе данных и машинном обучении. Эти три библиоте-
ки лежат в основе многих современных аналитических рабочих процессов,
и каждая из них вносит свой вклад в общее дело. Давайте повторим ключевые
особенности и предназначение этих библиотек.
1. Библиотека Pandas представляет собой незаменимый набор инстру-
ментов для манипулирования и очистки табличных данных. Ее арсенал
не ограничивается простыми инструментами для обработки данных,
а включает в себя множество функций и методов, позволяющих фильт-
ровать, группировать, агрегировать и преобразовывать исходные дан-
ные. С приобретением опыта в анализе данных вы заметите, что Pandas
будет играть все более значимую роль в ваших рабочих процессах. Эту
библиотеку можно использовать не только для обработки существую-
щих данных, но и для объединения разных наборов, создания новых
сложных признаков и многого другого. Удобный API и подробная до-
кументация помогут вам овладеть всеми инструментами из этой биб-
лиотеки на экспертном уровне.
2. Библиотека NumPy объединяет в себе множество инструментов для
выполнения эффективных матричных и векторных вычислений, спо-
собных значительно повысить скорость рабочих процессов, особенно
когда речь идет о работе с объемными наборами данных. Своим по-
тенциалом эта библиотека обязана реализованным в ней векторизо-
ванным операциям, позволяющим легко и быстро выполнять слож-
нейшие преобразования за счет обработки массивов данных целиком,
без необходимости применять традиционные циклы. С ростом слож-
ности и масштаба ваших проектов вы увидите, что начнете применять
векторизованные операции все чаще и практически уйдете от циклов
в Python, а в некоторых случаях будете применять их и вместо инстру-
ментов, реализованных в Pandas, с целью их оптимизации.
3. Библиотека Scikit-learn объединила в себе великое множество инстру-
ментов для построения моделей машинного обучения и оценки их
качества. Вклад этой библиотеки в экосистему анализа данных не-
возможно переоценить. Преимущества Scikit-learn состоят в удобстве
интерфейса и полном универсализме, позволяющем работать с абсо-
лютно разными алгоритмами и методами похожим образом. Помимо
функционала по созданию моделей машинного обучения, библиоте-
ка Scikit-learn имеет в своем арсенале широкий спектр инструментов
для выбора моделей, оценки их качества и подбора гиперпараметров.
А подробная документация и активное сообщество помогут вам в пол-
ной мере овладеть всеми возможностями этой библиотеки.
Научившись комбинировать эти библиотеки в своих проектах, вы сможете
создавать масштабные и универсальные аналитические рабочие процессы.
72 Введение. Основы и не только
1.4. Практические упражнения
Теперь, когда вы завершили чтение первой главы, вы можете попробовать
применить полученные знания на практике при решении упражнений. Каж-
дое упражнение содержит постановку задачи и одно из возможных решений.
Постарайтесь решать задачи самостоятельно, прежде чем смотреть предло-
женный вариант ответа.
Упражнение 1. Фильтрация и агрегация данных
при помощи Pandas
Есть набор данных о покупках в разных магазинах:
# Исходные данные
data = {'TransactionID': [101, 102, 103, 104, 105],
'Store': ['A', 'B', 'A', 'C', 'B'],
'PurchaseAmount': [250, 120, 340, 400, 200],
'Discount': [10, 15, 20, 25, 5]}
df = pd.DataFrame(data)
Ваша задача состоит в том, чтобы:
1) отфильтровать набор данных таких образом, чтобы в нем остались
только транзакции с суммой, превышающей 200;
2) сгруппировать транзакции по магазинам и вычислить среднюю и об-
щую сумму покупок за вычетом скидок для каждого магазина.
Решение
# Шаг 1: Отфильтровать набор данных, чтобы в нем остались только транзакции с суммой,
превышающей 200
filtered_df = df[df['PurchaseAmount'] > 200]
# Шаг 2: Сгруппировать транзакции по магазинам и вычислить среднюю и общую сумму покупок
для каждого магазина
df['NetPurchase'] = df['PurchaseAmount'] - df['Discount']
agg_purchases = df.groupby('Store').agg(
TotalPurchase=('NetPurchase', 'sum'),
AvgPurchase=('NetPurchase', 'mean')
)
print(filtered_df)
print(agg_purchases)
Вывод:
TransactionID Store PurchaseAmount Discount
0
101
A
250
10
2
103
A
340
20
3
104
C
400
25
Практические упражнения 73
TotalPurchase AvgPurchase
Store
A
560
280.0
B
300
150.0
C
375
375.0
Упражнение 2. Применение логарифмического
преобразования при помощи NumPy
Есть простой набор данных с суммами продаж:
# Исходные данные
sales = [100, 200, 50, 400, 300]
Ваша задача состоит в том, чтобы:
1) при помощи библиотеки NumPy применить операцию логарифмиче-
ского преобразования к этим данным;
2) вывести результат преобразования.
Решение
import numpy as np
# Шаг 1: Применяем операцию логарифмического преобразования
log_sales = np.log(sales)
print(log_sales)
Вывод:
[4.60517019 5.29831737 3.91202301 5.99146455 5.70378247]
Упражнение 3. Стандартизация данных о продажах
при помощи NumPy
Есть простой набор данных с суммами продаж:
# Исходные данные
sales = [100, 200, 50, 400, 300]
Ваша задача состоит в том, чтобы стандартизировать эти данные, вычислив
для каждого значения z-оценку с применением только библиотеки NumPy.
Решение
# Шаг 1: Рассчитываем среднее и стандартное отклонение
mean_sales = np.mean(sales)
std_sales = np.std(sales)
# Шаг 2: Вычисляем z-оценки
z_scores = (sales - mean _sales) / std_sales
print(z_scores)
74 Введение. Основы и не только
Вывод:
[-0 .85895569 -0 .07808688 -1 .2493901 1.48365074 0.70278193]
Упражнение 4. Построение модели классификации
с помощью Scikit-learn
Есть набор данных с транзакциями, в котором указаны магазины, сумма
продажи и сумма скидки:
# Исходные данные
data = {'TransactionID': [101, 102, 103, 104, 105],
'SalesAmount': [250, np.nan, 340, 400, 200],
'Discount': [10, 15, 20, np.nan, 5],
'Store': ['A', 'B', 'A', 'C', 'B']}
df = pd.DataFrame(data)
Ваша задача состоит в том, чтобы построить простую модель классифика-
ции для предсказания того, что транзакция относится к крупным продажам
(больше 250).
Что для этого нужно сделать?
1. Создать столбец HighSales для целевой переменной, значение которого
будет равно единице, если сумма продажи превышает $250, а иначе
нулю.
2. Воспользоваться библиотекой Scikit-learn для построения модели слу-
чайного леса, предсказывающей значение целевой переменной High-
Sales на основе значений предикторов SalesAmount и Discount.
3. Разделить набор данных на обучающую и тестовую выборки.
4. Обучить модель, предсказать исходы на тестовой выборке и вывести
результаты.
Решение
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
import pandas as pd
import numpy as np
# Шаг 1: Обрабатываем пропущенные значения
df['SalesAmount'] = df['SalesAmount'].fillna(df['SalesAmount'].mean())
df['Discount'] = df['Discount'].fillna(df['Discount'].mean())
# Шаг 2: Создаем столбец с именем HighSales
df['HighSales'] = (df['SalesAmount'] > 250).astype(int)
# Шаг 3: Определяем признаки и целевую переменную
X = df[['SalesAmount', 'Discount']]
y = df['HighSales']
Практические упражнения 75
# Шаг 4: Разделяем данные на обучающую и тестовую выборки
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# Шаг 5: Обучаем модель случайного леса
clf = RandomForestClassifier(random_state=42)
clf.fit(X_train, y_train)
# Шаг 6: Предсказываем исходы на тестовой выборке и выводим результаты
y_pred = clf.predict(X_test)
print("Предсказания:", y_pred)
Вывод:
Предсказания: [1 0]
Упражнение 5. Объединение библиотек Pandas, NumPy
и Scikit-learn в одном рабочем процессе
Есть набор данных с транзакциями, в котором указаны магазины, сумма
продажи и сумма скидки:
# Исходные данные
data = {'TransactionID': [101, 102, 103, 104, 105],
'SalesAmount': [250, np.nan, 340, 400, 200],
'Discount': [10, 15, 20, np.nan, 5],
'Store': ['A', 'B', 'A', 'C', 'B']}
df = pd.DataFrame(data)
Ваша задача состоит в том, чтобы:
1) обработать пропущенные значения в столбцах SalesAmount и Discount,
заменив их на средние значения;
2) применить логарифмическое преобразование к столбцу SalesAmount
с помощью библиотеки NumPy;
3) построить модель классификации с помощью библиотеки Scikit-learn
для предсказания того, что транзакция относится к крупным продажам
(HighSales);
4) разделить набор данных на обучающую и тестовую выборки;
5) обучить модель, предсказать исходы на тестовой выборке и вывести
результаты.
Решение
# Шаг 1: Обрабатываем пропущенные значения
df['SalesAmount'] = df['SalesAmount'].fillna(df['SalesAmount'].mean())
df['Discount'] = df['Discount'].fillna(df['Discount'].mean())
# Шаг 2: Применяем логарифмическое преобразование к столбцу SalesAmount
df['LogSales'] = np.log(df['SalesAmount'])
76 Введение. Основы и не только
# Шаг 3: Создаем столбец с именем HighSales
df['HighSales'] = (df['SalesAmount'] > 250).astype(int)
# Шаг 4: Определяем признаки и целевую переменную
X = df[['SalesAmount', 'Discount', 'LogSales']]
y = df['HighSales']
# Шаг 5: Разделяем данные на обучающую и тестовую выборки
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3,
random_state=42)
# Шаг 6: Обучаем модель случайного леса
clf = RandomForestClassifier(random_state=42)
clf.fit(X_train, y_train)
# Шаг 7: Предсказываем исходы на тестовой выборке и выводим результаты
y_pred = clf.predict(X_test)
print("Предсказания:", y_pred)
Вывод:
Предсказания: [1 0]
В этих практических упражнениях мы постарались вместить все основные
концепции, о которых говорили в первой главе книги. Вы попрактиковались
в фильтрации исходных данных, преобразовании признаков и построении
простых моделей машинного обучения. Представленные решения помогут
вам утвердиться во мнении, что вы все понимаете правильно. Будет хорошо,
если вы самостоятельно потренируетесь на других наборах данных, чтобы
закрепить полученные знания и навыки.
1.5. Возможные проблемы
На пути к новому уровню в области анализа данных и конструирования при-
знаков вас могут ждать определенные трудности и подводные камни. В то
же время они не всегда будут приводить к явным ошибкам, что затрудняет
процесс их обнаружения. В этом разделе мы перечислим распространенные
ловушки и подскажем обходные пути.
1.5.1. Неэффективное манипулирование данными
в Pandas
Хотя библиотека Pandas и предназначена для быстрого и удобного манипу-
лирования табличными данными, при существенном росте объемов скорость
обработки может падать, если вовремя не принять контрмеры.
Возможные проблемы 77
Что может пойти не так:
выполнение операций построчно вместо использования векторизо-
ванных операций, реализованных в библиотеке Pandas, может при-
вести к ощутимому замедлению работы;
использование множества копий датафрейма или помещение в память
огромных датафреймов без необходимости может привести к сниже-
нию быстродействия и излишнему расходованию памяти.
Решение
Всегда, когда это возможно, используйте векторизованные операции и из-
бегайте циклов по строкам датафрейма. При работе с большими наборами
данных рассмотрите возможность использования фреймворка Dask для мас-
штабирования выполняемых операций в Pandas или техник профилирова-
ния памяти для осуществления мониторинга ее состояния.
1.5.2. Неправильная обработка пропущенных
значений
Обработка пропущенных значений – довольно распространенная задача,
но если выполнять ее некорректно, это может негативно сказаться на ре-
зультатах анализа. Неправильная подстановка может привести к изменению
итоговых показателей, а такие ошибки бывает очень трудно обнаружить.
Что может пойти не так:
произвольная замена пропусков на нули или средние значения по
столбцу может приводить к появлению погрешности модели, особенно
если пропущенные значения представляют некую тенденцию;
при неспособности обнаружить шаблоны и закономерности в появ-
лении пропусков в данных (к примеру, можно ли считать пропуски
случайными или нет) сделанные выводы на основе данных могут не
соответствовать действительности.
Решение
Всегда старайтесь досконально понять природу возникновения пропущен-
ных значений и использовать подходящие техники для их замены. К при-
меру, техники прямого или обратного заполнения могут лучше подходить
для анализа временных рядов, а заполнение средними значениями или ме-
дианой – в ряде других сценариев. Для получения более точных подстановок
вы также можете воспользоваться методами машинного обучения на основе
других переменных, например методом k-ближайших соседей.
78 Введение. Основы и не только
1.5.3. Неправильное применение масштабирования
и преобразования признаков
К операции масштабирования признаков необходимо прибегать при ис-
пользовании большинства алгоритмов машинного обучения. В то же время
применение неподходящего метода или в неподходящее для этого время
может привести к неправильным предсказаниям модели.
Что может пойти не так:
масштабирование данных с использованием статистики на основе тес-
товой выборки, а не обучающей, может привести к так называемой
утечке информации (data leakage), когда модель во время обучения по-
лучает информацию из тестовых данных;
применение неподходящих типов преобразования (к примеру, лога-
рифмического преобразования или операции отрицания) может при-
вести к неправильным результатам работы модели или ошибкам.
Решение
Следите за тем, чтобы масштабирование применялось только к обучаю-
щим данным, а затем использовалось для преобразования тестовой выборки.
Выбирайте подходящие типы преобразования признаков. К примеру, если
в ваших данных содержатся отрицательные значения, вы можете воспользо-
ваться масштабированием с использованием минимума и максимума вместо
логарифмического преобразования.
1.5.4. Неправильное использование конвейеров
Scikit-learn
Применение конвейеров Scikit-learn может помочь автоматизировать про-
цесс предварительной обработки данных и построения модели. Однако при
неправильном использовании конвейеры могут приводить к появлению
ошибок и пропуску важных шагов обработки.
Что может пойти не так:
пропуск шагов при создании конвейеров (например, замены пропу-
щенных значений или масштабирования признаков) может привести
к тому, что обучение модели будет выполнено на неполных или необ-
работанных данных;
обучение модели с помощью конвейера на всем наборе данных до раз-
деления его на обучающую и тестовую выборки может привести к пере-
обучению (overfitting) или утечке информации.
Решение
Убедитесь, что все шаги по предварительной обработке данных включены
в конвейер и что конвейер запускается только на обучающей части исходных
Возможные проблемы 79
данных. Сцепляя вместе шаги в конвейере, вы можете предотвратить слу-
чайные пропуски важных этапов подготовки данных и обеспечить своему
рабочему процессу консистентность.
1.5.5. Неправильная интерпретация результатов
модели в Scikit-learn
При обучении моделей очень легко можно неправильно проинтерпрети-
ровать их результаты, особенно если вы плохо знакомы с метриками ка-
чества.
Что может пойти не так:
оценивать качество модели только по ее точности неправильно, в осо-
бенности если ваши исходные данные не сбалансированы. Модель с вы-
соким показателем точности предсказаний может плохо справляться
с прогнозированием исходов для классов с низкой представленностью;
слишком агрессивная стратегия подбора гиперпараметров или исполь-
зование сложных моделей без качественной проверки может приво-
дить к переобучению.
Решение
Всегда используйте комбинацию метрик качества модели, например точ-
ность, полноту, F1-меру и AUC-ROC, для оценки эффективности модели,
особенно в задачах, связанных с классификацией. Воспользуйтесь кросс-
валидацией, чтобы убедиться в достаточной обобщающей способности мо-
дели и отсутствии переобучения.
1.5.6. Узкие места в операциях NumPy
Библиотека NumPy позволяет эффективно выполнять операции, связанные
с математическими и векторными вычислениями, но неправильное ее ис-
пользование может привести к противоположному результату, особенно при
работе с большими объемами данных.
Что может пойти не так:
использование циклов в Python для применения преобразований
к массивам NumPy может приводить к ухудшению быстродействия;
недооценка пользы от применения вектризованных операций, реа-
лизованных в NumPy, может обернуться повышенным расходованием
памяти и замедлением работы.
Решение
Всегда, когда это возможно, используйте встроенные в библиотеку NumPy
функции для преобразования данных в векторизованном стиле. К примеру,
вместо того чтобы вызывать функцию логарифмирования в цикле, лучше
80 Введение. Основы и не только
будет применить векторизованную функцию np.log() для одновременной
обработки всех значений в столбце.
1.5.7. Избыточное конструирование признаков
Конструирование признаков способно значительно улучшить вашу модель,
но и здесь есть свои подводные камни. Дело в том, что создание слишком
большого количества новых признаков может привести к переобучению мо-
дели или ее чрезмерному усложнению.
Что может пойти не так:
создание избыточного количества признаков взаимодействия или по-
линомиальных переменных может привести к тому, что модель из-
лишне подгонится под обучающие данные, что снизит ее предсказа-
тельную эффективность на новых наблюдениях;
добавление в модель неподходящих признаков может чрезмерно
усложнить ее без прибавки в качестве, что может привести к увеличе-
нию времени обучения модели и ухудшению ее интерпретируемости.
Решение
Подходите к процессу конструирования и отбора признаков стратеги-
чески. Используйте различные техники, включая рекурсивное исключение
признаков, а также метрику важности признаков для определения перечня
переменных, которые помогут повысить качество модели и избавиться от ее
избыточного усложнения.
Всегда помните о перечисленных выше ловушках, которые поджидают вас
на каждом шагу, и следуйте данным советам, чтобы в них не угодить. Все эти
преграды вполне преодолимы, достаточно просто знать о них, а предупреж-
ден – значит вооружен.
Заключение
В этой главе мы заложили основы для вашего успешного продвижения
в аналитике данных и конструировании признаков. Начали мы с разговора
о необходимости перехода от базовых приемов манипулирования данными
к применению более передовых техник, требующих глубокого осмысления
рабочих процессов. На этом уровне недостаточно просто знать, какие функ-
ции можно использовать. Нужно понимать и уметь применять на практике
приемы по оптимизации рабочих процессов, особенно при работе с больши-
ми наборами данных.
Мы также рассмотрели три библиотеки, которые повсеместно и очень ак-
тивно используются при анализе данных: Pandas, NumPy и Scikit-learn. Не
будем снова перечислять все их достоинства, а скажем лишь, что библиотека
Заключение 81
Pandas применяется для манипулирования табличными данными, включая
их фильтрацию, агрегацию и группировку, Numpy незаменима при выполне-
нии числовых вычислений в векторизованном виде, а Scikit-learn идеально
справляется с некоторыми задачами предварительной обработки данных, но
главным образом используется для построения, обучения и проверки качест-
ва моделей машинного обучения, а также для создания удобных конвейеров
для последовательной обработки данных.
На протяжении всей главы мы подчеркивали важность совместного ис-
пользования этих библиотек. Именно в этом тройственном союзе кроется
залог успеха в создании оптимальных и эффективных рабочих процессов.
Завершили мы главу разделом с перечислением ожидающих вас на пути
проблем и подводных камней, которых можно миновать, если следовать
данным советам.
Итак, теперь вы готовы к следующему шагу и погружению в более сложные
приемы, связанные с анализом данных.
Глава 2
Оптимизация
потоков данных
При погружении в анализ данных одним из ключевых навыков можно на-
звать умение оптимизировать потоки данных. В современном мире, бази-
рующемся на данных, эффективность – более не роскошь, а предмет первой
необходимости. И при увеличении объемов данных, с которыми приходится
работать, на первый план выходит именно оптимизация.
Эта глава будет посвящена различным стратегиям и техникам, призван-
ным повысить эффективность и масштабируемость ваших процессов обра-
ботки данных. Мы рассмотрим более сложные методологии для преобразо-
вания, агрегирования и фильтрации данных с помощью Pandas. Кроме того,
обсудим техники очистки и структурирования данных, фактически ставшие
стандартом в индустрии.
Освоив эти навыки, вы будете готовы к решению сложных задач, свя-
занных с конструированием и отбором признаков для моделей машинного
обучения. И начнем мы с раздела, посвященного более сложному анализу
данных при помощи библиотеки Pandas.
2.1. Расширенное манипулирование
данными с Pandas
При работе с большими наборами данных вам будет недостаточно базовых
техник из библиотеки Pandas, а нужно будет подключать тяжелую артилле-
рию в виде перечисленных ниже методик.
Сложная фильтрация и извлечение подмножеств
Эта техника включает в себя применение множественных условий на разные
столбцы с целью извлечения нужных подмножеств данных. При этом она не
ограничивается одними лишь базовыми логическими операторами AND, OR
Расширенное манипулирование данными с Pandas 83
и NOT. К примеру, с помощью расширенной фильтрации вы можете получить
из набора данных транзакции только по определенным магазинам за ука-
занный временной интервал, сумма в которых превышает заданный порог.
Более того, расширенная фильтрация зачастую включает использование
регулярных выражений для поиска шаблонов в тексте. Это бывает особенно
удобно при работе с текстовыми данными для обнаружения определенных
последовательностей символов и их комбинаций. К примеру, вы можете вос-
пользоваться регулярными выражениями для поиска товаров с нужными вам
шаблонами в наименованиях или отзывов от покупателей определенного
типа.
При работе с данными, содержащими временную составляющую, бывает
очень полезно использовать специальные фильтры для временных рядов.
Такие фильтры позволяют выполнять специфические срезы на основе кален-
дарных критериев, таких как диапазоны дат или временных отрезков, дни
недели, кварталы и т. д. К примеру, при анализе финансов вам может понадо-
биться оставить в выборке только информацию за торговые дни с объемами,
превышающими определенный предел.
Освоение техник расширенной фильтрации позволит вам глубже погру-
зиться в данные и извлечь выводы, недоступные для простых критериев
отбора.
Множественная группировка и агрегация
Расширенные техники манипулирования данными открывают вам дорогу
к операциям, связанным с иерархической группировкой данных, с помощью
которых можно осуществлять анализ сразу по нескольким измерениям, что
бывает исключительно полезно.
К примеру, в розничных продажах вам может понадобиться сгруппировать
данные сначала по магазину, затем по категории товаров и только потом по
дате. Многоуровневый подход позволяет анализировать данные с разной
гранулярностью. Допустим, вы можете легко найти самые активно прода-
ваемые категории товаров в разрезе магазинов. К сгруппированным таким
образом данным можно применять различные функции агрегирования для
извлечения важных бизнес-выводов.
Кроме того, множественная группировка может оказаться полезной при ра-
боте с наборами данных, обладающими естественными иерархиями, такими
как географические данные (страны, штаты, города) или организационные
структуры (отделы, команды, сотрудники). Она позволяет сворачивать и раз-
ворачивать анализ по этим иерархиям, что обеспечивает гибкость отчетности.
В библиотеке Pandas этот функционал реализован с помощью богатых на
параметры методов groupby() и agg(), позволяющих задавать множественные
группировки и агрегации.
Сводные таблицы и изменение формы данных
Техники из этого раздела позволяют динамически менять структуру данных,
преобразовывая их из длинного формата в широкий и наоборот, что облег-
84 Оптимизация потоков данных
чает выполнение определенных видов анализа и построение визуализаций.
Разворачивание данных бывает полезно при необходимости их реорганиза-
ции для создания сводных таблиц или подготовки к специфическому анали-
зу. К примеру, при анализе розничных продаж вы можете быстро построить
сводную таблицу с магазинами в строках, товарами в столбцах и суммами
продаж в ячейках.
Функция melt(), наоборот, используется для приведения широких данных
в длинный формат. К примеру, если в вашем наборе данных данные о про-
дажах по годам располагаются в отдельных столбцах, вы можете с помощью
этой функции привести его к табличному виду с одним столбцом с указанием
года, а вторым – с суммой продаж.
Эффективный анализ временных рядов
Приемы работы с временными рядами активно используются в сферах, свя-
занных с финансами, экономикой и наукой об окружающей среде. К таким
приемам, о которых мы будем подробно говорить далее в этой книге, можно
отнести следующие техники.
1.
Передискретизация. Эта техника применяется для изменения грану-
лярности данных во временных рядах. К примеру, вам может понадо-
биться преобразовать данные с ежедневными транзакциями в месяч-
ные итоги или агрегировать данные по продажам с определенными
интервалами. Библиотека Pandas предоставляет все необходимые
функции для подобных преобразований.
2. Скользящие окна. Прием со скользящими окнами часто применяется
для анализа трендов и шаблонов во временных рядах. Вы узнаете спосо-
бы вычисления скользящих средних, стандартных отклонений и других
статистических показателей применительно к заданной ширине окна.
С помощью скользящих окон можно добиться сглаживания краткосроч-
ных колебаний и выявления долгосрочных тенденций в данных.
3. Работа с часовыми поясами и данными разной гранулярности.
В современном мире с его глобализацией работа с часовыми поясами
во временных рядах часто выходит на первый план. Вы узнаете о спо-
собах преобразования часовых поясов, о работе с данными из разных
источников и обработке перевода часов на летнее/зимнее время. Кро-
ме того, вы научитесь работать с данными разной гранулярности, на-
пример путем комбинирования дневных и месячных данных в одном
наборе.
4. Индексация и отбор данных на основе времени. В библиотеке Pan-
das содержатся все необходимые инструменты для выполнения ин-
дексации и отбора данных с использованием диапазонов дат, а также
построения сложных запросов на основе времени.
5. Обработка пропущенных значений во временных рядах. Времен-
ные ряды часто содержат пропущенные значения. Вы познакомитесь
с техниками поиска, заполнения и интерполяции пропущенных зна-
чений с целью поддержания целостности данных во временных рядах.
Расширенное манипулирование данными с Pandas 85
Оптимизация в плане производительности и расхода памяти
При росте объемов данных, с которыми вам приходится работать, возрас-
тает и необходимость в оптимизации кода в отношении его быстродействия
и адекватного использования ресурсов памяти. Вы узнаете о способах эко-
номии памяти при помощи использования оптимальных типов данных,
разбиения исходных наборов на секции и применения итераторов для об-
работки данных небольшими фрагментами. Кроме того, вы научитесь ис-
пользовать векторизованные операции, позволяющие значительно ускорить
вычисления, а также опираться на встроенные инструменты оптимизации,
присутствующие в Pandas.
В этом разделе мы также поговорим о методах параллельной обработки
данных, позволяющей задействовать несколько ядер процессора для мани-
пуляции данными. Вы также узнаете о том, как можно задействовать библио-
теки вроде Dask и Vaex для работы с массивами данных, не помещающимися
в оперативную память.
2.1.1. Сложная фильтрация и извлечение
подмножеств
При работе с табличными данными вам нередко бывает необходимо отфильт-
ровать информацию по нескольким столбцам. Помимо очевидных целей,
расширенная фильтрация может применяться в процессе валидации данных.
Аналитик может создать собственные правила проверки для отслеживания
нужных атрибутов. Это бывает особенно полезно при работе с независимы-
ми полями данных или при проверке данных на соответствие существую-
щим бизнес-правилам. К примеру, в медицинских наборах данных составные
фильтры могут использоваться для проверки карточек пациентов с исполь-
зованием различных медицинских параметров.
Польза расширенной фильтрации данных также состоит в удобстве прове-
дения разведочного анализа данных. Изолировав нужную область исходного
набора данных в соответствии с выбранными критериями, аналитик может
глубже погрузиться в распределение данных и тренды, незаметные в общей
массе.
Очистка данных и проверка качества
Давайте рассмотрим пример с набором данных, содержащим заказы покупа-
телей, в котором нам необходимо обнаружить и избавиться от потенциально
ошибочных значений:
import pandas as pd
import numpy as np
# Сгенерированные данные
data={
86 Оптимизация потоков данных
'OrderID': [1001, 1002, 1003, 1004, 1005],
'CustomerID': ['C001', 'C002', 'C003', 'C004', 'C005'],
'OrderDate': ['2023-01-15', '2023-01-16', '2023-01-17', '2023-01-18', '2023-01-19'],
'TotalAmount': [100.50, 200.75, -50.00, 1000000.00, 150.25],
'Status': ['Completed', 'Pending', 'Completed', 'Shipped', 'Invalid']
}
df = pd.DataFrame(data)
# Приведем поле OrderDate к типу datetime
df['OrderDate'] = pd.to_datetime(df['OrderDate'])
# Отфильтруем заказы с отрицательными и аномально высокими суммами
valid_orders = df[(df['TotalAmount'] > 0) & (df['TotalAmount'] < 10000)]
# Отберем заказы с некорректными статусами
invalid_status = df[~df['Status'].isin(['Completed', 'Pending', 'Shipped'])]
print("Действительные заказы:")
print(valid_orders)
print("\nЗаказы с некорректным статусом:")
print(invalid_status)
# Очистим данные путем удаления ошибочных записей и переустановки индекса
cleaned_df = df[(df['TotalAmount'] > 0) & (df['TotalAmount'] < 10000) &
(df['Status'].isin(['Completed', 'Pending', 'Shipped']))].reset_
index(drop=True)
print("\nОчищенный набор данных:")
print(cleaned_df)
Вывод:
Действительные заказы:
OrderID CustomerID OrderDate TotalAmount
Status
0
1001
C001 2023-01-15
100.50 Completed
1
1002
C002 2023-01-16
200.75 Pending
4
1005
C005 2023-01-19
150.25 Invalid
Заказы с некорректным статусом:
OrderID CustomerID OrderDate TotalAmount Status
4
1005
C005 2023-01-19
150.25 Invalid
Очищенный набор данных:
OrderID CustomerID OrderDate TotalAmount
Status
0
1001
C001 2023-01-15
100.50 Completed
1
1002
C002 2023-01-16
200.75 Pending
После импорта необходимых библиотек и создания набора данных мы
приводим данные в столбце OrderDate к типу datetime для комфортной работы
с датами в дальнейшем. После этого избавляемся от заказов с отрицательны-
ми и аномально высокими суммами (выше $10 000). Для получения заказов
с некорректными статусами мы сравниваем значение в поле Status со спис-
ком допустимых статусов. В результате применяем оба фильтра к нашему
набору данных и переустанавливаем индекс для датафрейма.
Расширенное манипулирование данными с Pandas 87
Это простой пример, демонстрирующий применение множественной
фильтрации к набору данных. Здесь мы объединили проверку числового
столбца на интервал значений с проверкой категориального столбца на
вхождение в список.
Целевой и гранулярный анализ
Выбирая нужные подмножества данных, аналитик может проводить иссле-
дования только на них, не затрагивая при этом все исходные данные. Такой
гранулированный подход к анализу позволяет делать точечные выводы об
интересующих аналитика аспектах, таких как покупательское поведение
потребителей в конкретном регионе или эффективность продажи опреде-
ленной категории товаров на конкретном рынке.
Расширенная фильтрация способствует проведению долговременных ис-
следований по определенным когортам признаков. Это бывает особенно
ценно при анализе поведения клиентов и прогнозировании их оттока.
Кроме того, применение нескольких фильтров играет важную роль при по-
иске аномалий и обнаружении мошеннических транзакций. Подобрав нуж-
ный набор условий, аналитик может выявить подозрительные операции, по
своему шаблону отличающиеся от других.
Давайте рассмотрим пример с набором данных с покупками, в котором мы
будем анализировать интересующий нас сегмент покупателей:
import pandas as pd
import numpy as np
# Простой набор данных
data={
'CustomerID': ['C001', 'C002', 'C003', 'C004', 'C005', 'C001', 'C002', 'C003'],
'Age': [25, 35, 45, 30, 50, 25, 35, 45],
'Gender': ['M', 'F', 'M', 'F', 'M', 'M', 'F', 'M'],
'ProductCategory': ['Electronics', 'Clothing', 'Home', 'Beauty', 'Sports',
'Clothing', 'Electronics', 'Beauty'],
'PurchaseAmount': [500, 150, 300, 200, 450, 200, 600, 100]
}
df = pd.DataFrame(data)
# Целевой анализ: женщины в диапазоне от 30 до 40 лет, покупавшие товары из категорий
Electronics или Clothing
target_segment = df[
(df['Gender'] == 'F') &
(df['Age'].between(30, 40)) &
(df['ProductCategory'].isin(['Electronics', 'Clothing']))
]
# Рассчитываем средние продажи для выделенного сегмента
avg_purchase = target_segment['PurchaseAmount'].mean()
# Находим в выбранном сегменте самую популярную категорию товаров
popular_category = target_segment['ProductCategory'].mode().values[0]
88 Оптимизация потоков данных
print("Анализ целевого сегмента:")
print(f"Средняя сумма покупки: ${avg_purchase:.2f}")
print(f"Самая популярная категория: {popular_category}")
# Сравниваем со средней суммой покупки во всем наборе данных
overall_avg = df['PurchaseAmount'].mean()
print(f"\nОбщая средняя сумма покупки: ${overall_avg:.2f}")
print(f"Разница: ${avg_purchase - overall_avg:.2f}")
Вывод:
Анализ целевого сегмента:
Средняя сумма покупки: $375.00
Самая популярная категория: Clothing
Общая средняя сумма покупки: $312.50
Разница: $62.50
После импорта библиотек и создания набора данных мы с помощью мно-
жественного фильтра определяем целевой сегмент, в который входят женщи-
ны в диапазоне от 30 до 40 лет, покупавшие товары из категорий Electronics
или Clothing. Далее мы применяем метод mean() для расчета средней суммы
покупки в этом сегменте. Самую популярную категорию товаров мы опреде-
ляем при помощи метода mode(). В завершение сравниваем среднюю сумму
покупки в сегменте и во всем наборе данных.
Этот пример показывает, как можно проводить углубленную аналитику
в выбранном сегменте данных.
Проверка гипотез и статистических показателей
Расширенная фильтрация также позволяет выполнять устойчивые статис-
тические тесты и проверять различные гипотезы. Эта методика позволяет
аналитикам тщательно подбирать подмножества данных, отвечающие опре-
деленным критериям, тем самым обеспечивая достоверность и надежность
статистических сравнений.
К примеру, при проведении A/B-тестирования с помощью расширенной
фильтрации можно выделить отдельные сегменты потребителей на основе
множества атрибутов, таких как место проживания, потребительское пове-
дение, частота покупок и т. д. Такой гранулярный подход позволит убедиться
в том, что сравнение версий предлагаемого продукта или разных маркетин-
говых кампаний выполняется на сопоставимых группах, что значительно
повышает ценность анализа.
В клинических исследованиях расширенная фильтрация может приме-
няться для разделения пациентов на контрольные группы по множеству
признаков, таких как возраст, история болезни, генетические предраспо-
ложенности и пр. Тщательный выбор групп позволяет повысить качество
исследований.
Давайте рассмотрим пример сравнения эффективности двух маркетин-
говых стратегий путем анализа вовлеченности потребителя, выраженной
Расширенное манипулирование данными с Pandas 89
в виде отношения числа переходов по ссылке к числу ее показов (показатель
ClickThrough):
import pandas as pd
import numpy as np
from scipy import stats
# Простой набор данных
np.random.seed(42)
data={
'Strategy': ['A'] * 1000 + ['B'] * 1000,
'ClickThrough': np.concatenate([
np.random.normal(0.05, 0.02, 1000), # Стратегия A
np.random.normal(0.06, 0.02, 1000) # Стратегия B
])
}
df = pd.DataFrame(data)
# Разделим данные для каждой стратегии
strategy_a = df[df['Strategy'] == 'A']['ClickThrough']
strategy_b = df[df['Strategy'] == 'B']['ClickThrough']
# Выполним t-тест
t_statistic, p_value = stats.ttest_ind(strategy_a, strategy_b)
print(f"T-статистика: {t_statistic}")
print(f"P-значение: {p_value}")
# Интерпретируем полученные результаты
alpha = 0.05
if p_value < alpha:
print("Отклоняем нулевую гипотезу. Между исследуемыми стратегиями есть статистически
значимые различия.")
else:
print("Не может отклонить нулевую гипотезу. Между исследуемыми стратегиями отсутствуют
статистически значимые различия.")
Вывод:
T-статистика: -12.477026028717436
P-значение: 1.849216934841512e-34
Отклоняем нулевую гипотезу. Между исследуемыми стратегиями есть статистически значимые
различия.
Здесь мы, помимо pandas, загрузили также библиотеки numpy и scipy для
работы с числами и выполнения статистического теста. Далее мы создали
простой набор данных с двумя маркетинговыми стратегиями A и B и запол-
нили для них показатели отношения числа переходов по ссылке к числу пока-
зов на основе нормального распределения с разными средними значениями.
После этого с помощью булевой индексации мы разделили данные по каждой
стратегии, выполнили t-тест с помощью функции scipy.stats.ttest_ind() д л я
сравнения средних значений двух групп, извлекли t-статистику и p-значение
90 Оптимизация потоков данных
и вывели их на экран. В завершение мы проинтерпретировали полученные
результаты путем сравнения p-значения с уровнем значимости, выставлен-
ным на границе 0.05. Если наше p-значение ниже этого значения, значит,
мы можем отклонить нулевую гипотезу о том, что наши группы не имеют
статистически значимых различий.
Этот пример показывает, как расширенная фильтрация для разделения
данных по стратегиям может быть использована совместно со статисти-
ческим тестом для проверки гипотезы о присутствии различий в группах.
Подобный анализ помогает в принятии решений в самых разных областях,
включая маркетинг, разработку новой продукции и научные исследования.
Оптимизация производительности и эффективная
обработка данных
Работая с небольшими подмножествами данных, отобранными при помощи
расширенной фильтрации, вы можете значительно повысить быстродей-
ствие используемых алгоритмов анализа. И это особенно важно при работе
с большими исходными наборами данных.
Отбор нужных данных для анализа поможет вам не только сэкономить
память, но и уменьшить объем передаваемого по сети трафика и накладных
расходов, если фильтрация производится на стороне базы данных. Это бы-
вает критически важно в распределенных системах, в которых узким местом
зачастую является именно сетевой трафик.
В приложениях, связанных с машинным обучением, расширенная фильт-
рация играет ключевую роль в деле отбора признаков и снижения размер-
ности данных. Идентификация и извлечение наиболее значимых признаков
и наблюдений может положительно сказаться на качестве будущих моделей,
времени их обучения и обобщающей способности. Особенно важно это бы-
вает при работе с наборами данных с большим количеством измерений, не-
редко страдающими от так называемого проклятия размерности.
Ниже показан простой пример эффективного расходования ресурсов при
помощи расширенной фильтрации исходных данных:
import pandas as pd
import numpy as np
import time
# Создаем большой набор данных
n_rows = 10000000
df = pd.DataFrame({
'id': range(n_rows),
'category': np.random.choice(['A', 'B', 'C'], n_rows),
'value': np.random.randn(n_rows)
})
# Создаем функцию со сложными математическими вычислениями
def complex_operation(x):
return np.sin(x) * np.cos(x) * np.tan(x)
Расширенное манипулирование данными с Pandas 91
# Замеряем время без фильтрации данных
start_time = time.time()
result_without_filter = df['value'].apply(complex_operation).sum()
time_without_filter = time.time() - start_time
# Применяем расширенную фильтрацию
filtered_df = df[(df['category'] == 'A') & (df['value'] > 0)]
# Замеряем время с фильтрацией данных
start_time = time.time()
result_with_filter = filtered_df['value'].apply(complex_operation).sum()
time_with_filter = time.time() - start_time
print(f"Время без фильтрации данных: {time_without_filter:.2f} с.")
print(f"Время с фильтрацией данных: {time_with_filter:.2f} с.")
print(f"Разница: {time_without_filter / time_with_filter:.2f}x")
Вывод:
Время без фильтрации данных: 12.87 с.
Время с фильтрацией данных: 2.17 с.
Разница: 5.93x
Здесь мы дополнительно импортировали библиотеку time для замера вре-
мени выполнения функции. Далее мы создали датафрейм со столбцами id,
category и value, состоящий из 10 млн строк. После этого объявили функцию
complex_operation(), выполняющую некие тригонометрические преобразо-
вания, требующие времени. Сначала мы вызвали эту функцию со всем ис-
ходным набором данных и замерили время выполнения. Затем мы оставили
в выборке только категорию A и положительные значения в столбце value
и снова замерили время запуска функции. В завершение сравнили полу-
ченные времена.
Как видите, на моем ноутбуке время выполнения функции сократилось
примерно в шесть раз.
Еще один пример фильтрации с множественными условиями
Скажем, у нас есть набор данных с информацией о розничных продажах,
и нам необходимо оставить в выборке продажи по магазинам A и C с суммой,
превышающей $100. Также исключим из набора транзакции со скидкой ниже
10 % и оставим только продажи по категориям Electronics и Clothing:
import pandas as pd
import numpy as np
# Создаем более сложный набор данных
np.random.seed(42)
data={
'TransactionID': range(1001, 1021),
'Store': np.random.choice(['A', 'B', 'C'], 20),
'SalesAmount': np.random.randint(50, 500, 20),
'Discount': np.random.randint(0, 30, 20),
92 Оптимизация потоков данных
'Category': np.random.choice(['Electronics', 'Clothing', 'Home', 'Food'], 20),
'Date': pd.date_range(start='2023-01-01', periods=20)
}
df = pd.DataFrame(data)
# Выводим исходный набор данных
print("Исходный набор данных:")
print(df)
print("\n")
# Накладываем множественный фильтр
filtered_df = df[
(df['Store'].isin(['A', 'C'])) &
(df['SalesAmount'] > 100) &
(df['Discount'] > 10) &
(df['Category'].isin(['Electronics', 'Clothing']))
]
print("Отфильтрованный набор данных:")
print(filtered_df)
print("\n")
# Дополнительный анализ на отфильтрованных данных
print("Общая статистика на отфильтрованных данных:")
print(filtered_df .describe())
print("\n")
print("Средние суммы продажи в разрезе категорий:")
print(filtered_df .groupby('Category')['SalesAmount'].mean())
print("\n")
print("Общие суммы продажи в разрезе дат:")
print(filtered_df .groupby('Date')['SalesAmount'].sum())
Вывод:
Исходный набор данных:
TransactionID Store SalesAmount Discount
Category
Date
0
1001
C
241
22
Clothing 2023-01-01
1
1002
A
493
19
Food 2023-01-02
2
1003
C
326
24
Food 2023-01-03
3
1004
C
210
2
Home 2023-01-04
...
17
1018
B
495
8
Clothing 2023-01-18
18
1019
B
100
25
Clothing 2023-01-19
19
1020
B
413
20
Clothing 2023-01-20
Отфильтрованный набор данных:
TransactionID Store SalesAmount Discount
Category
Date
0
1001
C
241
22
Clothing 2023-01-01
12
1013
A
237
24 Electronics 2023-01-13
Общая статистика на отфильтрованных данных:
TransactionID SalesAmount Discount
Date
count
2.000000
2.000000 2.000000
2
mean
1007.000000 239.000000 23.000000 2023-01-07 00:00:00
Расширенное манипулирование данными с Pandas 93
min
1001.000000 237.000000 22.000000 2023-01-01 00:00:00
25%
1004.000000 238.000000 22.500000 2023-01-04 00:00:00
50%
1007.000000 239.000000 23.000000 2023-01-07 00:00:00
75%
1010.000000 240.000000 23.500000 2023-01-10 00:00:00
max
1013.000000 241.000000 24.000000 2023-01-13 00:00:00
std
8.485281
2.828427 1.414214
NaN
Средние суммы продажи в разрезе категорий:
Category
Clothing
241.0
Electronics 237.0
Name: SalesAmount, dtype: float64
Общие суммы продажи в разрезе дат:
Date
2023-01-01 241
2023-01-13 237
Name: SalesAmount, dtype: int64
Здесь для вас ничего особенно нового и нет. Обратите внимание, как мы
сгенерировали набор данных. В столбец TransactionID мы поместили 20 по-
следовательных чисел из диапазона, в столбец Store – случайный выбор из
трех магазинов, в столбцы SalesAmount и Discount – случайные целые числа
в диапазоне от 50 до 500 и от 0 до 30 соответственно, в столбец Category –
случайный выбор из четырех возможных категорий, а в столбец Date – 20 по-
следовательных дат начиная с 1 января 2023 года.
Метод describe() позволяет вывести общую статистику по набору данных,
а методы groupby(), mean() и sum() мы используем для группировки данных
и расчета нужных нам агрегаций.
2.1.2. Многоуровневая группировка с агрегацией
Часто бывает необходимо в процессе анализа группировать данные сразу по
нескольким атрибутам, тем самым создавая иерархию агрегаций. Это помо-
гает анализировать данные на разных уровнях и с разной гранулярностью.
К примеру, если сгруппировать данные по продажам сначала по региону,
а затем по категории товаров, вы сможете ответить на следующие вопросы:
«Каковы общие суммы продаж по конкретной категории товаров в разрезе
магазинов?» и «Какая категория товаров лучше продается в каждом из ре-
гионов?».
Но многоуровневая группировка не ограничивается только двумя столб-
цами. Вы легко можете расширить эту концепцию, добавив в группировку
временные интервалы, сегменты покупателей и другие важные атрибуты.
При работе с иерархическими данными очень важно учитывать порядок
расположения группировок, поскольку именно он определяет структуру по-
лученных результатов и выводы, которые вы сможете сделать из анализа.
Давайте расширим наш предыдущий пример и посмотрим, как реализу-
ется многоуровневая группировка и агрегация в Pandas:
94 Оптимизация потоков данных
import pandas as pd
import numpy as np
# Набор данных
np.random.seed(42)
data={
'TransactionID': range(1001, 1021),
'Store': np.random.choice(['A', 'B', 'C'], 20),
'Category': np.random.choice(['Electronics', 'Clothing', 'Home', 'Food'], 20),
'SalesAmount': np.random.randint(50, 500, 20),
'Discount': np.random.randint(0, 30, 20),
'Date': pd.date_range(start='2023-01-20', periods=20)
}
df = pd.DataFrame(data)
# Выводим исходный набор данных
print("Исходный набор данных:")
print(df.head())
print("\n")
# Группируем продажи по столбцам Store и Category и вычисляем разные типы агрегаций
grouped_df = df.groupby(['Store', 'Category']).agg({
'SalesAmount': ['sum', 'mean', 'count'],
'Discount': ['mean', 'max']
}).reset_index()
# Собираем имена столбцов
grouped_df.columns = ['_' .join(col).strip() for col in grouped_df .columns.values]
print("Сгруппированный набор данных:")
print(grouped_df)
print("\n")
# Сводная таблица для вывода данных по столбцам Store и Category
pivot_df = pd.pivot_table(df, values='SalesAmount', index='Store', columns='Category',
aggfunc='sum', fill_value=0)
print("Сводная таблица. Продажи по магазинам и категориям:")
print(pivot_df)
print("\n")
# Анализ временных рядов
df['Date'] = pd.to_datetime(df['Date'])
df.set_index('Date', inplace=True)
monthly_sales = df.resample('ME')['SalesAmount'].sum()
print("Продажи по месяцам:")
print(monthly_sales)
print("\n")
# Расширенная фильтрация
high_value_transactions = df[
(df['SalesAmount'] > df['SalesAmount'].mean()) &
(df['Discount'] < df['Discount'].mean())
]
print("Крупные продажи (сумма выше среднего, скидка ниже среднего):")
print(high_value_transactions)
Расширенное манипулирование данными с Pandas 95
Вывод:
Исходный набор данных:
TransactionID Store
Category SalesAmount Discount
Date
0
1001
C
Food
239
17 2023-01-20
1
1002
A
Food
495
25 2023-01-21
2
1003
C Electronics
224
8 2023-01-22
3
1004
C Electronics
495
25 2023-01-23
4
1005
A
Food
100
20 2023-01-24
...
Сгруппированный набор данных:
Store_
Category_ SalesAmount_sum SalesAmount_mean SalesAmount_count \
0
A
Clothing
413
413.000000
1
1
A
Food
918
306.000000
3
2
A
Home
70
70.000000
1
...
8
C Electronics
1255
313.750000
4
9
C
Food
608
304.000000
2
10
C
Home
562
281.000000
2
Discount_mean Discount_max
0
1.000000
1
1
15.666667
25
2
28.000000
28
...
8
16.500000
27
9
15.500000
17
10
9.000000
11
Сводная таблица. Продажи по магазинам и категориям:
Category Clothing Electronics Food Home
Store
A
413
091870
B
216
293 940 63
C
104
1255 608 562
Продажи по месяцам:
Date
2023-01-31 3452
2023-02-28 1990
Freq: ME, Name: SalesAmount, dtype: int64
Крупные продажи (сумма выше среднего, скидка ниже среднего):
TransactionID Store
Category SalesAmount Discount
Date
2023-01-25
1006
A
Clothing
413
1
2023-01-28
1009
C
Food
369
14
2023-01-30
1011
C Electronics
356
6
2023-02-02
1014
C
Home
378
7
2023-02-04
1016
A
Food
323
2
2023-02-05
1017
B
Food
437
13
2023-02-07
1019
B
Food
365
3
96 Оптимизация потоков данных
Набор данных мы сгенерировали так же, как в предыдущих примерах,
но на этот раз начальную дату перенесли на 20 января, чтобы захватить
два календарных месяца. Многоуровневую группировку мы выполнили
при помощи инструкции df.groupby(['Store', 'Category']). Также в рамках
этой группировки мы рассчитали разные агрегации по полям SalesAmount
и Discount и перестроили индекс. Затем мы поправили имена столбцов, объ-
единив кортежи, полученные в результате агрегаций, с помощью символа
подчеркивания. В результате, к примеру, из кортежа ('SalesAmount', 'sum')
мы получили имя столбца SalesAmount_sum. Далее мы с помощью функции
pd.pivot_table() создали сводную, или перекрестную, таблицу, разместив
в строках значения из поля Store, а в столбцах – из Category. Аргумент fill_
value=0 гарантирует, что значения для всех пропущенных комбинаций этих
полей будут нулевыми. После этого мы преобразовали столбец Date к типу
данных datetime и установили его в качестве индекса в датафрейме. С по-
мощью метода df.resample('ME') мы сгруппировали (передискретизировали)
наши данные по месяцам, после чего для каждого месяца вычислили общую
сумму продаж. В заключение мы применили множественный фильтр для вы-
бора транзакций с большими продажами, используя для этого два условия.
На этом простом примере мы рассмотрели сразу несколько важных кон-
цепций, применяемых в Pandas, а именно:
многоуровневая группировка с множественной агрегацией;
создание сводных таблиц для выполнения перекрестного анализа (мы
снова затронем эту тему в следующем разделе);
передискретизация временных рядов для анализа по заданным ин-
тервалам;
расширенная фильтрация с несколькими условиями.
Все эти техники очень активно используются при анализе реальных на-
боров данных.
2.1.3. Сводные таблицы и изменение структуры
данных
Для преобразования структуры данных в Pandas в основном используются
методы pivot(), pivot_table() и melt().
Метод pivot() бывает удобно использовать при необходимости просто
преобразовать уникальные значения в колонке в разные столбцы. К примеру,
если у вас есть набор данных с отдельными столбцами с датами, товарами
и суммами продаж, вы можете воспользоваться этим методом для создания
новой таблицы, в которой каждый товар будет представлен отдельным столб-
цом, а в значениях таблицы будут находиться суммы продаж.
Метод pivot_table() является более универсальным и позволяет применять
различные агрегации, если в данных присутствует несколько значений для
одного и того же уровня группировки.
Расширенное манипулирование данными с Pandas 97
Метод melt() выполняет обратную операцию, т. е. преобразует отдельные
столбцы в строки. Это бывает удобно, когда у вас в столбцах представлены
однотипные данные и вы хотите объединить их в одной колонке. Например,
ее можно использовать для объединения разрозненных данных в столбцах
с годами в одну колонку.
Рассмотрим пример использования этих методов. Предположим, у нас есть
данные о продажах в магазинах по месяцам, и нам необходимо представить
их в виде таблицы с месяцами в строках и магазинами в столбцах с агреги-
рованными значениями. Затем мы выполним обратную операцию:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# Простой набор данных с продажами по магазинам и месяцам
np.random.seed(42)
stores = ['A', 'B', 'C']
months = ['1_Jan', '2_Feb', '3_Mar', '4_Apr', '5_May', '6_Jun']
data={
'Store': np.random.choice(stores, size=100),
'Month': np.random.choice(months, size=100),
'SalesAmount': np.random.randint(100, 1000, size=100),
'ItemsSold': np.random.randint(10, 100, size=100)
}
df = pd.DataFrame(data)
# Выводим исходный набор данных
print("Исходный набор данных:")
print(df.head())
print("\n")
# Создаем сводную таблицу с магазинами в столбцах и месяцами в строках (в значениях – суммы
продаж)
pivot_sales = df.pivot_table(index='Month', columns='Store', values='SalesAmount',
aggfunc='sum')
print("Сводная таблица – продажи по месяцам и магазинам:")
print(pivot_sales)
print("\n")
# Создаем длинную таблицу на основе сводной с магазинами в одном столбце и агрегированными
данными по продажам
melt_sales = pd.melt(pivot_sales.reset_index(), id_vars=['Month'], value_vars=['A', 'B',
'C'])
print("Длинная таблица на основе сводной:")
print(melt_sales)
print("\n")
# Сводная таблица со средним количеством единиц проданных товаров
pivot_items = df.pivot_table(index='Month', columns='Store', values='ItemsSold',
aggfunc='mean')
98 Оптимизация потоков данных
print("Сводная таблица – среднее количество единиц проданных товаров:")
print(pivot_items)
print("\n")
# Рассчитываем суммарные продажи по магазинам
store_totals = df.groupby('Store')['SalesAmount'].sum().sort_values(ascending=False)
print("Суммарные продажи по магазинам:")
print(store_totals)
print("\n")
# Находим месяцы с пиковыми продажами по магазинам
best_months = df.groupby('Store').apply(lambda x: x.loc[x['SalesAmount'].idxmax()])
print("Пиковые продажи по магазинам:")
print(best_months[['Store', 'Month', 'SalesAmount']])
print("\n")
# Визуализируем общие продажи по магазинам
plt.figure(figsize=(10, 6))
store_totals.plot(kind='bar')
plt.title('Общие продажи по магазинам')
plt.xlabel('Магазин')
plt.ylabel('Общие продажи')
plt.tight_layout()
plt.show()
# Визуализируем месячные тренды продаж по магазинам
pivot_sales.plot(kind='line', marker='o', figsize=(12, 6))
plt.title('Месячные тренды продаж по магазинам')
plt.xlabel('Месяц')
plt.ylabel('Общие продажи')
plt.legend(title='Магазин')
plt.tight_layout()
plt.show()
Вывод:
Исходный набор данных:
Store Month SalesAmount ItemsSold
0
C 4_Apr
541
33
1
A 3_Mar
663
63
2
C 3_Mar
367
42
3
C 1_Jan
609
33
4
A 3_Mar
906
84
Сводная таблица – продажи по месяцам и магазинам:
Store
A
B
C
Month
1_Jan 2901 3772 4112
2_Feb 578 222 4701
3_Mar 6057 3363 2249
4_Apr 2016 6731 2835
Расширенное манипулирование данными с Pandas 99
5_May 2447 3007 2272
6_Jun 2689 2829 2079
Длинная таблица на основе сводной:
Month Store value
0 1_Jan
A 2901
1 2_Feb
A 578
2 3_Mar
A 6057
3 4_Apr
A 2016
4 5_May
A 2447
5 6_Jun
A 2689
6 1_Jan
B 3772
7 2_Feb
B 222
8 3_Mar
B 3363
9 4_Apr
B 6731
10 5_May
B 3007
11 6_Jun
B 2829
12 1_Jan
C 4112
13 2_Feb
C 4701
14 3_Mar
C 2249
15 4_Apr
C 2835
16 5_May
C 2272
17 6_Jun
C 2079
Сводная таблица – среднее количество единиц проданных товаров:
Store
A
B
C
Month
1_Jan 35.200000 64.571429 40.444444
2_Feb 46.000000 31.000000 54.714286
3_Mar 53.666667 50.428571 49.800000
4_Apr 56.400000 61.888889 52.750000
5_May 37.000000 46.714286 55.333333
6_Jun 46.000000 71.000000 37.333333
Суммарные продажи по магазинам:
Store
B 19924
C 18248
A 16688
Name: SalesAmount, dtype: int64
Пиковые продажи по магазинам:
Store Month SalesAmount
Store
A
A 3_Mar
980
B
B 3_Mar
996
C
C 4_Apr
983
100 Оптимизация потоков данных
Рис. 2.1 Общие продажи по магазинам
Рис. 2.2 Месячные тренды продаж по магазинам
Расширенное манипулирование данными с Pandas 101
Что мы здесь сделали? Сначала создали набор данных с продажами по
трем магазинам и шести месяцам с января по июнь, а также с суммами и ко-
личеством проданных товаров. Далее мы создали сводную таблицу с мага-
зинами в столбцах и месяцами в строках с помощью метода pivot_table(),
после чего вытянули ее в длинную таблицу методом melt(), поместив все
магазины в один столбец. После этого мы создали еще одну сводную таблицу
со средним количеством проданных товаров в разрезе месяцев и магази-
нов. Затем мы сгруппировали данные по магазинам, вычислив суммарные
продажи по ним, после чего выполнили более сложную группировку, при-
менив к объекту группировки метод apply() с анонимной функцией lambda
x: x.loc[x['SalesAmount'].idxmax()]. С помощью этой функции мы извлекаем
пиковые продажи по каждому магазину. В заключение мы построили две
диаграммы: на первой в виде столбиков вывели общие продажи по магази-
нам, а на второй отобразили тренды по продажам магазинов по месяцам.
Включив в датафрейм одновременно и суммы продаж, и количество про-
данных товаров, мы получили возможность анализировать сразу оба пока-
зателя, что бывает удобно.
На этом примере мы продемонстрировали немного более сложный подход
к анализу данных, включающий в себя:
выбор нескольких показателей для анализа;
использование разных методов агрегации;
применение разных типов анализа (общие и средние показатели, ме-
сячные тренды и пиковые периоды);
визуализацию результатов.
2.1.4. Эффективный анализ временных рядов
Временные ряды традиционно добавляют сложности анализу, но без них
просто не обойтись при работе в финансовой сфере, в маркетинге и многих
других областях. В библиотеке Pandas представлен целый ряд удобных функ-
ций и методов для работы с датами и временем, позволяющих аналитику
проводить детализированный разбор стоящих перед ним задач. Эти инстру-
менты выходят за границы традиционных методов синтаксического разбора
дат и включают в себя операции по передискретизации временных рядов,
обработке часовых поясов и вычислению скользящих окон.
К примеру, при работе с биржевыми сводками вам может понадобиться
преобразовать поминутные данные в почасовые или подневные, адапти-
ровать анализ к разным часам открытия биржи в разных городах и странах
или рассчитать показатели для скользящих окон определенного размера.
С помощью инструментов Pandas вы можете все это делать легко и элегантно.
Кроме того, Pandas достаточно просто интегрируется с другими библио-
теками, такими как statsmodels или matplotlib. Это позволяет эффективно
выполнять полноценный анализ временных рядов: от подготовки и очистки
данных до статистического моделирования и визуализации.
102 Оптимизация потоков данных
Давайте рассмотрим пример передискретизации временного ряда. Допус-
тим, у вас есть данные о продажах с детализацией до дня, и вам необходимо
представить их в виде итогов по месяцам. Вот как можно это сделать:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# Простой набор данных с дневными продажами
np.random.seed(42)
date_range = pd.date_range(start='2023-01-01', end='2023-12-31', freq='D')
sales_data = {
'Date': date_range,
'SalesAmount': np.random.randint(100, 1000, size=len(date_range)),
'ProductCategory': np.random.choice(['Electronics', 'Clothing', 'Food'], size=len(date_
range))
}
df = pd.DataFrame(sales_data)
# Установим в качестве индекса столбец Date
df.set_index('Date', inplace=True)
# Первые несколько строк из исходного набора данных
print("Исходный набор данных:")
print(df.head())
print("\n")
# Передискретизируем дневные данные по месяцам и рассчитаем продажи помесячно
monthly_sales = df['SalesAmount'].resample('ME').sum()
print("Месячные продажи:")
print(monthly_sales)
print("\n")
# Рассчитаем скользящее среднее
df['MovingAverage'] = df['SalesAmount'].rolling(window=7).mean()
# Передискретизируем дневные данные по неделям и рассчитаем средние продажи понедельно
weekly_sales = df['SalesAmount'].resample('W').mean()
print("Недельные средние продажи:")
print(weekly_sales)
print("\n")
# Сгруппируем данные по категориям товаров и передискретизируем данные по месяцам
category_monthly_sales = df.groupby('ProductCategory')['SalesAmount'].resample('ME').sum().
unstack(level=0)
print("Месячные продажи по категориям товаров:")
print(category_monthly_sales)
print("\n")
# Визуализируем данные
plt.figure(figsize=(12, 6))
monthly_sales.plot(label='Месячные продажи')
weekly_sales.plot(label='Недельные средние продажи')
plt.title('Тренд продаж')
plt.xlabel('Дата')
plt.ylabel('Сумма продажи')
Расширенное манипулирование данными с Pandas 103
plt.legend()
plt.tight_layout()
plt.show()
# Визуализируем продажи по категориям товаров
category_monthly_sales.plot(kind='bar', stacked=True, figsize=(12, 6))
plt.title('Месячные продажи по категориям товаров')
plt.xlabel('Дата')
plt.ylabel('Сумма продажи')
plt.legend(title='Категория товара')
plt.tight_layout()
plt.show()
Вывод:
Исходный набор данных:
SalesAmount ProductCategory
Date
2023-01-01
202
Clothing
2023-01-02
535
Electronics
2023-01-03
960
Food
2023-01-04
370
Electronics
2023-01-05
206
Electronics
...
Месячные продажи:
Date
2023-01-31 14891
2023-02-28 17081
2023-03 -31 15903
...
2023-10-31 16723
2023-11 -30 18435
2023-12-31 14498
Freq: ME, Name: SalesAmount, dtype: int64
Недельные средние продажи:
Date
2023-01-01 202.000000
2023-01-08 451.714286
2023-01-15 427.142857
...
2023-12-17 463.285714
2023-12-24 504.571429
2023-12-31 440.571429
Freq: W-SUN, Name: SalesAmount, dtype: float64
Месячные продажи по категориям товаров:
ProductCategory Clothing Electronics Food
Date
2023-01-31
5713
4050 5128
2023-02-28
4299
9307 3475
2023-03 -31
5827
4885 5191
...
2023-10-31
2072
6914 7737
2023-11 -30
4034
7518 6883
2023-12-31
3163
7090 4245
104 Оптимизация потоков данных
Рис. 2 .3 Тренд продаж по неделям и месяцам
Рис. 2.4 Месячные продажи по категориям товаров
Расширенное манипулирование данными с Pandas 105
Сначала мы сгенерировали продажи по дням за весь 2023 год, восполь-
зовавшись функцией pd.date_range(). Затем установили в качестве индек-
са в датафрейме столбец с датами для удобства работы с временными ря-
дами. После этого мы передискретизировали дневные данные по месяцам
и по неделям с расчетом разных агрегаций, а также вычислили скользящие
средние продажи по неделям, чтобы сгладить дневные колебания. Далее мы
применили передискретизацию к сгруппированным по категориям товаров
данным, привели результат в компактный вид с помощью метода unstack()
и вывели итоги по месяцам для каждой категории. Наконец, мы построили
две диаграммы. На первой вывели тенденции продаж по неделям и месяцам,
а на второй сравнили помесячные продажи для разных категорий товаров.
Этот пример продемонстрировал несколько ключевых концепций, исполь-
зующихся при работе с временными рядами, а именно:
передискретизация временных рядов по месяцам или неделям;
вычисление скользящих показателей;
применение передискретизации к сгруппированным данным;
визуализация временных рядов при помощи библиотеки matplotlib.
Эти концепции позволяют проводить полноценный анализ данных, зави-
сящих от времени, отслеживать сезонные тенденции и выполнять сравнение
различных категорий данных.
В одной из следующих глав мы поговорим об анализе временных рядов
более подробно.
2.1.5. Оптимизация производительности
и использования памяти
С целью экономии памяти аналитик может применять разные стратегии.
Во-первых, ему стоит озаботиться тем, чтобы столбцам в датафрейме были
назначены оптимальные типы данных, не превышающие границ их диа-
пазонов. Во-вторых, можно воспользоваться специальным категориальным
типом для хранения столбцов с повторяющимися текстовыми вхождениями.
Также для наборов данных, в которых присутствует большое количество ну-
левых или пропущенных значений, можно использовать специальные разре-
женные структуры данных, о которых мы подробно поговорим в следующих
главах книги. Кроме того, в библиотеке Pandas реализована возможность
выполнять векторизованные операции и использовать полезные функции
eval() и query(), позволяющие оптимизировать вычисления в объемных на-
борах данных.
Рассмотрим небольшой пример по оптимизированию задействованной
памяти:
import pandas as pd
import numpy as np
106 Оптимизация потоков данных
import matplotlib.pyplot as plt
# Генерируем большой набор данных
np.random.seed(42)
n_rows = 1000000
data={
'TransactionID': range(1, n_rows + 1),
'SalesAmount': np.random.uniform(100, 1000, n_rows),
'Quantity': np.random.randint(1, 100, n_rows),
'CustomerID': np.random.randint(1000, 10000, n_rows),
'ProductCategory': np.random.choice(['Electronics', 'Clothing', 'Food', 'Books',
'Home'], n_rows)
}
df = pd.DataFrame(data)
# Изначальный расход памяти
print("Исходная информация по датафрейму:")
df.info(memory_usage='deep')
print("\n")
# Оптимизируем использование памяти
def optimize_dataframe(df):
for col in df.columns:
if df[col].dtype == 'float64':
df[col] = pd.to_numeric(df[col], downcast='float')
elif df[col].dtype == 'int64':
df[col] = pd.to_numeric(df[col], downcast='integer')
elif df[col].dtype == 'object':
if df[col].nunique() / len(df[col]) < 0.5: # Если меньше половины уникальных
значений
df[col] = df[col].astype('category')
return df
df_optimized = optimize_dataframe(df.copy())
# Оптимизированный расход памяти
print("Информация по датафрейму после оптимизации:")
df_optimized.info(memory_usage='deep')
print("\n")
# Рассчитаем экономию
original_memory = df.memory_usage(deep=True).sum()
optimized_memory = df_optimized.memory_usage(deep=True).sum()
memory_saved = original_memory - optimized_memory
print(f"Экономия памяти: {memory_saved / 1e6:.2f} MB")
print(f"Экономия в процентах: {(memory_saved / original_memory) * 100:.2f}%")
# Демонстрируем улучшение быстродействия
import time
def calculate_total_sales(dataframe):
return dataframe.groupby('ProductCategory', observed=False)['SalesAmount'].sum()
Расширенное манипулирование данными с Pandas 107
# Засекаем время на исходном датафрейме
start_time = time.time()
original_result = calculate_total_sales(df)
original_time = time.time() - start_time
# Засекаем время на оптимизированном датафрейме
start_time = time.time()
optimized_result = calculate_total_sales(df_optimized)
optimized_time = time.time() - start_time
print(f"\nРасход времени (на исходном датафрейме): {original_time:.4f} с.")
print(f"Расход времени (на оптимизированном датафрейме): {optimized_time:.4f} с.")
print(f"Ускорение: {(original_time - optimized_time) / original_time * 100:.2f}%")
Вывод:
Исходная информация по датафрейму:
<class 'pandas.core .frame.DataFrame'>
RangeIndex: 1000000 entries, 0 to 999999
Data columns (total 5 columns):
# Column
Non-Null Count
Dtype
---
------
--------------
-----
0 TransactionID 1000000 non-null int64
1 SalesAmount
1000000 non-null float64
2 Quantity
1000000 non-null int64
3 CustomerID
1000000 non-null int64
4 ProductCategory 1000000 non-null object
dtypes: float64(1), int64(3), object(1)
memory usage: 91.0 MB
Информация по датафрейму после оптимизации:
<class 'pandas.core .frame.DataFrame'>
RangeIndex: 1000000 entries, 0 to 999999
Data columns (total 5 columns):
# Column
Non-Null Count
Dtype
---
------
--------------
-----
0 TransactionID 1000000 non-null int32
1 SalesAmount
1000000 non-null float32
2 Quantity
1000000 non-null int8
3 CustomerID
1000000 non-null int16
4 ProductCategory 1000000 non-null category
dtypes: category(1), float32(1), int16(1), int32(1), int8(1)
memory usage: 11.4 MB
Экономия памяти: 83.40 MB
Экономия в процентах: 87.42%
Расход времени (на исходном датафрейме): 0.0387 с.
Расход времени (на оптимизированном датафрейме): 0.0056 с.
Ускорение: 85.58%
108 Оптимизация потоков данных
Как видите, экономия занимаемой памяти в процентах составила 87.42 %,
а расходуемого времени – 85.58 %.
Что мы здесь сделали?
1. Использование памяти до преобразований:
• мы воспользовались методом info() для отображения изначально за-
действованной под датафрейм памяти. Аргумент memory_usage='deep'
позволяет «копнуть глубже» и извлечь актуальные размеры затрачи-
ваемой памяти под все объекты в датафрейме.
2. Оптимизация памяти:
• мы объявили функцию optimize_dataframe(), в которой колонки пе-
реданного датафрейма оптимизируются в зависимости от их типа
данных:
• для колонок с типом float64 мы вызываем функцию pd.to_numeric()
с параметром downcast='float', что позволяет изменить тип данных
на минимально допустимый с учетом имеющихся в колонке значе-
ний;
• для колонок с типом int64 мы применили ту же функцию pd.to_numer-
ic() с параметром downcast='integer' для получения оптимального
целочисленного типа данных;
• для колонок с объектами (строками) мы выполнили дополнительную
проверку на долю уникальных значений. Если их доля составляет
менее 50 %, для колонки задается тип category, позволяющий су-
щественно сэкономить память на столбцах с большим количеством
повторений.
3. Сравнение использованной памяти:
• сравнили объем занимаемой памяти до и после оптимизации да-
тафрейма;
• вычислили абсолютный и относительный прирост по этому пока-
зателю.
4. Сравнение быстродействия:
• мы определили простую операцию расчета суммы продаж по ка-
тегориям товаров и засекли время ее выполнения для исходного
и оптимизированного датафреймов;
• сравнили полученные результаты и вывели на экран.
На этом примере мы продемонстрировали несколько концепций, связан-
ных с оптимизацией памяти и производительности в Pandas, а именно:
эффективное использование памяти при помощи оптимизации типов
данных и применения категориального типа;
измерение используемой памяти до и после оптимизации;
оценка быстродействия при выполнении операций над данными;
проверка результатов оптимизации.
Повышение производительности при помощи массивов NumPy 109
2.2. Повышение производительности
при помощи массивов NumPy
Позже, когда вы глубже погрузитесь в анализ данных, вы поймете, насколько
важную роль играет эффективность вычислений применительно к числовым
данным.
NumPy представляет собой библиотеку для эффективной работы с чис-
ловыми и многомерными данными. В этой библиотеке представлено все
многообразие инструментов для обработки объемных матриц и многомер-
ных массивов и применения к ним различных математических функций
и преобразований. Истинная мощь этой библиотеки, как мы уже упоминали,
кроется в возможности использовать векторизованные операции, т. е . вы-
полнять вычисления применительно к целым массивам значений вместо
традиционных циклов.
В следующих разделах мы более подробно поговорим о работе с массива-
ми в NumPy.
2.2.1. Работа с массивами в NumPy
Массивы NumPy произвели настоящую революцию в области анализа данных
в научной и прочих сферах. Своей потрясающей скоростью работы в сравне-
нии с традиционными списками в Python библиотека NumPy обязана эффек-
тивной работе с памятью и оптимизации числовых вычислений. В отличие
от списков Python, при работе с которыми сохраняются ссылки на все объ-
екты, разбросанные в памяти, в массивах NumPy используются непрерывные
блоки памяти. Такой способ хранения обеспечивает очень быстрый доступ
к данным и манипуляцию с ними, поскольку процессору требуется очень
мало времени на извлечение и обработку значений, физически находящихся
в одной области памяти.
Кроме того, в библиотеке NumPy применяется низкоуровневая оптимиза-
ция, именуемая векторизацией, специально разработанная для эффектив-
ных числовых вычислений.
В совокупности эффективное хранение данных в памяти и использование
векторизации дает огромный прирост при работе с большими наборами дан-
ных и выполнении сложных математических преобразований.
Давайте рассмотрим пример сравнения эффективности обработки данных
в NumPy и традиционных списках Python:
import numpy as np
import time
import matplotlib.pyplot as plt
def compare_performance(size):
110 Оптимизация потоков данных
# Создаем список и массив NumPy с переданным количеством элементов
py_list = list(range(1, size + 1))
np_array = np.arange(1, size + 1)
# Операция со списком в Python: умножение всех значений на 2
start = time.time()
py_result = [x * 2 for x in py_list]
py_time = time.time() - start
# Операция с массивом NumPy: умножение всех значений на 2
start = time.time()
np_result = np_array * 2
np_time = time.time() - start
return py_time, np_time
# Сравним быстродействие для разного количества элементов
sizes = [10**i for i in range(2, 8)] # от 100 до 10 000 000
py_times = []
np_times = []
for size in sizes:
py_time, np_time = compare_performance(size)
py_times.append(py_time)
np_times.append(np_time)
print(f"Размер: {size}")
print(f"Операция со списком в Python: {py_time:.6f} с.")
print(f"Операция с массивом NumPy: {np_time:.6f} с.")
print(f"Разница: {py_time / np_time:.2f} раз\n")
# Визуализируем результаты
plt.figure(figsize=(10, 6))
plt.plot(sizes, py_times, 'b-', label='Список Python')
plt.plot(sizes, np_times, 'r-', label='Массив NumPy')
plt.xscale('log')
plt.yscale('log')
plt.xlabel('Размер массива')
plt.ylabel('Время (секунды)')
plt.title('Сравнение быстродействия: списки Python против массивов NumPy')
plt.legend()
plt.grid(True)
plt.show()
# Сравнение использования памяти
import sys
size = 1000000
py_list = list(range(size))
np_array = np.arange(size)
py_memory = sys.getsizeof(py_list) + sum(sys.getsizeof(i) for i in py_list)
np_memory = np_array.nbytes
print(f"Использование памяти для хранения {size} элементов:")
Повышение производительности при помощи массивов NumPy 111
print(f"Список Python: {py_memory / 1e6:.2f} Мб")
print(f"Массив NumPy: {np_memory / 1e6:.2f} Мб")
print(f"Разница: {py_memory / np_memory:.2f} раз")
Вывод:
Размер: 100
Операция со списком в Python: 0.000011 с.
Операция с массивом NumPy: 0.000027 с.
Разница: 0.41 раз
Размер: 1000
Операция со списком в Python: 0.000116 с.
Операция с массивом NumPy: 0.000015 с.
Разница: 7.71 раз
Размер: 10000
Операция со списком в Python: 0.000708 с.
Операция с массивом NumPy: 0.000167 с.
Разница: 4.24 раз
Размер: 100000
Операция со списком в Python: 0.002717 с.
Операция с массивом NumPy: 0.000227 с.
Разница: 11.96 раз
Размер: 1000000
Операция со списком в Python: 0.033992 с.
Операция с массивом NumPy: 0.002144 с.
Разница: 15.86 раз
Размер: 10000000
Операция со списком в Python: 0.385633 с.
Операция с массивом NumPy: 0.042954 с.
Разница: 8.98 раз
Использование памяти для хранения 1000000 элементов:
Список Python: 36.00 Мб
Массив NumPy: 8.00 Мб
Разница: 4.50 раз
Что происходит в этом коде?
1. Объявление функции для сравнения быстродействия:
• на этом шаге мы определяем функцию compare_performance(size),
в которой создаются список Python и массив NumPy переданного
с помощью атрибута size размера, после чего измеряется время, не-
обходимое для умножения всех их элементов на два.
2. Проверка с масштабированием:
• здесь мы вызываем созданную функцию с разным количеством эле-
ментов (от 100 до 10 млн), чтобы понять, как зависит быстродействие
от числа производимых операций.
112 Оптимизация потоков данных
Рис. 2.5 Сравнение быстродействия: списки Python против массивов NumPy
3. Измерение времени:
• в нашей функции используется функция time.time(), позволяющая
выполнить замер времени, требующегося на выполнение операции
со списком и массивом.
4. Вывод результатов:
• для каждого количества элементов мы выводим результаты и рас-
считываем прирост.
5. Визуализация:
• при помощи библиотеки matplotlib строим график с логарифмиче-
ской шкалой на обеих осях, на котором наглядно показан прирост
быстродействия при работе с массивами NumPy.
6. Сравнение задействованной памяти:
• здесь мы смотрим, сколько памяти используется для хранения спис-
ка Python и массива NumPy, состоящих из 1 млн элементов. В случае
со списком мы учитываем размеры как самого объекта списка, так
и всех хранящихся в нем элементов.
7. Ключевые выводы:
• операции с массивами NumPy выполняются гораздо быстрее, осо-
бенно при большом количестве элементов;
• быстродействие растет с увеличением объема массива;
• для хранения массивов NumPy требуется существенно меньше па-
мяти в сравнении со списками Python.
Повышение производительности при помощи массивов NumPy 113
2.2.2. Векторизованные операции: скорость
и простота
Как мы уже не раз говорили, одним из главных преимуществ библиотеки
NumPy является возможность выполнения векторизованных операций (vec-
torized operation), применяемых одновременно ко всем элементам массивов.
Среди прочих достоинств векторизованных операций можно выделить
их высокое быстродействие, легкость восприятия в коде, эффективное ис-
пользование памяти, возможность параллельной обработки и простоту под-
держки.
Давайте рассмотрим пример, где у нас имеются данные о продажах, к ко-
торым нам нужно применить некоторые математические преобразования
в подготовке к дальнейшему анализу. Мы посчитаем натуральный логарифм,
квадратный корень и экспоненту с помощью векторизованных операций
NumPy:
import numpy as np
# Суммы продаж
sales = np.array([100, 200, 300, 400, 500])
# Применяем преобразования с помощью векторизованных операций
log_sales = np.log(sales)
sqrt_sales = np.sqrt(sales)
exp_sales = np.exp(sales)
# Выводим результаты
print("Исходные данные:", sales)
print("Логарифм:", log_sales)
print("Квадратный корень:", sqrt_sales)
print("Экспонента:", exp_sales)
# Рассчитываем некоторые статистики
mean_sales = np.mean(sales)
median_sales = np.median(sales)
std_sales = np.std(sales)
print(f"\nСреднее значение: {mean_sales:.2f}")
print(f"Медиана: {median_sales:.2f}")
print(f"Стандартное отклонение: {std_sales:.2f}")
# Выполняем поэлементные операции
discounted_sales = sales * 0.9 # 10 % скидка
increased_sales = sales + 50 # увеличение на 50
print("\nСумма со скидкой (10 %):", discounted_sales)
print("Увеличенная сумма (+$50):", increased_sales)
Вывод:
Исходные данные: [100 200 300 400 500]
Логарифм: [4.60517019 5.29831737 5.70378247 5.99146455 6.2146081 ]
114 Оптимизация потоков данных
Квадратный корень: [10.
14.14213562 17.32050808 20.
22.36067977]
Экспонента: [2.68811714e+043 7.22597377e+086 1.94242640e+130 5.22146969e+173
1.40359222e+217]
Среднее значение: 300.00
Медиана: 300.00
Стандартное отклонение: 141.42
Сумма со скидкой (10 %): [ 90. 180. 270. 360. 450.]
Увеличенная сумма (+$50): [150 250 350 450 550]
Здесь мы вручную создали вектор с данными о продажах, после чего при-
менили к нему математические преобразования с помощью функций NumPy
(np.log(), np.sqrt() и n p.exp()). Эти преобразования демонстрируют возмож-
ность поэлементного выполнения операций без использования циклов. Да-
лее мы вывели результаты и вычислили некоторые статистики на основе
нашего набора данных с помощью функций np.mean(), np.median() и np.std().
После этого мы поэлементно умножили все значения в векторе на 0.9, а также
прибавили к ним 50 так, словно работаем со скалярными значениями.
2.2.3. Транслирование: гибкие операции
с массивами
В библиотеке NumPy реализована очень мощная концепция под названием
транслирование (broadcasting), позволяющая выполнять математические
операции применительно к массивам разных размеров. Это бывает особенно
полезно, если вам необходимо применить преобразование к массивам без
ручного сопоставления их размеров.
Концепция транслирования предполагает выполнение ряда правил, для
того чтобы массивы могли быть приведены в сопоставимый вид. В резуль-
тате она позволяет не только сделать код более читаемым, но и повысить его
быстродействие, особенно при работе с большими массивами.
На самом деле правил для получения массивов сопоставимого размера
всего два.
1. В массив с меньшим количеством размерностей слева добавляются
пустые размерности до достижения равенства в числе размерностей
у двух массивов.
2. Выполняется проверка на то, чтобы в соответствующих размерностях
стояли либо одинаковые числа, либо в одном из массивов была едини-
ца.
Если эти правила выполняются, массивы могут быть приведены к сопо-
ставимому виду с помощью операции транслирования. В противном слу-
чае – нет.
К примеру, если у вас есть данные о продажах и вы хотите каждое значе-
ние увеличить или уменьшить на некую константу, вы можете сделать это
Повышение производительности при помощи массивов NumPy 115
напрямую, не изменяя форму данных вручную. Помимо добавления констан-
ты, транслирование бывает удобно применять для выполнения операций
над двумерным и одномерным массивами, например при масштабировании
признаков в наборе данных или расчете прогнозов.
Давайте воспользуемся простым транслированием на примере с вектора-
ми и константами:
import numpy as np
import matplotlib.pyplot as plt
# Суммы продаж
sales = np.array([100, 200, 300, 400, 500])
# Добавляем налог в размере 10 % к каждому значению с помощью транслирования
taxed_sales = sales * 1.10
# Добавляем наценку в размере $25 к каждой продаже
flat_fee_sales = sales + 25
# Рассчитываем разницу между суммами с налогом и с наценкой
difference = taxed_sales - flat_fee_sales
# Выводим результаты
print("Исходные значения:", sales)
print("Суммы с налогом 10 %:", taxed_sales)
print("Суммы с наценкой $25:", flat_fee_sales)
print("Разницы между суммами с налогом и суммами с наценкой:", difference)
# Рассчитываем статистики
total_sales = np.sum(sales)
average_sale = np.mean(sales)
max_sale = np.max(sales)
min_sale = np.min(sales)
print(f"\nОбщие продажи: ${total_sales}")
print(f"Средняя продажа: ${average_sale:.2f}")
print(f"Максимальная сумма: ${max_sale}")
print(f"Минимальная сумма: ${min_sale}")
Вывод:
Исходные значения: [100 200 300 400 500]
Суммы с налогом 10 %: [110. 220. 330 . 440. 550.]
Суммы с наценкой $25: [125 225 325 425 525]
Разницы между суммами с налогом и суммами с наценкой: [-15.
-5. 5. 15. 25.]
Общие продажи: $1500
Средняя продажа: $300.00
Максимальная сумма: $500
Минимальная сумма: $100
Здесь мы воспользовались концепцией транслирования для изменения
исходного вектора с числовыми данными, после чего сравнили результаты
преобразований.
116 Оптимизация потоков данных
2.2.4. Работа с памятью: типы данных в NumPy
Создавая массивы в NumPy, вы можете задавать типы хранящихся в них
значений. Это можно сделать при помощи атрибута dtype. Явное указание
типов данных позволяет сэкономить место в памяти и сократить время,
требующееся для выполнения вычислений. К примеру, использование типа
данных float32 вместо float64, заданного по умолчанию для массивов с пла-
вающей запятой, может помочь существенно сэкономить ресурсы при работе
с большими наборами данных.
Рассмотрим пример с явным указанием типов данных создаваемых мас-
сивов и узнаем, какую выгоду можно получить:
import numpy as np
import matplotlib.pyplot as plt
# Создаем большой массив с типом данных по умолчанию (float64)
large_array = np.arange(1, 10000001, dtype='float64')
print(f"Тип данных по умолчанию (float64), использование памяти: {large_array.nbytes} байт")
# Тот же массив с типом данных float32
optimized_array = np.arange(1, 10000001, dtype='float32')
print(f"Оптимизированный тип (float32), использование памяти: {optimized_array.nbytes} байт")
# Тот же массив с типом данных int32
int_array = np.arange(1, 10000001, dtype='int32')
print(f"Целочисленный тип данных (int32), использование памяти: {int_array.nbytes} bytes")
# Сравниваем время вычислений
import time
def compute_sum(arr):
return np.exp(np.sqrt(np.sin(arr ** 2) ** 2) ** 3)
start_time = time.time()
result_large = compute_sum(large_array)
time_large = time.time() - start_time
start_time = time.time()
result_optimized = compute_sum(optimized_array)
time_optimized = time.time() - start_time
start_time = time.time()
result_int = compute_sum(int_array)
time_int = time.time() - start_time
print(f"\nВремя вычисления (float64): {time_large:.6f} с.")
print(f"Время вычисления (float32): {time_optimized:.6f} с.")
print(f"Время вычисления (int32): {time_int:.6f} с.")
# Визуализация времени вычисления
dtypes = ['float64', 'float32', 'int32']
computation_times = [time_large, time_optimized, time_int]
plt.figure(figsize=(10, 6))
plt.bar(dtypes, computation_times)
Повышение производительности при помощи массивов NumPy 117
plt.title('Время вычисления по типам данных')
plt.xlabel('Тип данных')
plt.ylabel('Время вычисления (секунды)')
plt.show()
Вывод:
Тип данных по умолчанию (float64), использование памяти: 80000000 байт
Оптимизированный тип (float32), использование памяти: 40000000 байт
Целочисленный тип данных (int32), использование памяти: 40000000 bytes
Время вычисления (float64): 0.233548 с.
Время вычисления (float32): 0.116874 с.
Время вычисления (int32): 0.224736 с.
Рис. 2 .6 Время вычисления по типам данных
Здесь мы применили одно и то же сложное вычисление к массивам с тре-
мя разными типами данных (float64, float32 и int32), а также посмотрели на
разницу в расходуемой памяти.
Из этого примера можно сделать вывод о том, что с числами с большей
точностью вычисления выполняются дольше, что вполне объяснимо. В ка-
честве компромисса можно выбирать менее затратные типы данных, если
вы можете поступиться точностью.
118 Оптимизация потоков данных
2.2.5. Многомерные массивы: работа со сложными
структурами данных
Способность эффективно работать с массивами данных любой размерности
сделала библиотеку NumPy практически незаменимой при разработке при-
ложений для анализа данных и машинного обучения.
К примеру, для обработки изображений обычно используются трехмерные
массивы NumPy, в которых первые два измерения отвечают за координаты
пикселей, а третье – за цветовые каналы. В анализе временных рядов дву-
мерные массивы часто применяются для хранения информации об изменя-
ющихся во времени признаках.
Но гибкость многомерных массивов NumPy не ограничивается одним
лишь удобством их представления и визуализации. В библиотеке реализо-
вано большое количество функций и методов для эффективного изменения
формы массивов, а также осуществления срезов и транслирования. С их по-
мощью вы, например, можете очень легко извлечь нужный вам временной
диапазон из объемного трехмерного массива с данными о климатических
изменениях или применить необходимые преобразования сразу к несколь-
ким измерениям.
Давайте рассмотрим пример работы с двумерным массивом NumPy, пред-
ставляющим данные о продажах по магазинам и месяцам:
import numpy as np
import matplotlib.pyplot as plt
# Данные о продажах: в строках представлены магазины, а в столбцах – месяцы
sales_data = np.array([[250, 300, 400, 280, 390],
[200, 220, 300, 240, 280],
[300, 340, 450, 380, 420],
[180, 250, 350, 310, 330]])
# Суммы продаж по всем месяцам в разрезе магазинов
total_sales_per_store = sales_data.sum(axis=1)
print("Суммы продаж в разрезе магазинов:", total_sales_per_store)
# Средние продажи по всем магазинам в разрезе месяцев
average_sales_per_month = sales_data.mean(axis=0)
print("Средние продажи в разрезе месяцев:", average_sales_per_month)
# Найдем магазин с максимальными продажами
best_performing_store = np.argmax(total_sales_per_store)
print("Магазин с максимальными продажами:", best_performing_store)
# Найдем месяц с максимальными средними продажами
best_performing_month = np.argmax(average_sales_per_month)
print("Месяц с максимальными средними продажами:", best_performing_month)
# Рассчитаем процентное изменение суммы продаж между первым и последним месяцами
percentage_change = ((sales_data[:, -1] - sales_data[:, 0]) / sales_data[:, 0]) * 100
Повышение производительности при помощи массивов NumPy 119
print("Процентное изменение суммы продаж:", percentage_change)
# Визуализируем данные о продажах
plt.figure(figsize=(24, 16))
for i in range(sales_data.shape[0]):
plt.plot(sales_data[i], label=f'Магазин {i+1}')
plt.savefig('pics/2.7 .png')
plt.title('Месячные продажи по магазинам')
plt.xlabel('Месяц')
plt.ylabel('Продажи')
plt.xticks(np.arange(5), ['Янв', 'Фев', 'Мар', 'Апр', 'Май'])
plt.legend()
plt.grid(True)
plt.show()
# Выполняем поэлементные операции
tax_rate = 0.08
taxed_sales = sales_data * (1 + tax_rate)
print("\nСуммы продаж после добавки налога в размере 8 %:\n", taxed_sales)
# Воспользуемся булевой индексацией для поиска самых продуктивных месяцев и магазинов
high_performing_months = sales_data > 300
print("\nМагазины и месяцы с суммой продаж > 300:\n", high_performing_months)
# Рассчитаем корреляцию между магазинами
correlation_matrix = np.corrcoef(sales_data)
print("\nКорреляционная матрица по магазинам:\n", correlation_matrix)
Вывод:
Суммы продаж в разрезе магазинов: [1620 1240 1890 1420]
Средние продажи в разрезе месяцев: [232.5 277.5 375. 302.5 355. ]
Магазин с максимальными продажами: 2
Месяц с максимальными средними продажами: 2
Процентное изменение суммы продаж: [56.
40.
40.
83.33333333]
Суммы продаж после добавки налога в размере 8 %:
[[270. 324. 432. 302.4 421.2]
[216. 237.6 324. 259 .2 302.4]
[324. 367.2 486. 410.4 453.6]
[194.4 270. 378. 334.8 356.4]]
Магазины и месяцы с суммой продаж > 300:
[[False False True False True]
[False False False False False]
[False True True True True]
[False False True True True]]
Корреляционная матрица по магазинам:
[[1.
0.95294605 0.91615831 0.82844052]
[0.95294605 1.
0.98987064 0.92769126]
[0.91615831 0.98987064 1.
0.97000798]
[0.82844052 0.92769126 0.97000798 1.
]]
120 Оптимизация потоков данных
Рис. 2.7 Месячные продажи по магазинам
Здесь мы храним исходные данные о продажах в виде двумерного массива
NumPy, предполагая, что в строках представлены магазины, а в столбцах –
месяцы. Суммы продаж по всем месяцам в разрезе магазинов мы вычислили
при помощи инструкции total_sales_per_store = sales_data.sum(axis=1). Об-
ратите внимание на аргумент axis функции sum(), позволяющий указать ось,
вдоль которой будет производиться вычисление. Здесь мы указали первую
ось, а это значит, что суммы будут вычислены для каждой строки массива
отдельно (и будут включать в себя значения из всех столбцов). Далее мы
в функцию mean() передали нулевую ось, что позволило нам рассчитать сред-
ние продажи по всем магазинам в разрезе месяцев, т. е . для каждого столбца
в массиве вычислить среднее значение по всем строкам. Аргумент axis играет
очень важную роль в библиотеке NumPy, позволяя выполнять очень сложные
вычисления по разным осям массивов очень кратко и лаконично. После этого
мы воспользовались функцией np.argmax(), позволяющей получить индексы
элементов с максимальным значением по указанной оси. С помощью нее
мы узнали магазин с максимальными продажами и месяц с максимальны-
ми средними продажами. Для сравнения в процентах первого месяца с по-
следним мы воспользовались оператором извлечения среза sales_data[:,
-1] и sales_data[:, 0], получив тем самым последний (индекс –1) и первый
(индекс 0) столбцы. Двоеточие в первом измерении среза означает, что мы
берем значения из всех строк. Таким образом, с помощью индексирования
можно легко выполнять поэлементные операции над разными областями
Комбинирование инструментов для выполнения эффективного анализа данных 121
массивов. После вывода графика с месячными продажами по магазинам мы
продемонстрировали выполнение транслирования, умножив все элементы
нашего массива на константу. Также мы продемонстрировали операцию бу-
лева индексирования в инструкции high_performing_months = sales_data > 300.
Так мы можем накладывать на массив различные маски, получая булевы
значения True и F alse в зависимости от того, отвечают исходные значения
заданному критерию или нет. В завершение мы построили корреляционную
матрицу по продажам, воспользовавшись функцией np.corrcoef().
2.3. Комбинирование инструментов
для выполнения эффективного анализа
данных
В этом разделе мы будем последовательно добавлять в наш анализ данных
библиотеки Pandas, NumPy и Scikit-learn. К концу раздела у вас появится
полное представление о том, как можно гармонично и с пользой для общего
дела совмещать эти мощные инструменты анализа.
2.3.1. Шаг 1: предварительная обработка данных
с помощью Pandas и NumPy
Первый шаг в анализе данных практически всегда включает в себя следую-
щие стадии.
1. Очистка данных:
• обработка дублирующихся значений;
• приведение в порядок форматов данных, включая даты;
• стандартизация форматов, например приведение всех тестовых дан-
ных к нижнему регистру;
• обработка выбросов, способных исказить результаты анализа;
• приведение в порядок имен признаков и атрибутов.
2. Обработка пропущенных значений:
• подстановка:
подстановка с использованием среднего значения или медианы;
подстановка с помощью регрессионного анализа с использовани-
ем в качестве предикторов других переменных из набора данных;
подстановка с помощью метода k-ближайших соседей на основе
близлежащих наблюдений;
• удаление:
удаление строк, содержащих хотя бы одно пропущенное значение;
122 Оптимизация потоков данных
попарное удаление, предполагающее исключение строк только
для анализа, включающего предикторы, содержащие пропуски;
• продвинутые техники:
применение метода множественного восстановления пропущен-
ных данных;
оценка максимального правдоподобия, включающая использо-
вание статистических моделей для оценки предикторов, содер-
жащих пропуски;
методы машинного обучения, например использование метода
случайного леса или нейронной сети для предсказания пропу-
щенных значений.
3. Преобразование исходных данных:
• нормализация;
• стандартизация;
• кодирование категориальных переменных;
• применение математических преобразований для нивелирования
перекосов в распределениях.
Для выполнения всех перечисленных выше операций можно воспользо-
ваться библиотеками Pandas и NumPy, как показано в примере ниже. На
плечи Pandas ложатся преобразования табличных данных, а в зону ответ-
ственности NumPy входят эффективные числовые вычисления.
Предположим, что у нас есть набор данных, содержащий пропущенные
значения и переменные, нуждающиеся в преобразовании. Нам необходимо
подготовить эти данные для дальнейшего анализа и заполнить пропуски:
import pandas as pd
import numpy as np
from sklearn.preprocessing import StandardScaler
from sklearn.impute import SimpleImputer
# Простой набор данных с транзакциями по продаже
data={
'CustomerID': [1, 2, 3, 4, 5, 6, 7, 8],
'PurchaseAmount': [250, np.nan, 300, 400, np.nan, 150, 500, 350],
'Discount': [10, 15, 20, np.nan, 5, 12, np.nan, 18],
'Store': ['A', 'B', 'A', 'C', 'B', 'C', 'A', 'B'],
'CustomerAge': [35, 42, np.nan, 28, 50, np.nan, 45, 33],
'LoyaltyScore': [75, 90, 60, 85, np.nan, 70, 95, 80]
}
df = pd.DataFrame(data)
# Шаг 1: Обработка пропущенных значений
imputer = SimpleImputer(strategy='mean')
numeric_columns = ['PurchaseAmount', 'Discount', 'CustomerAge', 'LoyaltyScore']
df[numeric_columns] = imputer.fit_transform(df[numeric_columns])
# Шаг 2: Применение преобразований
df['LogPurchase'] = np.log(df['PurchaseAmount'])
df['DiscountRatio'] = df['Discount'] / df['PurchaseAmount']
Комбинирование инструментов для выполнения эффективного анализа данных 123
# Шаг 3: Кодирование категориальных переменных
df['StoreEncoded'] = df['Store'].astype('category').cat.codes
# Шаг 4: Создание признаков взаимодействия
df['AgeLoyaltyInteraction'] = df['CustomerAge'] * df['LoyaltyScore']
# Шаг 5: Разделение непрерывных переменных на отрезки
df['AgeBin'] = pd.cut(df['CustomerAge'], bins=[0, 30, 50, 100], labels=['Young', 'Middle',
'Senior'])
# Шаг 6: Масштабирование числовых признаков
scaler = StandardScaler()
df[numeric_columns] = scaler.fit_transform(df[numeric_columns])
# Шаг 7: Создание фиктивных переменных на основе категориальных признаков
df = pd.get_dummies(df, columns=['Store', 'AgeBin'], prefix=['Store', 'Age'])
print(df)
print("\nИнформация о наборе данных:")
print(df.info())
print("\nСуммарная статистика:")
print(df.describe())
Вывод:
CustomerID PurchaseAmount Discount CustomerAge LoyaltyScore \
0
1
- 0 .781133 -0 .766402 -0 .589018 -4 .106315e-01
1
2
0.000000 0 .383201
0.486580 1.026579e+00
2
3
- 0 .260378 1.532803
0.000000
- 1 .847842e+00
...
5
6
- 1 .822645 -0 .306561
0.000000
- 8 .897017e-01
6
7
1.822645 0.000000
0.947551 1.505649e+00
7
8
0.260378 1.072962 -0 .896332 6.843859e-02
LogPurchase DiscountRatio StoreEncoded AgeLoyaltyInteraction Store_A \
0
5.521461
0.040000
0
2625.000000
True
1
83825
0.046154
1
3780.000000 False
2
5.703782
0.066667
0
2330.000000
True
...
5
5.010635
0.080000
2
2718.333333 False
6
6.214608
0.026667
0
4275.000000
True
7
5.857933
0.051429
1
2640.000000 False
Store_B Store_C Age_Young Age_Middle Age_Senior
0 False False
False
True
False
1
True
False
False
True
False
2 False False
False
True
False
...
5 False
True
False
True
False
6 False False
False
True
False
7
True
False
False
True
False
124 Оптимизация потоков данных
Информация о наборе данных:
<class 'pandas.core .frame.DataFrame'>
RangeIndex: 8 entries, 0 to 7
Data columns (total 15 columns):
# Column
Non-Null Count Dtype
---
------
--------------
-----
0 CustomerID
8 non-null
int64
1 PurchaseAmount
8 non-null
float64
2 Discount
8 non-null
float64
3 CustomerAge
8 non-null
float64
4 LoyaltyScore
8 non-null
float64
5 LogPurchase
8 non-null
float64
6 DiscountRatio
8 non-null
float64
7 StoreEncoded
8 non-null
int8
8 AgeLoyaltyInteraction 8 non-null
float64
9 Store_A
8 non-null
bool
10 Store_B
8 non-null
bool
11 Store_C
8 non-null
bool
12 Age_Young
8 non-null
bool
13 Age_Middle
8 non-null
bool
14 Age_Senior
8 non-null
bool
dtypes: bool(6), float64(7), int64(1), int8(1)
memory usage: 696.0 bytes
None
Суммарная статистика:
CustomerID PurchaseAmount
Discount CustomerAge LoyaltyScore \
count
8.00000 8 .000000e+00 8.000000e+00 8.000000e+00 8.000000e+00
mean
4.50000 1.387779e-17
- 1 .110223e-16 -2.775558e-16 8.604228e-16
std
2.44949 1.069045e+00 1.069045e+00 1.069045e+00 1.069045e+00
min
1.00000
- 1 .822645e+00 -1 .916004e+00 -1.664616e+00 -1 .847842e+00
25%
2.75000 -3 .905667e-01 -4 .215209e-01 -6.658464e-01 -5 .303991e-01
50%
4.50000 0.000000e+00 0.000000e+00 0.000000e+00 3.421930e-02
75%
6.25000 3.905667e-01 5.556412e-01 6.018227e-01 6.672763e-01
max
8.00000 1.822645e+00 1.532803e+00 1.715835e+00 1.505649e+00
LogPurchase DiscountRatio StoreEncoded AgeLoyaltyInteraction
count
8.000000
8.000000
8.000000
8.000000
mean
5.733442
0.044954
0.875000
3089.077381
std
0.355957
0.021083
0.834523
782.236246
min
5.010635
0.015385
0.000000
2330.000000
25%
5.658202
0.031667
0.000000
2563.750000
50%
5.783825
0.043077
1.000000
2679.166667
75%
5.891316
0.055238
1.250000
3826.071429
max
6.214608
0.080000
2.000000
4275.000000
Для замены пропущенных значений мы вместо функции fillna() вос-
пользовались классом SimpleImputer из библиотеки sklearn. Этот подход бо-
лее универсальный и легко масштабируется для интеграции в конвейеры
машинного обучения. Мы применили его сразу ко всем числовым столб-
цам в датафрейме. Далее мы применили логарифмическое преобразование
Комбинирование инструментов для выполнения эффективного анализа данных 125
к столбцу PurchaseAmount и добавили новый расчетный признак DiscountRatio
для учета процента скидки. После этого мы преобразовали переменную Store
в категориальный признак и добавили переменную взаимодействия Age-
LoyaltyInteraction, которая может помочь нам отследить зависимости между
другими полями в наборе. Также мы разбили содержимое столбца CustomerAge
на три возрастные группы, что поможет отловить нелинейные зависимости
и избавит нас от влияния выбросов. Далее мы воспользовались классом Stan-
dardScaler для масштабирования всех числовых столбцов, а в завершение
создали фиктивные переменные на основе столбцов Store и AgeBin, которые
получили имена Store_A, Store_B , Store_C, Age_Young, Age_Middle и Age_Senior.
2.3.2. Шаг 2: конструирование признаков
с помощью Pandas и NumPy
На этом шаге мы объединим усилия библиотек NumPy и Pandas в деле соз-
дания новых признаков в наборе данных на основе существующих. Цель
этого одна – улучшить качество предсказаний итоговой модели, снабдив ее
скрытыми шаблонами и зависимостями, недоступными в исходном наборе
данных.
Итак, давайте расширим наш набор данных за счет новых полезных при-
знаков, как показано в примере ниже:
import pandas as pd
import numpy as np
from sklearn.preprocessing import StandardScaler
# Простой набор данных с транзакциями по продаже
data={
'CustomerID': [1, 2, 3, 4, 5, 6, 7, 8],
'PurchaseAmount': [250, 400, 300, 400, 150, 150, 500, 350],
'Discount': [10, 15, 20, 30, 5, 12, 25, 18],
'Store': ['A', 'B', 'A', 'C', 'B', 'C', 'A', 'B'],
'CustomerAge': [35, 42, 28, 28, 50, 39, 45, 33],
'LoyaltyScore': [75, 90, 60, 85, 65, 70, 95, 80]
}
df = pd.DataFrame(data)
# Создаем новый признак с суммой без скидки
df['NetPurchase'] = df['PurchaseAmount'] - df['Discount']
# Создаем переменную взаимодействия путем перемножения столбцов PurchaseAmount и Discount
df['Interaction_Purchase_Discount'] = df['PurchaseAmount'] * df['Discount']
# Создаем бинарный признак, говорящий о крупной покупке
df['HighValue'] = (df['PurchaseAmount'] > 300).astype(int)
# Создаем признак с процентом скидки
df['DiscountPercentage'] = (df['Discount'] / df['PurchaseAmount']) * 100
126 Оптимизация потоков данных
# Создаем группы возрастов
df['AgeGroup'] = pd.cut(df['CustomerAge'], bins=[0, 30, 50, 100], labels=['Young',
'Middle', 'Senior'])
# Создаем признак, разделяющий покупки по уровню лояльности покупателя к магазину
df['LoyaltyTier'] = pd.cut(df['LoyaltyScore'], bins=[0, 60, 80, 100], labels=['Bronze',
'Silver', 'Gold'])
# Создаем признак со средней покупкой в расчете на один балл лояльности
df['PurchasePerLoyaltyPoint'] = df['PurchaseAmount'] / df['LoyaltyScore']
# Стандартизируем числовые признаки
scaler = StandardScaler()
numeric_features = ['PurchaseAmount', 'Discount', 'NetPurchase', 'LoyaltyScore']
df[numeric_features] = scaler.fit_transform(df[numeric_features])
# Кодируем категориальные переменные с одним активным состоянием
df = pd.get_dummies(df, columns=['Store', 'AgeGroup', 'LoyaltyTier'])
print("\nИнформация о наборе данных:")
print(df.info())
print("\nСуммарная статистика:")
print(df.describe())
Вывод:
Информация о наборе данных:
<class 'pandas.core .frame.DataFrame'>
RangeIndex: 8 entries, 0 to 7
Data columns (total 19 columns):
# Column
Non-Null Count Dtype
---
------
--------------
-----
0 CustomerID
8 non-null
int64
1 PurchaseAmount
8 non-null
float64
2 Discount
8 non-null
float64
3 CustomerAge
8 non-null
int64
4 LoyaltyScore
8 non-null
float64
5 NetPurchase
8 non-null
float64
6 Interaction_Purchase_Discount 8 non-null
int64
7 HighValue
8 non-null
int64
8 DiscountPercentage
8 non-null
float64
9 PurchasePerLoyaltyPoint
8 non-null
float64
10 Store_A
8 non-null
bool
11 Store_B
8 non-null
bool
12 Store_C
8 non-null
bool
13 AgeGroup_Young
8 non-null
bool
14 AgeGroup_Middle
8 non-null
bool
15 AgeGroup_Senior
8 non-null
bool
16 LoyaltyTier_Bronze
8 non-null
bool
17 LoyaltyTier_Silver
8 non-null
bool
18 LoyaltyTier_Gold
8 non-null
bool
dtypes: bool(9), float64(6), int64(4)
memory usage: 840.0 bytes
None
Комбинирование инструментов для выполнения эффективного анализа данных 127
Суммарная статистика:
CustomerID PurchaseAmount Discount CustomerAge LoyaltyScore \
count
8.00000 8 .000000e+00 8.000000
8.000000 8 .000000e+00
mean
4.50000 -1 .387779e-17 0.000000 37.500000 -1 .387779e-17
std
2.44949 1.069045e+00 1.069045
7.946248 1.069045e+00
min
1.00000
- 1 .393746e+00 -1 .557796 28.000000
-1 .527525e+00
25%
2.75000 -7 .504788e-01 -0 .705108 31.750000 -7 .637626e-01
50%
4.50000 1.072113e-01 -0 .049194 37.000000 0 .000000e+00
75%
6.25000 7.504788e-01 0.573925 42.750000 7.637626e-01
max
8.00000 1.608169e+00 1.721774 50.000000 1.527525e+00
NetPurchase Interaction_Purchase_Discount HighValue \
count 8.000000e+00
8.000000 8 .000000
mean -1 .387779e-17
5981.250000 0.500000
std 1.069045e+00
4404.375868 0.534522
min -1 .424955e+00
750.000000 0 .000000
25% -7 .175627e-01
2325.000000 0 .000000
50% 9.379166e-02
6000.000000 0 .500000
75% 7.062625e-01
7725.000000 1.000000
max
1.621579e+00
12500.000000 1.000000
DiscountPercentage PurchasePerLoyaltyPoint
count
8.000000
8.000000
mean
5.424107
3.946546
std
1.770776
1.205136
min
3.333333
2.142857
25%
3.937500
3.076923
50%
5.071429
4.409722
75%
6.875000
4.779412
max
8.000000
5.263158
Здесь никаких дополнительных пояснений, помимо приведенных в ком-
ментариях в коде, не требуется.
2.3.3. Шаг 3: построение модели машинного
обучения с помощью Scikit-learn
После очистки данных и их обогащения за счет новых признаков можно стро-
ить предсказательную модель. Библиотека Scikit-learn предлагает полно-
ценный арсенал инструментов для создания моделей любых типов, включая
регрессию, классификацию, кластеризацию и снижение размерности.
Помимо алгоритмов машинного обучения, эта библиотека также предла-
гает вам воспользоваться своими универсальными конвейерами, метриками
оценки качества моделей и инструментами для подбора гиперпараметров.
Рассмотрим пример построения модели случайного леса для предсказания
совершения крупной покупки (на сумму выше $300):
import pandas as pd
import numpy as np
128 Оптимизация потоков данных
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
from sklearn.preprocessing import StandardScaler
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
# Загружаем данные (предположим, что данные в переменной df уже есть из предыдущего
примера)
# df = pd.read_csv('your_data.csv')
# Определяем признаки и целевую переменную
X = df[['PurchaseAmount', 'Discount', 'NetPurchase', 'LoyaltyScore', 'CustomerAge']]
y = df['HighValue']
# Разделяем набор данных на обучающую и тестовую выборки
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# Создаем конвейер
pipeline = Pipeline([
('imputer', SimpleImputer(strategy='median')),
('scaler', StandardScaler()),
('classifier', RandomForestClassifier(random_state=42))
])
# Определяем гиперпараметры для настройки
param_grid = {
'classifier__n_estimators': [100, 200, 300],
'classifier__max_depth': [None, 5, 10],
'classifier__min_samples_split': [2, 5, 10]
}
# Осуществляем поиск гиперпараметров по сетке
grid_search = GridSearchCV(pipeline, param_grid, cv=5, scoring='accuracy', n_jobs=-1)
grid_search.fit(X_train, y_train)
# Выбираем наилучшую модель
best_model = grid_search.best_estimator_
# Делаем предсказания на тестовой выборке
y_pred = best_model.predict(X_test)
# Оцениваем качество модели
accuracy = accuracy_score(y_test, y_pred)
conf_matrix = confusion_matrix(y_test, y_pred)
class_report = classification_report(y_test, y_pred)
# Выводим результаты
print(f"Оптимальные параметры: {grid_search.best_params_}")
print(f"Точность модели: {accuracy:.2f}")
print("\nМатрица несоответствий:")
print(conf_matrix)
print("\nОтчет о классификации:")
print(class_report)
Комбинирование инструментов для выполнения эффективного анализа данных 129
# Определяем значимость предикторов
feature_importance = best_model.named_steps['classifier'].feature_importances_
feature_names = X.columns
for name, importance in zip(feature_names, feature_importance):
print(f"{name}: {importance:.4f}")
Что здесь происходит? Сначала, как и всегда, мы импортируем нужные
нам библиотеки, включая pandas, numpy и различные модули из библиотеки
Scikit-learn. Предполагаем, что наши данные уже сохранены в переменную
df. Определяем признаки (X) и целевую переменную (y) модели. Здесь мы
воспользовались обогащенным набором предикторов, включающим Loy-
altyScore и CustomerAge. Далее мы разделяем набор данных на обучающую
и тестовую выборки. Во второй набор мы помещаем 30 % наблюдений. После
этого создаем конвейер Scikit-learn при помощи класса Pipeline для облег-
чения процесса очистки данных и моделирования. В конвейер мы включи-
ли классы SimpleImputer для подстановки пропущенных значений, S tandard-
Scaler для стандартизации признаков и RandomForestClassifier для создания
модели. Затем мы определили сетку из гиперпараметров для подбора их
оптимальных значений и запустили поиск с использованием 5-блочной
кросс-валидации. Далее мы воспользовались оптимальным набором ги-
перпараметров для предсказания на тестовой выборке. В качестве метрик
для оценки качества модели мы вывели качество модели, матрицу несоот-
ветствий (таблица с правильными и неправильными прогнозами), а также
полный отчет о классификации, включая метрики точности, полноты и F1-
меру. В завершение мы вывели информацию о значимости предикторов
в нашей модели.
Этот пример демонстрирует полноценный подход к построению и оценке
качества модели машинного обучения. Здесь мы воспользовались конвей-
ером, объединившим в себе стадии предварительной подготовки данных
и создания модели, а также выполнили подбор гиперпараметров по сетке
и вывели отчет о качестве полученной модели. А дополнительный отчет
о значимости предикторов позволил нам понять, какие из них оказывают
большое влияние на качество предсказаний.
2.3.4. Шаг 4: оптимизация рабочих процессов
с помощью конвейеров Scikit-learn
При усложнении рабочих процессов вам непременно захочется автомати-
зировать некоторые рутинные действия, которые приходится повторять
из раза в раз. И в этом вам помогут конвейеры из библиотеки Scikit-learn,
которые мы уже не раз использовали в этой книге, а сейчас закрепим прой-
денное.
Давайте создадим один общий конвейер, который будет включать в себя
этапы предварительной подготовки данных, создания модели и подбора оп-
тимальных гиперпараметров:
130 Оптимизация потоков данных
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
# Создаем простой набор данных
np.random.seed(42)
df = pd.DataFrame({
'PurchaseAmount': np.random.uniform(50, 500, 1000),
'Discount': np.random.uniform(0, 50, 1000),
'LoyaltyScore': np.random.randint(0, 100, 1000),
'CustomerAge': np.random.randint(18, 80, 1000),
'Store': np.random.choice(['A', 'B', 'C'], 1000)
})
df['HighValue'] = (df['PurchaseAmount'] > 300).astype(int)
# Определяем признаки и целевую переменную
X = df.drop('HighValue', axis=1)
y = df['HighValue']
# Разделяем набор данных на обучающую и тестовую выборки
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# Определяем действия для числовых столбцов (масштабирование)
numeric_features = ['PurchaseAmount', 'Discount', 'LoyaltyScore', 'CustomerAge']
numeric_transformer = Pipeline(steps=[
('imputer', SimpleImputer(strategy='median')),
('scaler', StandardScaler())
])
# Определяем действия для категориальных столбцов (кодирование)
categorical_features = ['Store']
categorical_transformer = Pipeline(steps=[
('imputer', SimpleImputer(strategy='constant', fill_value='missing')),
('onehot', OneHotEncoder(handle_unknown='ignore'))
])
# Объединяем шаги предварительной обработки
preprocessor = ColumnTransformer(
transformers=[
('num', numeric_transformer, numeric_features),
('cat', categorical_transformer, categorical_features)
])
# Создаем конвейер для предварительной обработки данных и обучения модели
pipeline = Pipeline(steps=[
('preprocessor', preprocessor),
('classifier', RandomForestClassifier(random_state=42))
])
Комбинирование инструментов для выполнения эффективного анализа данных 131
# Определяем пространство гиперпараметров
param_grid = {
'classifier__n_estimators': [100, 200, 300],
'classifier__max_depth': [None, 5, 10],
'classifier__min_samples_split': [2, 5, 10]
}
# Настраиваем поиск по сетке
grid_search = GridSearchCV(pipeline, param_grid, cv=5, scoring='accuracy', n_jobs=-1)
# Осуществляем подбор гиперпараметров
grid_search.fit(X_train, y_train)
# Получаем оптимальную модель
best_model = grid_search.best_estimator_
# Делаем предсказания на тестовой выборке
y_pred = best_model.predict(X_test)
# Оцениваем качество модели
accuracy = accuracy_score(y_test, y_pred)
conf_matrix = confusion_matrix(y_test, y_pred)
class_report = classification_report(y_test, y_pred)
# Выводим результаты
print(f"Оптимальные параметры: {grid_search.best_params_}")
print(f"Точность модели: {accuracy:.2f}")
print("\nМатрица несоответствий:")
print(conf_matrix)
print("\nОтчет о классификации:")
print(class_report)
# Значимость признаков
feature_importance = best_model.named_steps['classifier'].feature_importances_
feature_names = numeric_features + list(best_model.named_steps['preprocessor']
. na m ed_transformers_['cat']
. na m ed_steps['onehot']
. get_feature_names_out(categorical_features))
for name, importance in zip(feature_names, feature_importance):
print(f"{name}: {importance:.4f}")
Вывод:
Оптимальные параметры: {'classifier__max_depth': None, 'classifier__min_samples_split': 2,
'classifier__n _estimators': 100}
Точность модели: 1.00
Матрица несоответствий:
[[174 0]
[ 0 126]]
Отчет о классификации:
precision
recall f1-score support
0
1.00
1.00
1.00
174
1
1.00
1.00
1.00
126
132 Оптимизация потоков данных
accuracy
1.00
300
macro avg
1.00
1.00
1.00
300
weighted avg
1.00
1.00
1.00
300
PurchaseAmount: 0.9161
Discount: 0.0276
LoyaltyScore: 0.0242
CustomerAge: 0.0255
Store_A: 0.0021
Store_B: 0.0020
Store_C: 0.0025
Здесь мы создали простой набор данных и дополнили его булевой рас-
четной переменной HighValue, говорящей о том, превышает ли сумма по-
купки $300. Именно эту переменную мы определили в модели в качестве
целевой. Далее мы разделили набор данных на выборки. После этого созда-
ли отдельные конвейеры для обработки числовых и категориальных при-
знаков. В числовых мы заполнили пропуски медианой, а в категориаль-
ных – константой 'missing', после чего произвели их кодирование с одним
активным состоянием. Эти конвейеры мы объединили вместе посредством
класса ColumnTransformer. Далее мы создали общий конвейер, объединивший
в себе этапы предварительной обработки данных и создание модели с по-
мощью класса RandomForestClassifier. Затем подобрали гиперпараметры, как
и в предыдущем примере, обучили модель и рассчитали метрики качества.
В завершение вновь определили значимость разных предикторов, выяснив,
что наибольший вклад, как и ожидалось, в предсказания вносит признак
PurchaseAmount.
2.4. Практические упражнения
Теперь, когда вы завершили чтение второй главы, вы можете попробовать
применить полученные знания на практике при решении упражнений на
использование библиотек Pandas, NumPy и Scikit-learn.
Упражнение 1. Расширенная фильтрация данных
при помощи Pandas
Есть набор данных с заказами:
import pandas as pd
# Sample data: Online orders
data = {'OrderID': [1, 2, 3, 4, 5],
'CustomerID': [101, 102, 103, 101, 104],
'Category': ['Electronics', 'Clothing', 'Electronics', 'Furniture', 'Furniture'],
'OrderAmount': [250, 120, 300, 400, 500]}
df = pd.DataFrame(data)
Практические упражнения 133
Ваша задача состоит в том, чтобы:
1) отфильтровать набор данных таких образом, чтобы в нем остались
только заказы с суммой, превышающей 200;
2) сгруппировать заказы по столбцам Category и CustomerID и вычислить
общую и среднюю сумму для каждой группы;
3) построить сводную таблицу с полем Category в столбцах, а CustomerID –
в строках.
Решение
# Шаг 1: Фильтруем заказы по полю OrderAmount > 200
filtered_df = df[df['OrderAmount'] > 200]
# Шаг 2: Группируем заказы по столбцам Category и CustomerID и рассчитываем общие и средние
суммы
grouped_df = filtered_df .groupby(['Category', 'CustomerID']).agg(
TotalAmount=('OrderAmount', 'sum'),
AvgAmount=('OrderAmount', 'mean')
).reset_index()
# Шаг 3: Создаем сводную таблицу
pivot_df = grouped_df .pivot(index='CustomerID', columns='Category', values='TotalAmount').
fillna(0)
print(pivot_df)
Вывод:
Category Electronics Furniture
CustomerID
101
250.0
400.0
103
300.0
0.0
104
0.0
500.0
Упражнение 2. Расширенный анализ с помощью библиотеки
NumPy
Есть массив с ценами на товары:
import numpy as np
# Простой набор данных: цены на товары
prices = np.array([100, 150, 200, 250, 300])
Ваша задача состоит в том, чтобы:
1) применить к массиву значений логарифмическое преобразование
с целью их нормализации;
2) воспользоваться транслированием для применения 20-процентной
скидки ко всем ценам;
3) рассчитать среднюю цену со скидкой с использованием векторизован-
ных операций.
134 Оптимизация потоков данных
Решение
import numpy as np
# Простой набор данных: цены на товары
prices = np.array([100, 150, 200, 250, 300])
# Шаг 1: Применяем логарифмическое преобразование
log_prices = np.log(prices)
# Шаг 2: Применяем 20-процентную скидку
discounted_prices = prices * 0.80
# Шаг 3: Рассчитываем среднюю цену со скидкой
average_discounted_price = np.mean(discounted_prices)
print("Цены после логарифмирования:", log_prices)
print("Цены со скидкой:", discounted_prices)
print("Средняя цена со скидкой:", average_discounted_price)
Вывод:
Цены после логарифмирования: [4.60517019 5.01063529 5.29831737 5.52146092 5.70378247]
Цены со скидкой: [ 80. 120. 160. 200. 240.]
Средняя цена со скидкой: 160.0
Упражнение 3. Использование Pandas и NumPy
для конструирования признаков
Есть набор данных с суммами покупок и скидками:
import pandas as pd
import numpy as np
# Набор данных
data = {'CustomerID': [1, 2, 3, 4, 5],
'PurchaseAmount': [250, np.nan, 300, 400, np.nan],
'Discount': [10, 15, 20, np.nan, 5]}
df = pd.DataFrame(data)
Ваша задача состоит в том, чтобы:
1) заполнить пропуски в столбцах PurchaseAmount и Discount средними зна-
чениями по столбцам;
2) создать новый признак с именем NetPurchase, рассчитываемый как сум-
ма покупки после применения скидки;
3) воспользоваться NumPy для создания переменной взаимодействия
с именем Interaction_Purchase_Discount, выраженной через произведе-
ние столбцов PurchaseAmount и Discount.
Решение
# Шаг 1: Заполнение пропусков
df['PurchaseAmount'] = df['PurchaseAmount'].fillna(df['PurchaseAmount'].mean())
Практические упражнения 135
df['Discount'] = df['Discount'].fillna(df['Discount'].mean())
# Шаг 2: Создание признака NetPurchase
df['NetPurchase'] = df['PurchaseAmount'] - df['Discount']
# Шаг 3: Создание переменной взаимодействия при помощи NumPy
df['Interaction_Purchase_Discount'] = df['PurchaseAmount'] * df['Discount']
print(df)
Вывод:
CustomerID PurchaseAmount Discount NetPurchase \
0
1
250.000000
10.0 240.000000
1
2
316.666667
15.0 301.666667
2
3
300.000000
20.0 280.000000
3
4
400.000000
12.5 387.500000
4
5
316.666667
5.0 311.666667
Interaction_Purchase_Discount
0
2500.000000
1
4750.000000
2
6000.000000
3
5000.000000
4
1583.333333
Упражнение 4. Построение модели классификации
с помощью Scikit-learn
Есть набор данных с транзакциями покупок и скидками:
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
import pandas as pd
import numpy as np
# Набор данных
data = {'CustomerID': [1, 2, 3, 4, 5],
'PurchaseAmount': [250, 350, 300, 400, 150],
'Discount': [10, 15, 20, 5, 5]}
df = pd.DataFrame(data)
Ваша задача состоит в том, чтобы:
1) создать столбец для целевой переменной, в котором будут стоять еди-
ницы для транзакций с суммой выше $300 и нули – для остальных;
2) воспользоваться библиотекой Scikit-learn для разделения набора дан-
ных на обучающую и тестовую выборки;
3) построить модель случайного леса для предсказания значений создан-
ной переменной;
4) оценить качество полученной модели, выполнив предсказания на тес-
товой выборке.
136 Оптимизация потоков данных
Решение
# Шаг 1: Создаем целевую переменную (PurchaseAmount > 300)
df['HighValue'] = (df['PurchaseAmount'] > 300).astype(int)
# Шаг 2: Определяем признаки и целевую переменную
X = df[['PurchaseAmount', 'Discount']]
y = df['HighValue']
# Шаг 3: Разделяем набор данных на обучающую и тестовую выборки
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# Шаг 4: Строим и обучаем модель случайного леса
clf = RandomForestClassifier(random_state=42)
clf.fit(X_train, y_train)
# Шаг 5: Предсказываем значения и оцениваем качество модели
y_pred = clf.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
print(f"Точность модели: {accuracy:.2f}")
Вывод:
Точность модели: 0.50
Упражнение 5. Использование конвейеров Scikit-learn
при построении рабочих процессов
Есть набор данных с транзакциями покупок и скидками:
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
import pandas as pd
import numpy as np
# Набор данных
data = {'CustomerID': [1, 2, 3, 4, 5],
'PurchaseAmount': [250, np.nan, 300, 400, np.nan],
'Discount': [10, 15, 20, np.nan, 5]}
df = pd.DataFrame(data)
Ваша задача состоит в том, чтобы:
1) создать конвейер Scikit-learn, сочетающий в себе подстановку про-
пущенных значений, масштабирование признаков и обучение модели
случайного леса;
2) запустить конвейер на обучающей выборке и проверить качество полу-
ченной модели на тестовых данных.
Возможные проблемы 137
Решение
# Шаг 1: Определяем признаки и целевую переменную
df['HighValue'] = (df['PurchaseAmount'] > 300).astype(int)
X = df[['PurchaseAmount', 'Discount']]
y = df['HighValue']
# Разделяем на обучение и тест
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# Шаг 2: Создаем конвейер
pipeline = Pipeline(steps=[
('imputer', SimpleImputer(strategy='mean')), # Замена пропусков
('scaler', StandardScaler()), # Масштабирование
('classifier', RandomForestClassifier(random_state=42)) # Обучение модели
])
# Шаг 3: Запуск конвейера
pipeline.fit(X_train, y_train)
# Шаг 4: Предсказания и оценка качества модели
y_pred = pipeline.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
print(f"Точность модели: {accuracy:.2f}")
Вывод:
Точность модели: 1.00
2.5. Возможные проблемы
При использовании библиотек Pandas, NumPy и Scikit-learn вы можете столк-
нуться с распространенными проблемами, которые мы перечислим в этом
разделе. Постараемся дать советы, как можно их избежать.
2.5.1. Большие накладные расходы при работе
с большими наборами данных в Pandas
При росте объемов данных, с которыми вам необходимо работать, скорость
их обработки в Pandas может падать. Причин может быть множество, вот
лишь некоторые из них.
Что может пойти не так:
обработка больших наборов данных без оглядки на стратегию исполь-
зования ресурсов, включая память и трафик, может привести к значи-
тельному замедлению рабочих процессов;
избыточное использование типов данных по умолчанию (float64 или
int64) для числовых переменных может обернуться большими расхо-
дами памяти.
138 Оптимизация потоков данных
Решение
Всегда, когда это возможно, используйте приведение типов данных. Также
вы можете обрабатывать большие наборы данных по частям или восполь-
зоваться библиотеками Dask или Vaex, о которых мы будем говорить далее
в этой книге, для эффективной работы с большими данными.
2.5.2. Игнорирование или неправильное
использование векторизации в NumPy
Векторизованные операции призваны ускорить выполнение преобразова-
ний и математических операций, но с ними нужно правильно обращаться.
Что может пойти не так:
отказ от использования векторизации в пользу традиционных циклов
может привести к существенному снижению быстродействия;
неправильное применение концепции транслирования, кроющееся
в несоблюдении двух главных правил, о которых мы писали, может
приводить к тому, что массивы окажутся несопоставимых размеров,
вследствие чего возникнет ошибка.
Решение
Всегда, когда это возможно, используйте векторизованные операции, реа-
лизованные в NumPy, и внимательно следите за тем, чтобы массивы при
транслировании могли быть приведены к сопоставимым размерам.
2.5.3. Утечка информации в конвейерах Scikit-learn
Как мы уже говорили, конвейеры являются очень мощным инструментом
анализа данных, но при неправильном их использовании вы можете столк-
нуться с так называемой утечкой информации, когда наблюдения из тесто-
вой выборки непреднамеренно участвуют в обучении модели.
Что может пойти не так:
к утечке информации может привести ситуация, когда шаги по предва-
рительной подготовке данных, связанные с масштабированием, заме-
ной пропусков и преобразованием признаков, выполняются примени-
тельно ко всему набору данных, перед его разделением на обучающую
и тестовую выборки;
применение преобразований только к обучающим данным в процессе
кросс-валидации может привести к чрезмерно оптимистичной оценке
качества модели.
Решение
Всегда следите за тем, чтобы все предварительные шаги по подготовке
данных выполнялись в рамках конвейера Scikit-learn. Это гарантирует при-
менение преобразований только к обучающим данным и использование
Возможные проблемы 139
полученных данных при преобразовании тестовых данных, что позволит
избежать утечки.
2.5.4. Чрезмерная надежда на значения
гиперпараметров модели по умолчанию
Многие модели Scikit-learn работают вполне успешно с параметрами по
умолчанию, но если не предпринять попытку поиска более оптимальных
значений, можно лишить будущую модель обобщающей способности.
Что может пойти не так:
отказ от поиска оптимальных значений гиперпараметров модели мо-
жет привести к снижению ее качества;
при несоответствии гиперпараметров характеристикам ваших данных
вы можете получить недообучение или переобучение модели.
Решение
Всегда используйте техники подбора оптимальных значений гиперпара-
метров, такие как поиск по сетке или случайный подбор. Классы GridSearchCV
и RandomizedSearchCV, входящие в состав библиотеки Scikit-learn, помогут вам
с поиском компромиссов.
2.5.5. Излишняя сложность конвейеров
Хотя конвейеры Scikit-learn позволяют значительно упростить рутинные
задачи, стоящие перед аналитиками, зачастую их самих делают излишне
сложными, что может приводить к проблемам.
Что может пойти не так:
конвейеры с избыточным количеством преобразований или моделей
может быть сложно поддерживать и отлаживать;
лишние шаги могут привести к замедлению работы конвейера и повы-
шению риска возникновения ошибок.
Решение
Не усложняйте без необходимости свои конвейеры и сосредоточьтесь на
действительно нужных преобразованиях. Включайте в конвейеры только те
шаги, которые гарантированно повысят качество итоговой модели. Прове-
ряйте шаги по отдельности и решите, каким из них можно выделить драго-
ценное место в конвейере.
140 Оптимизация потоков данных
Заключение
В этой главе мы рассмотрели критически важные концепции и техники, по-
зволяющие оптимизировать рабочие процессы в аналитике. Глава была по-
делена на три основных раздела, посвященных важным библиотекам Pandas,
NumPy и Scikit-learn.
Сначала мы рассмотрели эти библиотеки по отдельности, а затем научи-
лись объединять их инструменты с помощью конвейеров.
Мы рассмотрели множество примеров и научились конструировать до-
вольно сложные конвейеры, сочетающие в себе все необходимые операции
для подготовки данных, создания модели, ее обучения и проверки качества.
В следующей главе мы погрузимся в более сложные техники конструиро-
вания признаков, которые позволят повысить качество итоговой модели.
Контрольный опрос.
Часть I. Подготовка
данных для дальнейшего
анализа
Приведенные ниже вопросы помогут вам вспомнить все самое важное, что
было пройдено в двух первых главах книги. Ответьте на вопросы, чтобы
убедиться, что вы готовы двигаться дальше.
Вопрос 1: манипулирование данными в Pandas
Назовите главное преимущество использования библиотеки Pandas для ма-
нипулирования данными в сравнении с применением традиционных спис-
ков и словарей Python.
a) в Pandas встроены возможности по визуализации данных;
b) с помощью Pandas можно эффективно работать с большими наборами
табличных данных;
c) библиотека Pandas автоматически масштабирует признаки для моде-
лей машинного обучения;
d) Pandas хорошо интегрируется с традиционными циклами Python.
Вопрос 2: эффективная фильтрация данных с Pandas
Как бы вы с помощью библиотеки Pandas отфильтровали датафрейм таким
образом, чтобы оставить только строки с SalesAmount > 200 и Store = ‘A’ ?
a) df[(df['SalesAmount'] > 200) & (df['Store'] == 'A')];
b) df.filter(SalesAmount > 200 & Store == 'A');
c) df.query('SalesAmount > 200' & 'Store == "A"');
d) df.where('SalesAmount' > 200 and df['Store'] == 'A').
Вопрос 3: эффективные вычисления с помощью NumPy
Какая из перечисленных ниже операций не оптимизирована при помощи
векторизованного подхода, характерного для NumPy?
142 Контрольный опрос. Часть I. Подготовка данных для дальнейшего анализа
a) поэлементное сложение значений в массивах;
b) матричное умножение;
c) итерации по элементам с помощью циклов в Python;
d) применение математических преобразований вроде np.log.
Вопрос 4: транслирование в NumPy
Что означает термин транслирование в NumPy?
a) способность NumPy автоматически распараллеливать выполнение
операций по разным ядрам процессора;
b) процедура, выполняемая NumPy при попытке применения операций
к массивам разного размера;
c) оптимизационная техника, используемая в NumPy для хранения мас-
сивов в памяти;
d) способ замены пропущенных значений в массивах NumPy.
Вопрос 5: группировка и агрегация в Pandas
Как бы вы для приведенного ниже датафрейма рассчитали общую сумму
и среднее значение по полю PurchaseAmount с группировкой по столбцу Cate-
gory?
import pandas as pd
df = pd.DataFrame({
'CustomerID': [1, 2, 3, 4],
'Category': ['Electronics', 'Clothing', 'Electronics', 'Furniture'],
'PurchaseAmount': [200, 100, 300, 400]
})
a) df.groupby('Category').agg({'PurchaseAmount': ['sum', 'mean']});
b) df.filter('Category').groupby('PurchaseAmount').sum().mean();
c) df.pivot('Category').sum().mean('PurchaseAmount');
d) df.sum().groupby('PurchaseAmount').mean('Category').
Вопрос 6: конвейеры Scikit-learn
Назовите главное достоинство конвейеров из библиотеки Scikit-learn.
a) позволяют автоматически визуализировать результаты вычислений
после каждого шага;
b) позволяют объединить в цепочку шаги по предварительной обработке
данных и моделированию в один рабочий процесс;
c) снижают нагрузку на память при работе с большими наборами данных
за счет компрессии;
d) автоматически подбирают значения гиперпараметров для моделей
машинного обучения.
Контрольный опрос. Часть I. Подготовка данных для дальнейшего анализа 143
Вопрос 7: утечка информации в конвейерах машинного
обучения
Что называется утечкой информации и в чем состоит связанная с ней опас-
ность при построении моделей машинного обучения?
a) утечка информации относится к излишнему дублированию данных
в процессе обучения модели и приводит к чрезмерному расходованию
памяти;
b) утечка информации возникает, когда модель получает доступ к тесто-
вым данным в процессе обучения, что приводит к завышению качества
модели;
c) утечкой информации называется пропуск признаков в модели, что
ведет к снижению ее качества;
d) утечкой информации называется ее повреждение вследствие непра-
вильной загрузки в память.
Вопрос 8: оптимизация памяти в Pandas
Назовите основное преимущество приведения типов в Pandas.
a) повышает точность расчетов;
b) позволяет снизить расход памяти при работе с объемными наборами
данных;
c) позволяет Pandas хранить строки более эффективно;
d) автоматически приводит числовые столбцы к категориальному типу.
Вопрос 9: создание переменных взаимодействия
Как бы вы в процессе конструирования признаков создали переменную взаи-
модействия между столбцами PurchaseAmount и Discount с использованием
Pandas и NumPy?
a) df['Interaction'] = df['PurchaseAmount'] + df['Discount'];
b) df['Interaction'] = df['PurchaseAmount'] * df['Discount'];
c) df['Interaction'] = df['PurchaseAmount'] / df['Discount'];
d) df['Interaction'] = np.add(df['PurchaseAmount'], df['Discount']).
Вопрос 10: передискретизация временных рядов
Работая с временными рядами в Pandas, как бы вы изменили детализацию
данных с дневной на месячную и рассчитали общие суммы в разрезе меся-
цев?
a) df.resample('ME').sum();
b) df.resample('D').sum('ME');
c) df.resample('W').groupby('ME').sum();
d) df.groupby('ME').resample('D').sum().
144 Контрольный опрос. Часть I. Подготовка данных для дальнейшего анализа
Ответы
Вопрос 1
Правильный ответ – b: с помощью Pandas можно эффективно работать
с большими наборами табличных данных.
Вопрос 2
Правильный ответ – a: df[(df['SalesAmount'] > 200) & (df['Store'] == 'A')].
Вопрос 3
Правильный ответ – c: итерации по элементам с помощью циклов в Python.
Вопрос 4
Правильный ответ – b: процедура, выполняемая NumPy при попытке при-
менения операций к массивам разного размера.
Вопрос 5
Правильный ответ – a: df.groupby('Category').agg({'PurchaseAmount': ['sum',
'mean']}).
Вопрос 6
Правильный ответ – b: позволяют объединить в цепочку шаги по предвари-
тельной обработке данных и моделированию в один рабочий процесс.
Вопрос 7
Правильный ответ – b: утечка информации возникает, когда модель получает
доступ к тестовым данным в процессе обучения, что приводит к завышению
качества модели.
Вопрос 8
Правильный ответ – b: позволяет снизить расход памяти при работе с объ-
емными наборами данных.
Вопрос 9
Правильный ответ – b: df['Interaction'] = df['PurchaseAmount'] * df['Discount'].
Вопрос 10
Правильный ответ – a: df.resample('ME').sum().
ЧАСТЬ II
Конструирование
признаков для моделей
машинного обучения
Проект 1
Предсказание стоимости
домов с помощью
конструирования
признаков
Добро пожаловать в первый проект в рамках этой книги, в котором мы по-
строим модель машинного обучения, воспользовавшись для этого констру-
ированием признаков. В наборе данных, с которым мы будем работать, со-
брана полная информация о домах, включающая их расположение, размер,
количество комнат и другие характеристики, которые мы будем использо-
вать для предсказания рыночной стоимости интересующего нас дома.
Выбор модели очень важен, но иногда гораздо большую роль играет выбор
или создание правильного набора признаков. Мы можем как пользоваться
уже имеющимися переменными в наборе, так и конструировать новые на
их основе, что позволяет извлечь из исходных данных недоступные ранее
шаблоны.
Начнем с обзора набора данных и определения ключевых переменных,
после чего погрузимся в процедуру конструирования и отбора признаков.
Для начала нам необходимо выполнить очистку данных и их подготовку.
Исследование переменных
и очистка данных
Первым делом при работе с любым набором данных необходимо привести
его к виду, подходящему для использования в модели. Ключевые фазы этого
процесса следующие.
Исследование переменных и очистка данных 147
1. Загрузка и исследование данных: обзор структуры, содержимого
и особенностей набора данных, включая используемые типы данных,
диапазоны значений и т. д .
2. Обработка пропущенных значений: поиск и понимание природы
пропущенных значений в наборе данных.
3. Обработка выбросов: поиск и, при необходимости, устранение экс-
тремальных значений, способных исказить анализ. Выбросы могут
представлять собой как характерные для данных аномалии, так и оши-
бочные сведения.
4. Оценка качества данных: проверка данных на консистентность, на-
личие дубликатов и выявление возможных проблем с форматами.
5.
Базовый анализ признаков: выделение потенциально важных пере-
менных и связей между ними, а также целевой переменной.
Шаг 1. Загрузка и исследование данных
Начнем с загрузки нашего набора данных и визуального анализа имеющихся
в нем переменных
1
.
import pandas as pd
# Загружаем набор данных
df = pd.read_csv('house_prices.csv')
# Просматриваем первые несколько строк
print(df.head())
Вывод:
LotSize Neighborhood KitchenQual YearBuilt Bathrooms Bedrooms SalePrice
0 8450.0
CollgCr
Gd
2003
2
3 208500.0
1 9600.0
Veenker
TA
1976
2
3 181500.0
2 11250.0
CollgCr
Gd
2001
2
3 223500.0
3 9550.0
Crawfor
Gd
1915
1
3 140000.0
4 14260.0
NoRidge
Gd
2000
2
4 250000.0
Дадим краткое описание использованных в этом проекте столбцов. В поле
LotSize содержится общая площадь дома в кв. см. Поле Neighborhood отвечает
за соседство дома с важными объектами городской инфраструктуры. В столб-
це KitchenQual отражено качество кухни. Столбец YearBuilt хранит информа-
цию о годе постройки дома. Поля Bathrooms и Bedrooms отвечают за количество
ванных комнат и спален в доме соответственно. А столбец SalePrice – это
рыночная стоимость дома, наша целевая переменная.
1
Ссылку на сопроводительные файлы можно найти на странице книги на сайте из-
дательства «ДМК Пресс».
148 Предсказание стоимости домов с помощью конструирования признаков
Шаг 2. Обработка пропущенных значений
Наборы данных, с которыми приходится работать в реальной аналитике,
очень часто содержат пропущенные значения. Если говорить о нашем на-
боре, то, к примеру, пропуски в столбцах LotSize или YearBuilt могут сущест-
венно отразиться на итоговом анализе. Если в наборе будет много пропу-
щенных значений в столбце LotSize, это может привести к недооценке или
переоценке значимости площади дома при его оценке, притом что обычно
этот фактор является одним из решающих. Года постройки дома это касается
в такой же степени.
В то же время при простом удалении строк с пропусками в этих столбцах
мы можем недосчитаться важных сведений, что негативно скажется на на-
шей модели.
Мы заполним пропуски в этих столбцах при помощи медианного значе-
ния, хотя это также не всегда может быть правильно. Позже мы будем гово-
рить о более сложных способах подстановки.
# Проверяем датафрейм на пропущенные значения
missing_values = df.isnull().sum()
print(f'До обработки:\n{missing_values[missing_values > 0]}')
# Заполним пропуски в столбце LotSize медианой
df['LotSize'] = df['LotSize'].fillna(df['LotSize'].median())
# Удалим строки с пропусками в целевой переменной SalePrice
df = df.dropna(subset=['SalePrice'])
# Снова проверяем датафрейм на пропущенные значения
missing_values = df.isnull().sum()
print(f'\nПосле обработки:\n{missing_values[missing_values > 0]}')
Вывод:
До обработки:
LotSize
25
SalePrice 15
dtype: int64
После обработки:
Series([], dtype: int64)
В этом примере мы сначала проверили наличие пропусков в наборе дан-
ных, узнали, что пропущенные значения есть в двух столбцах, после чего
заменили пропуски в столбце LotSize на медиану, а строки с отсутствующими
значениями в целевой переменной SalePrice просто удалили.
В заключение мы убедились, что пропусков в нашем наборе данных не
осталось.
Исследование переменных и очистка данных 149
Шаг 3. Обработка выбросов
Выбросами называются значения, выбивающиеся из общего диапазона зна-
чений в столбце. В нашем конкретном случае причинами выбросов могут
быть как ошибки в данных, так и появление в наборе экстремально дорогих
или дешевых домов. На наличие выбросов могут влиять и временные взлеты,
и падения в экономике.
Давайте удалим наблюдения, в которых рыночная стоимость дома выходит
за границы полутора межквартильных размахов (расстояние от первого до
третьего квартиля) от соответствующих квартилей. Это поможет избавить
нашу модель от резких перепадов при обучении и прогнозировании.
import numpy as np
# Определяем выбросы при помощи метода межквартильного размаха
Q1 = df['SalePrice'].quantile(0.25)
Q3 = df['SalePrice'].quantile(0.75)
IQR=Q3-Q1
# Определяем порог для нахождения выбросов
outliers = df[(df['SalePrice'] < (Q1 - 1 .5 * IQR)) | (df['SalePrice'] > (Q3 + 1.5 * IQR))]
print(f"Количество выбросов в столбце SalePrice: {len(outliers)}")
# Удаляем выбросы
df = df[~((df['SalePrice'] < (Q1 - 1 .5 * IQR)) | (df['SalePrice'] > (Q3 + 1.5 * IQR)))]
Вывод:
Количество выбросов в столбце SalePrice: 60
Шаг 4. Корреляция переменных
Перед погружением в область конструирования признаков необходимо по-
нять зависимости между имеющимися предикторами и целевой перемен-
ной. В этом нам может помочь корреляционный анализ. С помощью него
можно узнать степень влияния отдельных переменных на рыночную стои-
мость дома.
Этот вид анализа не ограничивается обнаружением линейных зависимо-
стей, а помогает найти сложные взаимосвязи между предикторами и исполь-
зовать их совместно при создании новых признаков. К примеру, мы могли бы
обнаружить, что комбинация из расположения дома и его размера оказывает
большее влияние на стоимость в сравнении с каждым из этих предикторов по
отдельности. Такие нюансы играют важную роль при определении итогового
набора признаков для модели.
Кроме того, корреляционный анализ может помочь в выявлении избы-
точных переменных, не оказывающих существенного влияния на целевую
переменную.
150 Предсказание стоимости домов с помощью конструирования признаков
Пример корреляционного анализа:
import seaborn as sns
import matplotlib.pyplot as plt
# Строим корреляционную матрицу
correlation_matrix = df.corr(numeric_only=True)
# Визуализируем корреляционную матрицу при помощи тепловой карты
plt.figure(figsize=(10, 10))
sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm')
plt.show()
# Обращаем внимание на корреляцию предикторов с целевой переменной SalePrice
print(correlation_matrix['SalePrice'].sort_values(ascending=False))
Вывод:
SalePrice 1.000000
Bathrooms 0.574044
YearBuilt 0.526029
LotSize
0.259234
Bedrooms
0.178733
Name: SalePrice, dtype: float64
Рис. П1.1 Корреляционная матрица для набора данных
о стоимости домов
Конструирование признаков 151
В этом примере мы воспользовались для визуализации корреляционной
матрицы библиотекой seaborn. Поскольку в нашем наборе данных присут-
ствуют не только числовые переменные, мы передали методу corr() пара-
метр numeric_only=True, чтобы в расчет принимались только числовые пре-
дикторы. Далее мы вывели на экран информацию о корреляции каждого
столбца с переменной SalePrice. Как видите, в том виде, в котором у нас
сейчас находится датафрейм, наибольшее влияние на стоимость дома ока-
зывает количество ванных комнат и год постройки дома.
Конструирование признаков
После очистки и подготовки данных можно приступать к процессу кон-
струирования и отбора признаков. Именно на этом этапе аналитик может
проявить творческий подход к делу в полной мере. Основная его цель – та-
ким образом преобразовать исходный набор переменных в признаки, что-
бы выявить максимальное количество скрытых шаблонов и зависимостей
в данных.
В отношении набора данных с рыночными ценами на дома мы, напри-
мер, могли бы соорудить признак, отвечающий за индекс роскоши, который
включал бы в себя переменные, отвечающие за отделку дома (верхний цено-
вой сегмент), архитектурные особенности, характерные для дорогих домов,
и наличие дорогостоящего оборудования в доме. Также мы могли бы создать
признаки, отвечающие за рыночные тренды, включив в них исторические
цены на дом и местные экономические показатели. Это позволит нашей
модели учитывать динамику цен на дома на рынке.
В этом разделе мы коснемся трех аспектов конструирования признаков,
которые можно применить к нашему набору данных:
создание новых признаков: использование существующих перемен-
ных для создания новых;
кодирование категориальных переменных: приведение нечисловых
предикторов к числовому виду, понятному большинству моделей ма-
шинного обучения;
преобразование числовых переменных: применение математических
операций к переменным для лучшей интерпретации их взаимосвязей
с целевой переменной.
Создание новых признаков
При помощи новых признаков в наборе данных вы, кроме всего прочего,
можете учитывать нелинейные связи между переменными. К примеру, влия-
ние возраста дома может нелинейно влиять на его цену по причине того, что
очень старые дома могут стоит дороже из-за своей исторической ценности,
тогда как просто старые дома подобным бонусом похвастаться не могут.
152 Предсказание стоимости домов с помощью конструирования признаков
Создание дополнительных признаков может помочь отследить подобные
сложные зависимости между переменными.
Давайте создадим два новых признака в нашем наборе данных. Первый
назовем HouseAge, и он будет отвечать за возраст дома в годах, а второй – Lot-
SizePerBedroom – будет отражать площадь, приходящуюся на одну спальню:
# Возраст дома
df['HouseAge'] = 2024 - df['YearBuilt']
# Площадь, приходящаяся на одну спальню
df['LotSizePerBedroom'] = df['LotSize'] / df['Bedrooms']
# Смотрим первые несколько строк датафрейма с новыми признаками
print(df[['LotSize', 'Bedrooms', 'LotSizePerBedroom', 'YearBuilt', 'HouseAge']].head())
Вывод:
LotSize Bedrooms LotSizePerBedroom YearBuilt HouseAge
0 8450.0
3
2816.666667
2003
21
1 9600.0
3
3200.000000
1976
48
2 11250.0
3
3750.000000
2001
23
3 9550.0
3
3183.333333
1915
109
4 14260.0
4
3565.000000
2000
24
Новый признак LotSizePerBedroom предоставляет модели более детализи-
рованную информацию о распределении жилой площади в доме, что может
стать одним из важных факторов при определении его рыночной стоимости.
Кодирование категориальных переменных
В нашем наборе данных, посвященном рыночной стоимости домов, присут-
ствуют не только числовые переменные, но и категориальные. В то же вре-
мя большинство алгоритмов машинного обучения требуют на вход именно
числовые признаки.
Процесс преобразования переменных из категориальных в числовые име-
нуется кодированием (encoding) и часто применяется на этапе подготовки
данных для модели. Существует несколько способов кодирования категори-
альных переменных, но чаще всего используются кодирование с одним актив-
ным состоянием (one-hot encoding) и кодирование по меткам (label encoding).
Первый из них лучше подходит для категориальных переменных, не об-
ладающих какой-либо иерархией или порядком. При его применении для
каждого уникального значения в столбце создается отдельный столбец, ко-
торый заполняется единицей в случае, если для конкретного наблюдения
стоит именно это значение. В противном случае в столбце будут стоять нули.
Например, для столбца Neighborhood в нашем наборе данных этот тип кодиро-
вания приведет к созданию столбцов для каждого важного объекта городской
инфраструктуры, которые могут располагаться по соседству с домом.
Техника кодирования с одним активным состоянием позволяет модели
рассматривать каждую категорию измерения независимо, что нередко при-
Конструирование признаков 153
водит к обнаружению скрытых зависимостей между конкретными значения-
ми и целевой переменной.
При этом стоит отметить, что этот вид кодирования существенно увели-
чивает количество предикторов в итоговой модели, особенно если в катего-
риальном столбце присутствует большое количество уникальных значений.
Это может привести к явлению, известному под названием проклятие раз-
мерности (curse of dimensionality), что может потребовать дополнительного
отбора переменных.
# Кодируем поле Neighborhood с одним активным состоянием
df = pd.get_dummies(df, columns=['Neighborhood'])
# Просматриваем результат
print(df.head())
Вывод:
LotSize KitchenQual YearBuilt ... Neighborhood_StoneBr Neighborhood_Timber Neighborhood_Veenker
0 8450.0
3 (Good)
2003 ...
False
False
False
1 9600.0 2 (Typical)
1976 ...
False
False
True
2 11250.0
3 (Good)
2001 ...
False
False
False
3 9550.0
3 (Good)
1915 ...
False
False
False
4 14260.0
3 (Good)
2000 ...
False
False
False
[5 rows x 33 columns]
Здесь мы воспользовались функцией pd.get_dummies(), которая создала
в наборе данных по одному столбцу для каждого уникального значения
в поле Neighborhood. Как видите, количество столбцов в наборе данных вы-
росло аж до 33.
Второй подход, известный как кодирование по меткам, не увеличивает
количество столбцов в наборе данных, а вместо этого каждое уникальное
значение в столбце преобразует в очередное целое число по порядку. Этот
метод можно использовать в случаях, когда значения в столбце обладают
некой иерархией или установленным порядком.
В нашем случае мы можем применить этот вид кодирования к столбцу
KitchenQual, отвечающему за качество кухни. Значения в этом столбце могут
быть следующими: 4 (Excellent) (превосходное), 3 (Good) (хорошее), 2 (Typi-
cal) (типичное/среднее), 1 (Fair) (удовлетворительное) или 0 (Poor) (плохое).
В процессе кодирования каждому уникальному значению будет дано цело-
численное значение.
При этом необходимо использовать кодирование по меткам с большой
осторожностью. Если применить его к столбцу, в котором отсутствует внут-
ренняя иерархия или порядок, модель может начать обнаруживать неже-
лательные зависимости в данных. К примеру, если так закодировать цвета
красный, синий и зеленый, то модель может в какой-то момент предполо-
жить, что красный цвет больше похож на зеленый, чем на синий.
При использовании этого вида кодирования важно задокументировать
схему кодирования и применять ее при интерпретации модели.
154 Предсказание стоимости домов с помощью конструирования признаков
from sklearn.preprocessing import LabelEncoder
# Кодируем столбец KitchenQual по меткам
label_encoder = LabelEncoder()
df['KitchenQualEncoded'] = label_encoder.fit_transform(df['KitchenQual'])
# Просматриваем результат
print(df[['KitchenQual', 'KitchenQualEncoded']])
print(label_encoder.classes_)
Вывод:
KitchenQual KitchenQualEncoded
0
3 (Good)
2
1
2 (Typical)
1
2
3 (Good)
2
3
3 (Good)
2
4
3 (Good)
2
...
...
...
1434 2 (Typical)
1
1435
3 (Good)
2
1436 4 (Excellent)
3
1437
1 (Fair)
0
1438
3 (Good)
2
[1439 rows x 2 columns]
['1 (Fair)' '2 (Typical)' '3 (Good)' '4 (Excellent)']
Здесь мы воспользовались классом LabelEncoder для приведения значений
из столбца KitchenQual к числовому формату. Также мы вывели все получив-
шиеся классы нового предиктора с помощью свойства classes_.
Преобразование числовых переменных
На этапе предварительной подготовки данных для моделей машинного
обучения очень важно выполнить преобразование числовых переменных
везде, где это необходимо, особенно когда дело касается смещенных рас-
пределений значений в столбцах. Два наиболее часто встречающихся преоб-
разования – это нормализация и логарифмическое преобразование.
Логарифмическое преобразование бывает полезно в случаях, когда диа-
пазон значений в переменной очень большой и смещенный. Применитель-
но к нашему набору данных это может касаться, например, переменных
SalePrice и LotSize. Применяя логарифмическое преобразование, мы как
бы сжимаем масштаб для больших значений и расширяем – для маленьких.
Это позволяет избавиться от смещенности распределения, нивелировать
влияние выбросов и сделать зависимости между переменными более ли-
нейными.
К примеру, после логарифмического преобразования стоимости для до-
мов, равные $1 000 000 и $100 000, получат значения 13.82 и 11.51 соответ-
ственно.
Конструирование признаков 155
Но применять логарифмическое преобразование следует с осторожностью.
Наиболее эффективным оно будет в случае, если значения скошены в боль-
шую сторону, к тому же они должны быть положительными. Кроме того,
интерпретировать результаты модели тоже нужно с оглядкой на логарифми-
рование и помнить, что исходные масштабы переменных были изменены.
# Применяем логарифмическое преобразование к столбцам SalePrice и LotSize
df['LogSalePrice'] = np.log(df['SalePrice'])
df['LogLotSize'] = np.log(df['LotSize'])
# Смотрим результаты
print(df[['SalePrice', 'LogSalePrice', 'LotSize', 'LogLotSize']].head())
Вывод:
SalePrice LogSalePrice LotSize LogLotSize
0 208500.0
12.247694 8450.0 9.041922
1 181500.0
12.109011 9600.0 9.169518
2 223500.0
12.317167 11250.0 9.328123
3 140000.0
11.849398 9550.0 9.164296
4 250000.0
12.429216 14260.0 9.565214
Здесь мы воспользовались функцией np.log() и вывели результаты.
Второй вид преобразования – нормализация (normalization) – применяется
для изменения масштаба переменных, обычно с приведением его к диапазо-
ну от 0 до 1. Это бывает очень полезно, когда переменные обладают разными
масштабами или единицами измерения. К примеру, в нашем наборе данных
площадь дома исчисляется тысячами, тогда как количество спален и ван-
ных комнат редко может превышать 2 или 3. Многие алгоритмы машинного
обучения, в частности те, которые базируются на технике градиентного спу-
ска, очень чувствительны к масштабам переменных. Если масштабы пере-
менных будут разными, те из них, у которых масштаб больше, будут больше
воздействовать на модель во время обучения, в результате чего пострадает
качество итоговой модели.
Кроме того, нормализация может значительно ускорить скорость сходи-
мости оптимизационных алгоритмов, использующихся в процессе обучения
моделей. Это бывает особенно полезно при реализации нейронных сетей или
использовании метода опорных векторов.
Давайте применим нормализацию в диапазоне от 0 до 1 к столбцам Lot-
Size, HouseAge и SalePrice, для чего воспользуемся классом MinMaxScaler:
from sklearn.preprocessing import MinMaxScaler
# Определяем числовые столбцы для нормализации
numerical_columns = ['LotSize', 'HouseAge', 'SalePrice']
# Инстанцируем класс MinMaxScaler
scaler = MinMaxScaler()
156 Предсказание стоимости домов с помощью конструирования признаков
# Применяем нормализацию
df[numerical_columns] = scaler.fit_transform(df[numerical_columns])
# Смотрим результаты
print(df[numerical_columns].head())
Вывод:
LotSize HouseAge SalePrice
0 0.033420 0.050725 0.241078
1 0.038795 0.246377 0.203583
2 0.046507 0.065217 0.261908
3 0.038561 0.688406 0.145952
4 0.060576 0.072464 0.298709
Создание переменных взаимодействия
Переменные взаимодействия (interaction feature) создаются путем объеди-
нения двух или более существующих признаков для выявления сложных
зависимостей между ними, способных оказывать существенное влияние на
целевую переменную. В контексте нашего набора данных мы можем предпо-
ложить, что признак взаимодействия переменных, отвечающих за количест-
во спален и ванных комнат, может пригодиться нашей модели, поскольку он
содержит информацию об использовании полезной площади.
Эффект взаимодействия не ограничивается одним лишь общим количест-
вом спален и ванных комнат, поскольку стоимости на дома с тремя спальня-
ми и двумя ванными могут сильно отличаться от домов с двумя спальнями
и тремя ванными. Переменные взаимодействия зачастую способны улавли-
вать едва заметные нюансы в зависимостях между переменными, которые
могут положительно сказаться на качестве итоговой модели.
Также мы могли бы исследовать на предмет полезного взаимодействия
переменные LotSize и Neighborhood.
# Создаем переменную взаимодействия между столбцами Bedrooms и Bathrooms
df['BedroomBathroomInteraction'] = df['Bedrooms'] * df['Bathrooms']
# Смотрим результаты
print(df[['Bedrooms', 'Bathrooms', 'BedroomBathroomInteraction']].head())
Вывод:
Bedrooms Bathrooms BedroomBathroomInteraction
0
3
2
6
1
3
2
6
2
3
2
6
3
3
1
3
4
4
2
8
Построение и оценка предсказательной модели 157
Построение и оценка
предсказательной модели
После завершения этапов очистки данных и конструирования признаков
можно приступать к построению модели машинного обучения. Мы после-
довательно пройдемся по четырем следующим шагам, которые реализуем
с помощью библиотеки Scikit-learn:
разделение исходных данных на обучающую и тестовую выборки:
этот шаг очень важен с точки зрения того, насколько хорошей обобща-
ющей способностью будет обладать наша модель;
обучение модели: для нашей задачи регрессии мы выбрали метод
случайного леса. Это устойчивый ансамблевый метод, реализующийся
при помощи построения множества деревьев решений и способный
учитывать сложные зависимости в данных;
оценка качества модели: после обучения модели она прогоняется на
тестовых данных. Мы в нашем анализе воспользуемся традиционны-
ми метриками качества для регрессионных моделей при оценке того,
насколько точно наша модель способна предсказывать цены на дома;
настройка гиперпараметров: для повышения качества предсказаний
мы попробуем подобрать оптимальные значения гиперпараметров для
нашей модели.
Разделение данных на обучающую и тестовую выборки
Перед созданием и обучением нашей модели нам необходимо разнести дан-
ные, на которых она будет обучаться, и данные, на которых мы будем про-
верять качество модели. Для этого мы воспользуемся удобной функцией
train_test_split() из библиотеки Scikit-learn. С помощью аргумента test_size
можно задать долю исходной выборки, которая будет выделена под проверку
модели.
from sklearn.model_selection import train_test_split
# Определяем признаки (X) и целевую переменную (y)
X = df[['HouseAge', 'LotSizePerBedroom', 'LogLotSize', 'Bedrooms', 'Bathrooms',
'KitchenQualEncoded', 'BedroomBathroomInteraction']]
y = df['SalePrice']
# Разделяем данные на обучающую и тестовую выборки (80% обучение, 20% тест)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# Выведем размеры наших выборок
print(f"Размер обучающей выборки: {X_train.shape}, размер тестовой выборки: {X_test.shape}")
158 Предсказание стоимости домов с помощью конструирования признаков
Вывод:
Размер обучающей выборки: (1151, 7), размер тестовой выборки: (288, 7)
Аргументом random_state мы воспользовались с целью воспроизводимости
эксперимента.
Обучение модели случайного леса
После разделения данных на обучающую и тестовую выборки мы можем
создать модель случайного леса и обучить ее. Мы выбрали именно эту мо-
дель, поскольку она идеально подходит для нашего конкретного примера по
следующим причинам:
хорошо обрабатывает нелинейные зависимости и сложные взаимосвя-
зи между предикторами;
располагает метрикой для оценки значимости предикторов, что позво-
лит нам узнать, какие переменные оказывают наибольшее воздействие
на стоимость дома;
не так сильно подвержена переобучению в сравнении с обычным дере-
вом решений благодаря агрегации предсказаний на разных деревьях;
допускает наличие пропусков в данных, что часто встречается в ре-
альных задачах.
Воспользуемся классом RandomForestRegressor для создания модели, а также
обучим ее и сделаем первые предсказания:
from sklearn.ensemble import RandomForestRegressor
# Initialize the Random Forest Regressor
rf_model = RandomForestRegressor(random_state=42)
# Train the model on the training data
rf_model.fit(X_train, y_train)
# Make predictions on the test data
y_pred = rf_model.predict(X_test)
print("Model training complete.")
Оценка качества модели
Для оценки качества модели мы воспользуемся двумя ключевыми метрика-
ми, использующимися в задачах, связанных с регрессией: средней абсолют-
ной ошибкой (Mean Absolute Error (MAE)) и R-квадрат (R2). По этим метрикам
мы попробуем определить качество нашей модели:
метрика MAE рассчитывается путем усреднения разниц между пред-
сказанными и фактическими стоимостями домов, взятых по модулю.
С помощью нее можно оценить точность предсказаний в тех же еди-
ницах измерения, в которых исчисляется целевая переменная (т. е .
в долларах). Низкий показатель MAE соответствует высокому качеству
модели, поскольку говорит о незначительных величинах ошибки пред-
сказания;
Построение и оценка предсказательной модели 159
метрика R
2
, также известная как коэффициент детерминации, показы-
вает долю дисперсии целевой переменной (стоимость домов), которая
может быть объяснена выбранными предикторами. Этот показатель
ограничен диапазоном от 0 до 1, где 1 соответствует идеальной моде-
ли. К примеру, если вы получили R
2
, равный 0.7, это означает, что 70 %
изменчивости в стоимостях на дома мы можем объяснить с помощью
выбранных для модели предикторов.
Эти метрики дополняют друг друга, в совокупности показывая качество
итоговой модели. Если показатель MAE можно очень легко проинтерпре-
тировать применительно к ошибке предсказания, то R2
помогает понять,
насколько хорошо модель выявляет шаблоны и зависимости в данных. Ана-
лизируя обе эти метрики, можно нащупать способы для улучшения модели.
from sklearn.metrics import mean_absolute_error, r2_score
# Рассчитываем среднюю абсолютную ошибку
mae = mean_absolute_error(y_test, y_pred)
# Рассчитываем R-квадрат
r2 = r2_score(y_test, y_pred)
print(f"Средняя абсолютная ошибка (MAE): {mae:.5f}")
print(f"R-квадрат: {r2:.5f}")
Вывод:
Средняя абсолютная ошибка (MAE): 0.03972
R-квадрат: 0.68961
Здесь мы воспользовались функциями mean_absolute_error() и r2_score(),
предварительно загруженными из модуля sklearn.metrics.
Теперь давайте попробуем улучшить качество нашей модели...
Настройка гиперпараметров модели
Модель случайного леса предлагает на выбор большое количество гиперпа-
раметров (hyperparameter), которые можно настраивать с целью повышения
качества итоговой модели. Эти параметры позволяют контролировать раз-
личные аспекты структуры и поведения модели. Ниже перечислены некото-
рые ключевые гиперпараметры:
n_estimators: этот параметр отвечает за количество деревьев в модели.
Увеличение количества деревьев может позволить улучшить качество
модели, но в то же время негативно сказывается на объеме используе-
мых ресурсов;
max_depth: этот параметр задает максимальную глубину используемых
в модели деревьев. Чем больше глубина деревьев, тем более сложные
шаблоны в данных будет способна обнаруживать модель, но и тем бо-
лее склонна будет модель к переобучению;
160 Предсказание стоимости домов с помощью конструирования признаков
min_samples_split: с помощью этого параметра можно указать мини-
мальное количество наблюдений во внутреннем узле для возможно-
сти его расщепления. Параметр позволяет контролировать рост дерева
с целью избежания переобучения модели;
min_samples_leaf: этот параметр отвечает за минимальное количество
наблюдений, которые должны находиться в концевых узлах.
Параметры min_samples_split и min_samples_leaf удобно использовать вмес-
те для управления внешним видом деревьев. Но не все понимают разницу
между ними. К примеру, если задать min_samples_split = 6 , то при наличии
семи наблюдений во внутреннем узле он, согласно правилам, может быть
расщеплен. Однако если в результате такого расщепления мы получим узлы
с количеством наблюдений 6 и 1, а в параметре min_samples_leaf будет задано
значение 2, то мы не сможем произвести это разделение, поскольку 1 < 2.
Для нахождения оптимальной комбинации значений гиперпараметров
модели можно воспользоваться классом GridSearchCV из библиотеки Scikit-
learn. Путем полного перебора указанных параметров и их значений этот
инструмент определит наиболее оптимальный набор значений для всех па-
раметров с учетом заданной метрики качества, будь то средняя абсолютная
ошибка или R-квадрат.
Процесс подбора гиперпараметров имеет решающее значение при выборе
характеристик итоговой модели, поскольку позволяет повысить ее обобща-
ющую способность, т. е . способность делать предсказания на наблюдениях,
не участвовавших в процессе обучения.
from sklearn.model_selection import GridSearchCV
# Определяем гиперпараметры для настройки
param_grid = {
'n_estimators': [100, 200, 300],
'max_depth': [10, 20, 30, None]
}
# Инициализируем класс GridSearchCV с нашей моделью RandomForestRegressor
grid_search = GridSearchCV(estimator=rf_model, param_grid=param_grid, cv=5, scoring='neg_
mean_absolute_error')
# Подбираем значения гиперпараметров на обучающих данных
grid_search.fit(X_train, y_train)
# Лучший набор значений гиперпараметров
print(f"Лучшие гиперпараметры: {grid_search.best_params_}")
# Оставляем модель с оптимальными значениями гиперпараметров
best_rf_model = grid_search.best_estimator_
# Делаем предсказания на тестовой выборке
best_y _pred = best_rf_model.predict(X_test)
# Оцениваем качество подстроенной модели
best_mae = mean_absolute_error(y_test, best_y _pred)
Итоги проекта 161
best_r2 = r2_score(y_test, best_y _pred)
print(f"Исправленная средняя абсолютная ошибка (MAE): {best_mae:.5f}")
print(f"Исправленный R-квадрат: {best_r2:.5f}")
Вывод:
Лучшие гиперпараметры: {'max_depth': 10, 'n_estimators': 200}
Исправленная средняя абсолютная ошибка (MAE): 0.03870
Исправленный R-квадрат: 0.70860
Что мы здесь делаем? Сначала создаем сетку гиперпараметров для на-
стройки при помощи обычного словаря. Мы решили подбирать значения для
параметров n_estimators и max_depth. Эту сетку мы сохранили в переменной
param_grid. После этого создали экземпляр класса GridSearchCV, передав ему
во время инициализации нашу исходную модель (rf_model), сетку параметров
(param_grid), количество блоков кросс-валидации (cv) и метрику качества для
оценки (scoring). Далее мы запустили процесс подбора при помощи метода
fit(). По итогам этого процесса лучшая модель записывается в свойство best_
estimator_, которое можно извлечь. В завершение мы повторно делаем пред-
сказание на тестовой выборке и смотрим, как изменились метрики качества.
Как видите, метрика MAE снизилась с 0.03972 до 0.03870, а R-квадрат уве-
личился с 0.68961 до 0.70860. Хоть и небольшое, но улучшение мы получили.
Итоги проекта
Теперь, когда мы завершили наш первый проект, можно подвести его итоги
и еще раз быстро пройти по всем совершенным действиям.
1. Исследование и очистка данных:
• загрузили исходные данные, выполнили подстановку пропущенных
значений или удалили строки, где выполнить подстановку невоз-
можно;
• нашли выбросы в данных и избавились от них, воспользовавшись
методом на основе межквартильного размаха;
• провели корреляционный анализ с целью выявления зависимостей
между нашими исходными предикторами и целевой переменной
и определения их влияния на будущие предсказания.
2. Конструирование признаков:
• создали новые переменные на основе существующих для обнаруже-
ния скрытых зависимостей в данных;
• применили логарифмическое преобразование к числовым столбцам
для нивелирования их исходных масштабов и повышения обобщаю-
щей способности будущей модели;
• закодировали категориальные переменные с помощью метода с од-
ним активным состоянием и кодирования по меткам для преоб-
162 Предсказание стоимости домов с помощью конструирования признаков
разования текстовых значений в числовые, с которыми работает
большинство моделей машинного обучения.
3. Создание модели и оценка ее качества:
• построили на основе подготовленных признаков модель случайного
леса и оценили ее качество на тестовой выборке с использованием
метрик средней абсолютной ошибки или R-квадрат;
• выполнили поиск оптимальных значений гиперпараметров модели
по сетке с помощью класса GridSearchCV.
4. Оценка итоговой модели:
• выбрали модель с оптимальным набором значений гиперпараметров
и заново сделали предсказания, добившись в итоге незначительного
снижения показателя метрики средней абсолютной ошибки.
Дальнейшие улучшения
Для дальнейшего улучшения качества модели можно предпринять следую-
щие действия:
отбор признаков. Мы создали несколько новых признаков в нашей
модели и преобразовали остальные. Однако не все признаки могут
положительно влиять на качество предсказаний модели. Используя
инструмент определения важности признаков из класса RandomFor-
estRegressor или отдельный класс RFE, в котором применяется техника
рекурсивного исключения признаков (recursive feature elimination), мож-
но выявить наиболее значимые признаки и оставить в модели только
их. Это может положительно сказаться на ее качестве. Подробно об
этом мы будем говорить в одной из следующих глав книги;
расширенное конструирование признаков. Существует множество
продвинутых техник конструирования признаков, которые можно
применить к исходным данным, в число которых входит создание по-
линомиальных признаков и переменных взаимодействия нескольких
предикторов;
регуляризация и ансамблевые модели. Помимо модели случайного
леса можно также воспользоваться другими алгоритмами машинного
обучения, такими как градиентный бустинг, включая его экстремаль-
ный вариант XGBoost, или метод LightGBM. Также повысить обобщаю-
щую способность модели и избежать переобучения могут помочь тех-
ники регуляризации, такие как регрессия Лассо и гребневая регрессия;
кросс-валидация. Хотя мы использовали разделение исходного на-
бора данных на обучающую и тестовую выборки, применение техники
перекрестной проверки, или кросс-валидации, может помочь полу-
чить более устойчивую оценку качества модели. Используя k-блочную
кросс-валидацию, можно обеспечить модели более стабильную обоб-
щающую способность на новых данных.
Глава 3
Роль конструирования
признаков
в машинном обучении
Конструирование признаков можно сравнить с секретным ингредиентом,
отличающим просто хорошие модели машинного обучения от превосходных.
Этот процесс представляет собой творчество в чистом виде по превращению
сырых необработанных данных в осмысленные признаки, способные су-
щественно повысить качество создаваемых моделей.
В этой главе мы углубимся в процедуру конструирования и отбора при-
знаков и на примерах посмотрим, как создание значимых предикторов спо-
собно улучшить точность предсказаний используемых алгоритмов.
Также мы познакомимся с новыми способами преобразования сырых дан-
ных в информативные и осмысленные признаки. Эти способы могут вклю-
чать в себя как простые и не очень математические преобразования, так
и специфичные для конкретной предметной области методики извлечения
ценных выводов из данных.
3.1. Почему так важно конструировать
признаки?
В своей основе процесс конструирования признаков содержит преобразова-
ние сырых данных в формат, позволяющий алгоритмам машинного обучения
более эффективно обучаться. Это своеобразный мостик между разрозненны-
ми и беспорядочными сырыми данными, с которыми мы обычно имеем дело
в жизни, и понятными и хорошо структурированными признаками, посту-
пающими на вход алгоритмам, качество работы которых напрямую зависит
от качества входных данных.
164 Роль конструирования признаков в машинном обучении
Основная причина важности конструирования признаков состоит в том, что
методы машинного обучения по своей сути представляют собой не что иное,
как системы распознавания шаблонов. Они просто определяют и извлекают
зависимости в предоставленных им данных. И если какие-то зависимости
не представлены в признаках в явной форме, алгоритм просто пропустит их.
Кроме того, конструирование признаков способно нивелировать недостаток
сложности модели. В большинстве случаев более простая модель, но с хорошо
продуманными и структурированными признаками, будет превосходить по
качеству сложную модель с великим множеством полусырых данных на входе.
3.1.1. Области влияния признаков
на качество моделей
Как мы уже сказали, хорошо спроектированные и выверенные признаки
способны повысить качество предсказаний модели. Отсутствие таких при-
знаков или их избыток может привести к следующим губительным для мо-
дели последствиям:
недообучение: если признаки не в полной мере отражают зависимо-
сти в исходных данных, итоговая модель может оказаться чересчур
простой и не сможет хорошо предсказывать значения целевой пере-
менной на новых данных;
переобучение: это обратная ситуация, возникающая тогда, когда при-
знаки оказываются слишком специфичными для обучающих данных.
В таких случаях модель может идеально предсказывать отклик на на-
блюдениях, использовавшихся в процессе обучения, но будет страдать
от недостатка обобщающей способности на новых данных;
сбивающие с толку предсказания: признаки, вносящие в данные
лишний шум или не относящуюся к делу информацию, могут испор-
тить итоговую модель, предсказания которой не будут отражать име-
ющиеся в данных шаблоны.
Процесс конструирования признаков может включать в себя следующие
действия:
масштабирование числовых переменных, позволяющее привести их
к сопоставимым диапазонам;
кодирование категориальных переменных с приведением их к виду,
приемлемому для использования в алгоритмах машинного обучения;
создание переменных взаимодействия для выявления зависимостей
между предикторами;
применение знаний о конкретной предметной области для получе-
ния новых, более информативных признаков на основе существующих
переменных.
Однако хорошо сконструированные признаки позволяют не только повы-
сить качество предсказаний модели, но и сделать ее более интерпретируе-
мой. А это важно сразу по нескольких причинам:
Почему так важно конструировать признаки 165
1. Прозрачность и объяснимость. С хорошо структурированными и вы-
веренными признаками можно гораздо легче понять, как именно мо-
дель делает те или иные предсказания. Прозрачность очень важна для
принятия бизнес-решений на основе результатов работы модели.
2. Соблюдение нормативных требований. Во многих отраслях к ин-
струментам искусственного интеллекта применяются жесткие прави-
ла, согласно которым их выводы должны быть понятными и объясни-
мыми. Хорошо спроектированные признаки позволяют соблюсти эти
правила и облегчить аудит и проверку выводов модели.
3. Отладка и улучшение. При наличии легко интерпретируемых при-
знаков ошибки и погрешности в модели становятся хорошо заметны,
что позволяет довольно легко производить ее улучшение.
4. Взаимодействие с заказчиками. Понятные признаки позволяют лег-
ко доносить выводы модели до людей, принимающих решение, но не
знакомых с техническими аспектами. Это дает возможность облегчить
взаимодействие специалистов по работе с данными с заказчиками.
5. Этические аспекты. В некоторых чувствительных сферах, таких как
уголовное право или выдача кредитов, легко интерпретируемые при-
знаки позволяют убедиться в том, что решения, принимаемые на основе
результатов работы модели, являются справедливыми и непредвзятыми.
Давайте снова обратимся к нашему набору данных, посвященному оценке
рыночной стоимости домов, и построим на его основе две модели: одну без
конструирования признаков, а вторую – с конструированием.
Первая модель
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
from sklearn.preprocessing import StandardScaler
import matplotlib.pyplot as plt
# Загружаем набор данных
df2 = pd.read_csv('house_prices.csv')
# Удаляем строки с пропусками в целевой переменной SalePrice и переменной LotSize
df2 = df2.dropna(subset=['SalePrice', 'LotSize'])
# Основная информация о наборе данных
print(df2.info())
print("\nПервые несколько строк исходных данных:")
print(df2.head())
X = df2[['LotSize', 'Bedrooms', 'Bathrooms', 'YearBuilt']]
y = df2['SalePrice']
# Разделяем набор данных на обучающую и тестовую выборки
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
166 Роль конструирования признаков в машинном обучении
# Масштабируем признаки
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# Обучаем модель случайного леса
rf_model2 = RandomForestRegressor(n_estimators=100, random_state=42)
rf_model2.fit(X_train_scaled, y_train)
# Делаем предсказания
y_pred = rf_model2.predict(X_test_scaled)
# Оцениваем качество модели
mae = mean_absolute_error(y_test, y_pred)
mse = mean_squared_error(y_test, y_pred)
rmse = np.sqrt(mse)
r2 = r2_score(y_test, y_pred)
# Выводим результаты
print(f"\nКачество модели:")
print(f"Средняя абсолютная ошибка (MAE): {mae:.2f}")
print(f"Средняя среднеквадратическая ошибка (RMSE): {rmse:.2f}")
print(f"R-квадрат: {r2:.4f}")
# Значимость признаков
feature_importance = pd.DataFrame({
'feature': X.columns,
'importance': rf_model2.feature_importances_
}).sort_values('importance', ascending=False)
print("\nЗначимость признаков:")
print(feature_importance)
# Визуализируем предсказания в сравнении с фактами
plt.figure(figsize=(10, 10))
plt.scatter(y_test, y_pred, alpha=0.5)
plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], 'r--', lw=2)
plt.xlabel("Фактическая стоимость")
plt.ylabel("Предсказанная стоимость")
plt.title("Фактические и предсказанные стоимости домов")
plt.tight_layout()
plt.show()
Вывод:
<class 'pandas.core .frame.DataFrame'>
Index: 1429 entries, 0 to 1428
Data columns (total 7 columns):
# Column
Non-Null Count Dtype
---
------
--------------
-----
0 LotSize
1429 non-null float64
1 Neighborhood 1429 non-null object
2 KitchenQual 1429 non-null object
3 YearBuilt
1429 non-null int64
4 Bathrooms
1429 non-null int64
5 Bedrooms
1429 non-null int64
Почему так важно конструировать признаки 167
6 SalePrice
1429 non-null float64
dtypes: float64(2), int64(3), object(2)
memory usage: 89.3+ KB
None
Первые несколько строк исходных данных:
LotSize Neighborhood KitchenQual YearBuilt Bathrooms Bedrooms SalePrice
0 8450.0
CollgCr
3 (Good)
2003
2
3 208500.0
1 9600.0
Veenker 2 (Typical)
1976
2
3 181500.0
2 11250.0
CollgCr
3 (Good)
2001
2
3 223500.0
3 9550.0
Crawfor
3 (Good)
1915
1
3 140000.0
4 14260.0
NoRidge
3 (Good)
2000
2
4 250000.0
Качество модели:
Средняя абсолютная ошибка (MAE): 32200.25
Средняя среднеквадратическая ошибка (RMSE): 48854.21
R-квадрат: 0.5452
Значимость признаков:
feature importance
3 YearBuilt 0.498726
0 LotSize
0.330771
2 Bathrooms 0.106826
1 Bedrooms 0.063677
Рис. 3.1 Фактические и предсказанные стоимости домов
без конструирования признаков
168 Роль конструирования признаков в машинном обучении
Давайте быстро пробежимся по тому, что мы сделали, а затем оценим ка-
чество полученных предсказаний.
Итак, мы загрузили наш набор данных и первым делом просто избавились
от строк, в которых присутствуют пропуски в столбцах SalePrice и LotSize,
не пытаясь выполнять их подстановку. Далее мы разделили набор данных
на обучающую и тестовую выборки в отношении 80 на 20 и масштабировали
признаки в обеих выборках в диапазоне от 0 до 1. Это очень важно, посколь-
ку модель случайного леса очень чувствительна к масштабам переменных.
После этого мы обучили нашу модель с использованием 100 деревьев, сде-
лали предсказание на тестовой выборке и вычислили три метрики: среднюю
абсолютную ошибку, среднюю среднеквадратическую ошибку и R-квадрат.
В заключение мы рассчитали значимость предикторов в модели и вывели
на точечной диаграмме противопоставление фактических и предсказанных
значений в тестовом наборе. Как читать эту диаграмму? Берем точку (на-
блюдение), смотрим ее фактическое значение по оси X и предсказанное
значение по оси Y. В идеале, если бы модель работала безошибочно, все
наши наблюдения расположились бы на диагональной пунктирной линии,
а это означало бы, что все фактические и предсказанные значения были бы
равны.
Отметим для себя, что значение метрики MAE без конструирования при-
знаков составило 32 200.25, а R-квадрат оказался равен 0.5452.
Вторая модель
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler
from sklearn.preprocessing import LabelEncoder
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
from sklearn.preprocessing import StandardScaler
import matplotlib.pyplot as plt
import seaborn as sns
# Загружаем набор данных
df3 = pd.read_csv('house_prices.csv')
# Удаляем строки с пропусками в целевой переменной SalePrice и переменной LotSize
df3 = df3.dropna(subset=['SalePrice', 'LotSize'])
# Заполним пропуски в столбце LotSize медианой
df3['LotSize'] = df3['LotSize'].fillna(df3['LotSize'].median())
# Удалим строки с пропусками в целевой переменной SalePrice
df3 = df3.dropna(subset=['SalePrice'])
# Создадим новые признаки на основе существующих
df3['HouseAge'] = 2024 - df3['YearBuilt'] # Возраст дома
df3['LotSizePerBedroom'] = df3['LotSize'] / df3['Bedrooms'] # Площадь в расчете на одну
спальню
Почему так важно конструировать признаки 169
df3['TotalRooms'] = df3['Bedrooms'] + df3['Bathrooms'] # Общее количество спален и ванных
комнат
# Логарифмическое преобразование для снижения перекосов в данных
df3['LogSquareFootage'] = np.log(df3['LotSize'])
# Кодирование категориальных переменных на основе меток
label_encoder = LabelEncoder()
df3['NeighborhoodEncoded'] = label_encoder.fit_transform(df3['Neighborhood'])
# Определяем предикторы и целевую переменную
X = df3[['HouseAge', 'LotSizePerBedroom', 'LogSquareFootage', 'Bedrooms', 'Bathrooms',
'TotalRooms', 'NeighborhoodEncoded']]
y = df3['SalePrice']
# Основная информация о наборе данных
print(df3.info())
print("\nПервые несколько строк исходных данных:")
print(df3.head())
# Разделяем набор данных на обучающую и тестовую выборки
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# Масштабируем признаки
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# Обучаем модель случайного леса
rf_model3 = RandomForestRegressor(n_estimators=100, random_state=42)
rf_model3.fit(X_train_scaled, y_train)
# Делаем предсказания
y_pred = rf_model3.predict(X_test_scaled)
# Оцениваем качество модели
mae = mean_absolute_error(y_test, y_pred)
mse = mean_squared_error(y_test, y_pred)
rmse = np.sqrt(mse)
r2 = r2_score(y_test, y_pred)
# Выводим результаты
print(f"\nКачество модели:")
print(f"Средняя абсолютная ошибка (MAE): {mae:.2f}")
print(f"Средняя среднеквадратическая ошибка (RMSE): {rmse:.2f}")
print(f"R-квадрат: {r2:.4f}")
# Значимость признаков
feature_importance = pd.DataFrame({
'feature': X.columns,
'importance': rf_model3.feature_importances_
}).sort_values('importance', ascending=False)
print("\nЗначимость признаков:")
print(feature_importance)
170 Роль конструирования признаков в машинном обучении
# Визуализируем предсказания в сравнении с фактами
plt.figure(figsize=(10, 10))
plt.scatter(y_test, y_pred, alpha=0.5)
plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], 'r--', lw=2)
plt.xlabel("Фактическая стоимость")
plt.ylabel("Предсказанная стоимость")
plt.title("Фактические и предсказанные стоимости домов")
plt.tight_layout()
plt.show()
Вывод:
<class 'pandas.core .frame.DataFrame'>
Index: 1429 entries, 0 to 1428
Data columns (total 12 columns):
# Column
Non-Null Count Dtype
---
------
--------------
-----
0 LotSize
1429 non-null float64
1 Neighborhood
1429 non-null object
2 KitchenQual
1429 non-null object
3 YearBuilt
1429 non-null int64
4 Bathrooms
1429 non-null int64
5 Bedrooms
1429 non-null int64
6 SalePrice
1429 non-null float64
7 HouseAge
1429 non-null int64
8 LotSizePerBedroom 1429 non-null float64
9 TotalRooms
1429 non-null int64
10 LogSquareFootage
1429 non-null float64
11 NeighborhoodEncoded 1429 non-null int64
dtypes: float64(4), int64(6), object(2)
memory usage: 145.1+ KB
None
Первые несколько строк исходных данных:
LotSize Neighborhood KitchenQual ... TotalRooms LogSquareFootage
NeighborhoodEncoded
0 8450.0
CollgCr
3 (Good) ...
5
9.041922
5
1 9600.0
Veenker 2 (Typical) ...
5
9.169518
24
2 11250.0
CollgCr
3 (Good) ...
5
9.328123
5
3 9550.0
Crawfor
3 (Good) ...
4
9.164296
6
4 14260.0
NoRidge
3 (Good) ...
6
9.565214
15
[5 rows x 12 columns]
Качество модели:
Средняя абсолютная ошибка (MAE): 27035.47
Средняя среднеквадратическая ошибка (RMSE): 41708.56
R-квадрат: 0.6685
Почему так важно конструировать признаки 171
Значимость признаков:
feature importance
0
HouseAge 0.441933
2
LogSquareFootage 0.212846
6 NeighborhoodEncoded 0.116715
1 LotSizePerBedroom 0.090815
5
TotalRooms 0.079121
4
Bathrooms 0.042535
3
Bedrooms 0.016035
Рис. 3.2 Фактические и предсказанные стоимости домов
с конструированием признаков
Что мы добавили в этом фрагменте кода? В первую очередь мы выполнили
подстановку пропущенных значений в столбце LotSize с помощью медианы,
тем самым снизив потерю данных. Далее мы добавили расчетные перемен-
ные HouseAge, LotSizePerBedroom и TotalRooms, а также переменную LogSquare-
Footage, применив логарифмическое преобразование к столбцу LotSize. После
этого мы при помощи кодирования на основе меток преобразовали столбец
Neighborhood. Все эти переменные мы отправили в список предикторов, обучи-
ли модель и сделали предсказания, рассчитав метрики качества модели.
Как видим, показатель средней абсолютной ошибки после добавления
сконструированных признаков снизился с 32 200.25 до 27 035.47 (в долларах),
а метрика R-квадрат выросла с 0.5452 до 0.6685.
Также мы могли бы выполнить процедуру подбора значений гиперпара-
метров с помощью класса GridSearchCV, как уже делали раньше, чтобы еще
улучшить качество модели. Давайте это сделаем:
172 Роль конструирования признаков в машинном обучении
from sklearn.model_selection import GridSearchCV
# Определяем гиперпараметры для настройки
param_grid = {
'n_estimators': [301, 303, 305],
'max_depth': [7, 8, 9, 10]
}
# Инициализируем класс GridSearchCV с нашей моделью RandomForestRegressor
grid_search = GridSearchCV(estimator=rf_model3, param_grid=param_grid, cv=5, scoring='neg_
mean_absolute_error')
# Подбираем значения гиперпараметров на обучающих данных
grid_search.fit(X_train_scaled, y_train)
# Лучший набор значений гиперпараметров
print(f"Лучшие гиперпараметры: {grid_search.best_params_}")
# Оставляем модель с оптимальными значениями гиперпараметров
best_rf_model3 = grid_search.best_estimator_
# Делаем предсказания на тестовой выборке
best_y _pred = best_rf_model3.predict(X_test_scaled)
# Оцениваем качество подстроенной модели
best_mae = mean_absolute_error(y_test, best_y _pred)
best_r2 = r2_score(y_test, best_y _pred)
print(f"Исправленная средняя абсолютная ошибка (MAE): {best_mae:.5f}")
print(f"Исправленный R-квадрат: {best_r2:.5f}")
Вывод:
Лучшие гиперпараметры: {'max_depth': 8, 'n_estimators': 303}
Исправленная средняя абсолютная ошибка (MAE): 26193.95847
Исправленный R-квадрат: 0.68239
Как видите, нам удалось еще немного снизить ошибку (с 27 035.47 до
26 193.95) и повысить R-квадрат с 0.6685 до 0.68239.
3.2. Примеры эффективного
конструирования признаков
В этом разделе мы рассмотрим несколько эффективных техник конструи-
рования признаков, которые могут существенно улучшить качество ваших
моделей. Мы обсудим особенности этих техник, поговорим об их важности
в контексте машинного обучения и представим варианты их реализации на
практике. И рассмотрим мы следующие техники:
Примеры эффективного конструирования признаков 173
создание переменных взаимодействия. Применение этого приема
мы уже видели ранее, а здесь повторим его и чуть углубимся;
создание признаков на основе временных рядов. Фактор времени
является ключевым во множестве предсказательных моделей машин-
ного обучения. В данном разделе мы рассмотрим различные способы
эффективного извлечения и представления информации, связанной со
временем, научим наши модели улавливать временные тренды, сезон-
ность и другие шаблоны, зависящие от времени;
разбиение числовых переменных на интервалы. Мы обсудим тех-
ники для преобразования непрерывных числовых столбцов в дискрет-
ные категории, которые нередко помогают выявить нелинейные зави-
симости в данных и повысить интерпретируемость модели;
кодирование на основе целевой переменной. Мы узнаем, как в на-
борах данных с категориальными переменными с высокой кардиналь-
ностью (большим количеством уникальных значений) можно приме-
нять кодирование на основе целевой переменной вместо кодирования
с одним активным состоянием, позволяющее повысить качество моде-
ли, не увеличивая при этом ее размерность.
3.2.1. Создание переменных взаимодействия
Создание переменных взаимодействия подразумевает объединение тем или
иным способом существующих предикторов в новые признаки. Особенно
мощной эта техника является при наличии глубоких знаний о конкретной
предметной области, позволяющих делать те или иные предположения о том,
что в совокупности некоторые переменные могут давать модели больше цен-
ной информации, чем по отдельности. К примеру, в нашей модели со стои-
мостями домов переменная, отвечающая за соседство с важными объектами
городской инфраструктуры, может оказаться более значимой в сочетании
с площадью оцениваемого дома.
Пример взаимодействия переменных с количеством спален
и ванных комнат
В нашем наборе данных, посвященном оценке домов, переменная взаимо-
действия между количеством спален и ванных комнат может оказывать су-
щественное влияние на стоимость дома. Таким образом, мы можем перемно-
жить эти исходные переменные, чтобы отслеживать совокупный эффект от
изменения этих предикторов на стоимость дома. К примеру, новый признак
может позволить узнать, что эффект от добавления одной ванной комнаты
может быть разным в зависимости от количества спален в доме.
Допустим, в доме с одной спальней разница между одной и двумя ванны-
ми комнатами может оказаться относительно низкой. В то же время в доме
с четырьмя спальнями количество ванных комнат будет иметь гораздо боль-
174 Роль конструирования признаков в машинном обучении
шее значение, а значит, и влияние на стоимость будет выше. Перемножив
две исходные переменные, мы сможем улавливать эти тонкие взаимосвязи.
Кроме того, переменная взаимодействия между этими двумя переменны-
ми может помочь в определении статуса дома. Дома с большим количеством
ванных комнат и спален можно отнести к классу люкс, тогда как недостаток
спален и ванных может говорить о скромном статусе дома. Эти нюансы могут
помочь в оценке домов из разных рыночных сегментов.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# Загружаем набор данных
df = pd.read_csv('house_prices.csv')
# Создаем переменную взаимодействия между предикторами Bedrooms и Bathrooms
df['Bed_Bath_Int'] = df['Bedrooms'] * df['Bathrooms']
# Создаем более сложную переменную взаимодействия
df['Bed_Bath_Size_Int'] = df['Bedrooms'] * df['Bathrooms'] * np.log1p(df['LotSize'])
# Выводим первые несколько строк с новыми признаками
print(df[['Bedrooms', 'Bathrooms', 'LotSize', 'Bed_Bath_Int', 'Bed_Bath_Size_Int']].head())
# Визуализируем зависимость между переменной взаимодействия и целевой переменной SalePrice
plt.figure(figsize=(10, 6))
plt.scatter(df['Bed_Bath_Int'], df['SalePrice'], alpha=0.5)
plt.xlabel('Bed_Bath_Int')
plt.ylabel('SalePrice')
plt.title('Влияние переменной взаимодействия на целевую переменную')
plt.show()
# Рассчитываем корреляционную матрицу
correlation_matrix = df[['Bedrooms',
'Bathrooms',
'LotSize',
'Bed_Bath_Int',
'Bed_Bath_Size_Int',
'SalePrice']].corr()
# Выводим корреляционную матрицу
plt.figure(figsize=(10, 10))
sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm', vmin=-1, vmax=1, center=0)
plt.yticks(rotation=0)
plt.title('Корреляционная матрица признаков')
plt.show()
Вывод:
Bedrooms Bathrooms LotSize Bed_Bath_Int Bed_Bath_Size_Int
0
3
2 8450.0
6
54.252240
1
3
2 9600.0
6
55.017735
Примеры эффективного конструирования признаков 175
2
3
2 11250.0
6
55.969274
3
3
1 9550.0
3
27.493203
4
4
2 14260.0
8
76.522271
Рис. 3.3 Влияние переменной взаимодействия на целевую переменную
Рис. 3.4 Корреляционная матрица признаков
176 Роль конструирования признаков в машинном обучении
Многое из того, что происходит в этом коде, вы уже прекрасно понимаете.
Здесь мы создали две переменные взаимодействия, назвав их Bed_Bath_Int
и Bed_Bath_Size_Int соответственно. В первой мы просто перемножили коли-
чество спален и ванных комнат, а во второй умножили еще на натуральный
логарифм от площади дома с добавленной единицей (log(1 + x)). Добавление
единицы позволит избежать коллизий в случае с нулевыми показателями
площади дома и нивелировать влияние экстремально больших значений
площади.
На этапе визуализации мы вывели две диаграммы. Первая показывает
влияние переменной взаимодействия Bed_Bath_Int на целевую переменную.
С помощью такого графика можно выявить нелинейные зависимости или
кластеры в данных, которые образуются благодаря созданной переменной
взаимодействия. На второй диаграмме мы вывели корреляционную мат-
рицу, в которую включили исходные предикторы, созданные переменные
взаимодействия и целевую переменную. По этой матрице видно, что наши
переменные взаимодействия оказывают значительное влияние на целевую
переменную.
3.2.2. Создание признаков на основе
временных рядов
Без признаков на основе временных рядов (time-based feature), отражающих
дату и время в том или ином виде, не обходится практически ни один про-
мышленный набор данных, и такие признаки зачастую играют решающую
роль при использовании алгоритмов машинного обучения. В то же время
признаки на основе временных рядов требуют особого подхода в плане пре-
образований для извлечения всего их потенциала при создании моделей.
Сырые данные о датах и времени могут нести в себе полезную информацию,
но при этом не улавливать шаблонов в отношении циклической природы,
лежащей в основе временных рядов.
Получение важной информации из признаков на основе временных ря-
дов предполагает использование множества техник: от извлечения простых
компонент даты и времени до сложных видов кодирования данных на основе
периодов. К примеру, извлечение компонентов, связанных с годами, меся-
цами, числами и часами, может помочь выявить сезонные колебания или
шаблоны, зависящие от дня недели. Более сложные техники могут включать
в себя создание циклических признаков на основе гармонических функций,
таких как синус и косинус, способных хорошо улавливать циклическую при-
роду времени.
Кроме того, создание признаков, представляющих временные интервалы,
таких как количество дней, прошедших с определенного события, или раз-
ница между двумя датами, также позволяет извлечь ценные сведения из дан-
ных, недоступные в исходном наборе. Подобные признаки часто оказывают
ключевое влияние на качество итоговой предсказательной модели.
Примеры эффективного конструирования признаков 177
Извлечение компонентов из дат
При работе с временными рядами очень важно уметь создавать полезные
признаки на основе информации о датах и времени. Наборы данных, в кото-
рых присутствуют даты, традиционно представляют собой гораздо больший
полигон для применения техник конструирования и отбора признаков. Вмес-
то того чтобы использовать исходные данные как есть, мы можем вывести на
их основе некоторые полезные компоненты, например следующие:
год: с помощью этого компонента можно отслеживать долгосрочные
колебания и шаблоны, возникающие на ежегодной основе;
месяц: этот компонент поможет в определении шаблонов, связанных
с сезонностью, таких как пиковые повышения продаж в праздники
или колебания потребления электроэнергии в зависимости от времени
года;
день недели: поможет в выявлении недельных шаблонов, таких как
рост посещаемости ресторанов на выходных и пиковая активность на
фондовой бирже в рабочие дни;
час: позволяет еще больше увеличить гранулярность и отслеживать
такие шаблоны, как количество пробок на дорогах или потребление
электроэнергии в зависимости от времени суток.
Давайте рассмотрим вариант извлечения компонентов из переменных дат
и времени на примере набора данных, в котором присутствует три столбца:
дата и дата события (Новый год) в текстовом формате YYYY-MM -DD, а также
столбец MaxTemp, соответствующий максимальной температуре воздуха, от-
меченной в этот день.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# Загружаем данные о максимальных температурах в Австралии за несколько лет
df = pd.read_csv('weatherAUS_mini.csv')
# Приводим столбцы с датами к формату даты и времени
df['Date'] = pd.to_datetime(df['Date'])
df['EventDate'] = pd.to_datetime(df['EventDate'])
# Извлекаем простые компоненты, связанные с датой и временем
df['Year'] = df['Date'].dt.year
df['Month'] = df['Date'].dt.month
df['DayOfWeek'] = df['Date'].dt.dayofweek
df['Quarter'] = df['Date'].dt.quarter
df['DayOfYear'] = df['Date'].dt.dayofyear
df['WeekOfYear'] = df['Date'].dt.isocalendar().week
df['IsWeekend'] = df['Date'].dt.dayofweek.isin([5, 6]).astype(int)
# Создаем циклические признаки для месяца и дня недели
df['MonthSin'] = np.sin(2 * np.pi * df['Month']/12)
178 Роль конструирования признаков в машинном обучении
df['MonthCos'] = np.cos(2 * np.pi * df['Month']/12)
df['DayOfWeekSin'] = np.sin(2 * np.pi * df['DayOfWeek']/7)
df['DayOfWeekCos'] = np.cos(2 * np.pi * df['DayOfWeek']/7)
# Создаем признак с разницей в днях между текущей датой и датой события (Новый год)
df['DaysSinceEvent'] = (df['Date'] - df['EventDate']).dt.days
# Просматриваем результат
print(df[['Date', 'Year', 'Month', 'DayOfWeek', 'Quarter', 'DayOfYear', 'WeekOfYear',
'IsWeekend',
'MonthSin',
'MonthCos',
'DayOfWeekSin',
'DayOfWeekCos',
'DaysSinceEvent']].head())
# Визуализируем распределение числовой целевой переменной по месяцам
plt.figure(figsize=(12, 6))
sns.boxplot(x='Month', y='MaxTemp', data=df)
plt.title('Распределение максимальной температуры по месяцам')
plt.show()
# Анализируем корреляцию между признаками и целевой переменной
correlation_matrix = df[['Year', 'Month', 'DayOfWeek', 'Quarter', 'DayOfYear',
'WeekOfYear', 'IsWeekend', 'MonthSin', 'MonthCos', 'DayOfWeekSin',
'DayOfWeekCos', 'DaysSinceEvent', 'MaxTemp']].corr()
plt.figure(figsize=(12, 10))
sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm', vmin=-1, vmax=1, center=0)
plt.title('Корреляционная матрица между признаками и целевой переменной')
plt.show()
Вывод:
Date Year Month DayOfWeek Quarter DayOfYear WeekOfYear \
0 2008-12-01 2008
12
0
4
336
49
1 2008-12-02 2008
12
1
4
337
49
2 2008-12-03 2008
12
2
4
338
49
3 2008-12-04 2008
12
3
4
339
49
4 2008-12-05 2008
12
4
4
340
49
IsWeekend
MonthSin MonthCos DayOfWeekSin DayOfWeekCos \
0
0 -2 .449294e-16
1.0
0.000000
1.000000
1
0 -2 .449294e-16
1.0
0.781831
0.623490
2
0 -2 .449294e-16
1.0
0.974928
- 0 .222521
3
0 -2 .449294e-16
1.0
0.433884
- 0 .900969
4
0 -2 .449294e-16
1.0
-0 .433884
- 0 .900969
DaysSinceEvent
0
335
1
336
2
337
3
338
4
339
Примеры эффективного конструирования признаков 179
Рис. 3 .5 Распределение максимальной температуры по месяцам
Что мы здесь делаем?
1. Загружаем в датафрейм файл weatherAUS_mini.csv
1
, содержащий инфор-
мацию о максимальных ежедневных температурах в Австралии за не-
сколько лет.
2. Приводим столбцы с датами (Date и EventDate) к формату даты и време-
ни datetime с помощью функции pd.to_datetime().
3. Извлекаем простые компоненты, связанные с датой и временем, такие
как год, месяц, день недели, номер недели и т. д . Особо стоит отметить
способ получения флага, говорящего о том, выходной это день или нет.
Мы воспользовались конструкцией df['Date'].dt.dayofweek.isin([5,
6]).astype(int), в которой сначала извлекли номер дня недели, затем
перевели его в булево значение посредством сравнения с числами 5 и 6
(суббота и воскресенье), после чего привели к целочисленному типу,
получив в результате 1 (выходные) или 0 (будние дни).
4. Для отслеживания циклической природы изменения месяцев и дней
недели мы создали особые признаки MonthSin, MonthCos, DayOfWeekSin
и DayOfWeekCos, воспользовавшись для этого гармоническими функция-
ми синус (np.sin) и косинус (np.cos). Это позволит нам, к примеру, по-
казать модели, что декабрь (12) и январь (1) располагаются близко друг
к другу. Для интереса мы можем взглянуть на все уникальные значения
в столбце DayOfWeekSin и заметить, что их ровно семь – по количеству
дней в неделе, причем каждый день недели имеет свое значение в диа-
пазоне синусоиды – от –1 до 1:
1
Ссылку на сопроводительные файлы можно найти на странице книги на сайте из-
дательства «ДМК Пресс».
180 Роль конструирования признаков в машинном обучении
df['DayOfWeekSin'].value_counts()
Вывод:
DayOfWeekSin
- 0 .781831 20447
0.974928 20431
0.000000 20404
0.781831 20369
0.433884 20250
- 0 .974928 20015
- 0 .433884 19955
Name: count, dtype: int64
Рис. 3.6 Корреляционная матрица между признаками и целевой переменной
Примеры эффективного конструирования признаков 181
5. Создаем признак DaysSinceEvent с помощью выражения (df['Date'] -
df['EventDate']).dt.days, получая тем самым разницу в днях между
текущей датой и датой определенного события (столбец E ventDate) .
В нашем случае в столбце EventDate находится первый день года. Такая
техника позволит захватывать шаблоны в данных, зависящие от опре-
деленных событий во времени.
6. Выводим на диаграмме размаха (рис. 3 .5) распределение максималь-
ной температуры по месяцам. Не забывайте, что это данные по Австра-
лии, где непривычно для нас в летние месяцы температура воздуха
ниже, чем в зимние.
7. Строим корреляционную матрицу по всем признакам и целевой пере-
менной и выводим ее на экран. Можно заметить, что наибольшей кор-
реляцией максимальная температура обладает как раз с созданными
признаками MonthSin и MonthCos, тогда как с признаками для недель кор-
реляция околонулевая. Это объясняется тем, что температура гораздо
больше зависит от месяца, чем от дня недели (хотя иногда кажется, что
в будние всегда теплее и солнечнее).
Создание признаков на основе временных рядов позволяет моделям учи-
тывать при прогнозировании шаблоны, связанные с датами и временем, что
существенно повышает их качество.
Работа с временными интервалами
Еще одной полезной техникой при работе с временными рядами является
создание признаков на основе временных интервалов. Эта техника базиру-
ется на расчете разницы между двумя точками во времени. Так, к примеру,
можно вычислить количество дней, прошедших от даты выставления дома на
продажу до даты его покупки, или время, прошедшее с момента последней
активности пользователя в маркетинговой кампании. Подобные признаки
позволяют моделям улавливать шаблоны, связанные с временной динамикой.
Рассмотрим модификацию нашего набора данных с рыночной стоимостью
домов, в котором добавлены столбцы с датой выставления дома на продажу
(ListingDate) и датой его фактической продажи (SaleDate). В сопроводитель-
ных материалах этот набор данных присутствует в виде файла с именем
house_prices_w_dates.csv. Таким образом, мы могли бы создать признак Day-
sOnMarket, говорящий о том, сколько дней дом находился на рынке. Этот при-
знак может дать модели довольно много информации о востребованности
лота или рыночных условиях.
Также подобные признаки могут использоваться для улавливания нели-
нейных зависимостей между переменными. К примеру, мы могли бы при-
менить к новому признаку логарифмическое преобразование, что позволило
бы отразить, что разница между 5 и 10 днями может иметь большее значе-
ние, чем разница между 95 и 100 днями. К тому же на основе полученной
информации вы можете создавать собственные категориальные признаки,
разделяющие наблюдения на сегменты, или кластеры.
182 Роль конструирования признаков в машинном обучении
Рассмотрим пример с измененным набором данных с рыночной стои-
мостью домов:
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
# Загружаем данные
df = pd.read_csv('house_prices_w_dates.csv')
# Убедимся, что столбцы с датами загружены в формате datetime
df['ListingDate'] = pd.to_datetime(df['ListingDate'])
df['SaleDate'] = pd.to_datetime(df['SaleDate'])
# Создадим признак DaysOnMarket путем вычитания даты выставления лота на продажу из даты
фактической продажи
df['DaysOnMarket'] = (df['SaleDate'] - df['ListingDate']).dt.days
# Применим логарифмическое преобразование к переменной DaysOnMarket
df['LogDaysOnMarket'] = np.log1p(df['DaysOnMarket'])
# Разобьем дома на категории по длительности активности лота
bins = [0, 90, 180, 270, np.inf]
labels = ['Быстрая продажа', 'Нормальная продажа', 'Долгая продажа', 'Очень долгая продажа']
df['MarketSpeedCategory'] = pd.cut(df['DaysOnMarket'], bins=bins, labels=labels)
# Посмотрим на созданные категории
print(df[['ListingDate',
'SaleDate',
'DaysOnMarket',
'LogDaysOnMarket',
'MarketSpeedCategory']].head())
# Визуализируем распределение признака DaysOnMarket
plt.figure(figsize=(12, 6))
sns.histplot(data=df, x='DaysOnMarket', kde=True)
plt.title('Распределение продолжительности активности лота')
plt.xlabel('Дни на рынке')
plt.show()
# Анализируем зависимость между переменными DaysOnMarket и SalePrice
plt.figure(figsize=(12, 6))
sns.scatterplot(data=df, x='DaysOnMarket', y='SalePrice')
plt.title('Зависимость между количеством дней на рынке и стоимостью дома')
plt.xlabel('Дни на рынке')
plt.ylabel('Стоимость')
plt.show()
# Сравним средние стоимости по разным категориям из столбца MarketSpeedCategories
avg_prices = df.groupby('MarketSpeedCategory', observed=False)['SalePrice'].mean().sort_
values(ascending=False)
plt.figure(figsize=(10, 6))
sns.barplot(x=avg_prices.index, y=avg_prices.values)
plt.title('Средние стоимости домов по категориям')
plt.xlabel('Категория')
Примеры эффективного конструирования признаков 183
plt.ylabel('Средняя стоимость')
plt.show()
Вывод:
ListingDate SaleDate DaysOnMarket LogDaysOnMarket MarketSpeedCategory
0 2024-05-21 2024-10-03
135
4.912655 Нормальная продажа
1 2024-03 -12 2024-07-25
135
4.912655 Нормальная продажа
2 2024-01-17 2024-11 -22
310
5.739793 Очень долгая продажа
3 2024-02-02 2024-08 -30
210
5.351858
Долгая продажа
4 2024-06-25 2024-09-27
94
4.553877 Нормальная продажа
Рис. 3.7 Распределение продолжительности активности лота
Здесь мы сначала загрузили данные, измененные за счет добавления
столбцов с датой выставления дома на продажу и датой его фактической
продажи. Затем создали новую переменную DaysOnMarket, рассчитав ее пу-
тем вычитания даты выставления лота на продажу из даты фактической
продажи, после чего применили к созданному признаку логарифмическое
преобразование для сглаживания возможных выбросов. После этого созда-
ли категориальную переменную MarketSpeedCategory, заполнив ее значения-
ми 'Быстрая продажа', 'Нормальная продажа', 'Долгая продажа' или 'Очень долгая
продажа' на основе количества дней, на протяжении которых дом продавался.
В завершение мы построили три диаграммы. На первой при помощи ги-
стограммы показали распределение продолжительности активности лота
(рис. 3 .7), на второй вывели зависимость между количеством дней на рынке
и стоимостью дома с помощью диаграммы рассеяния (рис. 3.8), а на третьей
столбиками отобразили средние стоимости домов по созданным нами ранее
категориям (рис. 3 .9).
184 Роль конструирования признаков в машинном обучении
Рис. 3.8 Зависимость между количеством дней на рынке
и стоимостью дома
Рис. 3 .9 Средние стоимости домов по категориям
Признаемся, что мы с целью демонстрации заполнили столбцы с датами
случайным образом, вследствие чего и получили такие ровные распределе-
ния. В реальном наборе данных зависимости могли бы быть менее равно-
мерными и более показательными.
Примеры эффективного конструирования признаков 185
3.2.3. Разбиение числовых переменных
на интервалы
Разбиение числовых переменных на интервалы (binning) представляет со-
бой очень мощную технику конструирования признаков, позволяющую
разделить непрерывные числовые переменные на дискретные категории,
или интервалы. Этот метод бывает особенно полезен при работе с пре-
дикторами, связанными с целевой переменной нелинейно, или когда нам
известно, что определенные интервалы значений похожим образом влияют
на отклик.
В процессе разбиения на интервалы каждому наблюдению присваивается
определенный интервал по заранее установленным правилам. К примеру,
в нашем наборе данных, посвященном стоимости домов, общая площадь
дома может быть нелинейно связана с итоговой ценой – вместо этого на
графике могут присутствовать определенные скачки.
Техника разбиения числовых переменных на интервалы обладает следу-
ющими достоинствами:
улавливает нелинейные зависимости без необходимости приме-
нять к переменным сложные математические преобразования. Это по-
зволяет выявлять сложные скрытые шаблоны в данных и лучше понять
зависимости между переменными;
нивелирует влияние выбросов. Группируя экстремальные значения
переменной в отдельные интервалы, мы тем самым снижаем их воз-
действие на целевую переменную. Этот механизм позволяет убедить-
ся в том, что аномальные наблюдения не будут непропорционально
смещать анализ, а это, в свою очередь, позволит повысить качество
и надежность модели;
повышает интерпретируемость модели. Использование разбитых
на интервалы признаков зачастую позволяет получить более понятную
и легкую для интерпретации модель. Дискретные данные дают воз-
можность точнее определить, как те или иные категории влияют на
целевую переменную, что облегчает взаимодействие с заказчиками,
не обладающими специальным техническим образованием;
позволяет справиться с разреженностью данных. При наличии раз-
реженных или неравномерно распределенных данных в переменной
разбиение ее на интервалы может пойти на пользу и улучшить качест-
во предсказаний для областей, в которых отсутствуют значения.
При этом к процессу разбиения числовых переменных на интервалы необ-
ходимо подходить очень обдуманно и скрупулезно. Выбор интервалов может
оказывать существенное влияние на качество модели и должен быть сделан
на основе знаний о предметной области, а также с учетом распределения
значений и применения статистических методов.
186 Роль конструирования признаков в машинном обучении
Давайте в нашем примере из проекта разделим переменную с общей пло-
щадью дома в квадратных футах на категории. Для начала посмотрим, как
распределяются значения в этой переменной:
print(df['LotSize'].describe())
print(f'\n90-й процентиль: {df["LotSize"].quantile(0.9)}')
Вывод:
count
1429.000000
mean
10495.610217
std
10008.150525
min
1300.000000
25%
7540.000000
50%
9480.000000
75%
11600.000000
max
215245.000000
Name: LotSize, dtype: float64
90-й процентиль: 14366.2
Как видите, минимальная площадь дома составляет 1300 кв. футов, а мак-
симальная – 215 245 кв. футов. При этом среднее значение и медиана равны
10 495 и 9480 кв. футов соответственно, а 90-й процентиль – 14 366 кв. футов.
Это говорит нам о том, что очень больших домов в базе крайне мало, а ос-
новная их доля лежит в интервале от 2000 до 15 000 кв. футов.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# Загружаем данные
df = pd.read_csv('house_prices.csv')
# Определяем интервалы для площади домов
bins = [0, 4000, 7500, 11500, 15000, 30000, np.inf]
labels = ['Очень маленький', 'Маленький', 'Средний', 'Большой', 'Очень большой', 'Дворец']
# Создаем переменную с интервалами
df['HouseSizeCategory'] = pd.cut(df['LotSize'], bins=bins, labels=labels)
# Просматриваем первые строки и сравниваем значения в столбцах
print(df[['LotSize', 'HouseSizeCategory']].head())
# Рассчитываем среднюю цену на 1 кв. фут помещения
df['PricePerSqFt'] = df['SalePrice'] / df['LotSize']
# Группируем дома по созданным категориям и рассчитываем по ним среднюю стоимость за кв. фут
avg_price_per_sqft = df.groupby('HouseSizeCategory', observed=False)['PricePerSqFt'].mean().
sort_values(ascending=False)
# Визуализируем распределение
plt.figure(figsize=(12, 6))
Примеры эффективного конструирования признаков 187
sns.histplot(data=df, x='LotSize', bins=20, kde=True)
plt.title('Распределение домов по категориям')
plt.xlabel('Площадь, кв. футы')
plt.show()
# Визуализируем среднюю стоимость по категориям домов
plt.figure(figsize=(10, 6))
sns.barplot(x=avg_price_per_sqft.index, y=avg_price_per_sqft.values)
plt.title('Средняя стоимость за 1 кв. фут по категориям домов')
plt.xlabel('Категория')
plt.ylabel('Средняя стоимость за 1 кв. фут')
plt.xticks(rotation=45)
plt.show()
# Анализируем зависимость между размером дома и его стоимостью
plt.figure(figsize=(12, 6))
sns.scatterplot(data=df, x='LotSize', y='SalePrice', hue='HouseSizeCategory')
plt.title('Зависимость между размером дома и его стоимостью')
plt.xlabel('Площадь, кв. футы')
plt.ylabel('Стоимость')
plt.show()
Вывод:
LotSize HouseSizeCategory
0 8450.0
Средний
1 9600.0
Средний
2 11250.0
Средний
3 9550.0
Средний
4 14260.0
Большой
Рис. 3 .10 Распределение домов по категориям
188 Роль конструирования признаков в машинном обучении
Рис. 3.11 Средняя стоимость за 1 кв. фут по категориям домов
Рис. 3.12 Зависимость между размером дома и его стоимостью
Здесь мы воспользовались функцией pd.cut() для разделения домов на
категории по площади. Новый признак мы назвали HouseSizeCategory. Также
мы рассчитали среднюю стоимость одного квадратного фута площади для
Примеры эффективного конструирования признаков 189
каждой категории домов. Как видно на рис. 3 .11, эта стоимость снижается
с ростом площади.
На рис. 3 .10 мы показали распределение домов по категориям, подтвер-
див свою догадку о том, что дворцы встречаются не так часто. А на рис. 3 .12
мы отобразили зависимость между размером дома и его стоимостью. Здесь
также видно, что экстремально больших домов в нашей базе не так много.
3.2.4. Кодирование на основе целевой переменной
Кодирование на основе целевой переменной (target encoding) представляет со-
бой мощную технику предварительной обработки категориальных перемен-
ных, в особенности тех, которые обладают высокой кардинальностью, т. е.
насчитывают большое количество уникальных значений. В отличие от коди-
рования с одним активным состоянием, которое в таких случаях может при-
водить к созданию излишне большого количества дополнительных столбцов
и опасности возникновения проклятия размерности, кодирование на основе
целевой переменной выполняет замену каждого значения категориальной
переменной на число, рассчитанное с использованием целевой переменной.
Такой подход может оказаться крайне эффективным при обработке пере-
менных, хранящих почтовые индексы, идентификаторы товаров или другие
категории с большим количеством уникальных значений.
В процессе кодирования на основе целевой переменной мы вычисляем
среднее значение или любую другую статистику по целевой переменной для
каждой отдельной категории и используем полученные величины в качестве
значений нового признака. В нашем примере с оценкой стоимости домов
мы могли бы таким образом рассчитать среднюю стоимость дома с группи-
ровкой по важным объектам городской инфраструктуры. Это поможет не
только избежать создания дополнительных столбцов в наборе данных, но
и дополнит его ценной информацией о зависимости между категориями
и стоимостью домов.
Преимущества кодирования на основе целевой переменной:
снижение размерности. Этот вид кодирования позволяет избежать
увеличения количества предикторов в модели, что делает ее более
предсказуемой и точной в своих прогнозах, а также повышает ее ин-
терпретируемость за счет снижения сложности расчетов. К примеру,
в наборе данных, содержащем информацию о тысячах уникальных
идентификаторов товаров, кодирование на основе целевой перемен-
ной может позволить свести всю полезную нагрузку к одному инфор-
мативному признаку;
учет редко встречающихся категорий. Эта техника предлагает эле-
гантное решение для обработки категорий, встречающихся в наборе
данных не так часто. В случае применения метода кодирования с од-
ним активным состоянием присутствие таких категорий может при-
190 Роль конструирования признаков в машинном обучении
водить к образованию сильно разреженной матрицы или переобуче-
нию модели. В процессе кодирования на основе целевой переменной
таким категориям будут присвоены осмысленные значения на основе
их взаимосвязи с целевой переменной, что позволит модели извлечь
полезную информацию даже из самых редких категорий в столбце;
выявление сложных зависимостей. Применяя этот метод, вы мо-
жете заставить модель обнаруживать нелинейные зависимости между
предиктором и целевой переменной. К примеру, в наборе данных, по-
священном оттоку клиентов, местоположение контрагента может быть
связано с вероятностью оттока очень нелинейно. Кодирование на осно-
ве целевой переменной позволяет эффективно справляться с такими
ситуациями;
повышение интерпретируемости модели. Закодированный таким
образом признак можно будет легко проинтерпретировать в отноше-
нии целевой переменной, что позволяет повысить объяснимость мо-
дели. Это особенно важно в сферах, где решения, принятые на основе
результатов предсказания модели, должны быть прозрачными и по-
нятными. К примеру, в модели, оценивающей риски при кредитовании,
бывает очень важно понимать, как тот или иной род занятий клиента
влияет на прогноз по возвращению кредита;
беспроблемная обработка новых категорий. При появлении в мо-
мент развертывания модели новых категорий, не участвовавших в про-
цессе ее обучения, метод кодирования на основе целевой переменной
позволяет легко их обработать. Использование сквозного среднего зна-
чения целевой переменной или байесовского среднего даст возмож-
ность обработать новые категории без существенной потери качества
предсказаний модели.
В то же время применять кодирование на основе целевой переменной не-
обходимо с осторожностью, чтобы избежать утечки информации. Можно вос-
пользоваться кросс-валидацией или техникой усреднения прогнозов каждой
модели, полученной в результате перекрестной проверки (Out Of Fold – OOF),
чтобы убедиться, что в процессе кодирования используются только данные
из обучающей выборки. Это позволит избежать переобучения модели и обес-
печит целостность процесса оценки ее качества.
Давайте применим этот вид кодирования в нашем наборе данных к столб-
цу Neighborhood, отвечающему за соседство дома с важными объектами город-
ской инфраструктуры. В качестве значений категорий будем использовать
среднюю стоимость.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
from sklearn.linear_model import LinearRegression
Примеры эффективного конструирования признаков 191
# Загружаем набор данных
df = pd.read_csv('house_prices.csv')
# Удалим строки с пропусками в целевой переменной SalePrice
df = df.dropna(subset=['SalePrice'])
# Выводим информацию об интересующих нас столбцах
print(df['SalePrice'].describe(), end='\n\n')
print(df['Neighborhood'].value_counts(), end='\n\n')
# Рассчитываем среднюю стоимость для каждого вида соседства
neighborhood_avg_price = df.groupby('Neighborhood')['SalePrice'].mean()
# Создаем новый столбец с закодированными значениями
df['NeighborhoodEncoded'] = df['Neighborhood'].map(neighborhood_avg_price)
# Просматриваем первые несколько строк
print(df[['Neighborhood', 'NeighborhoodEncoded', 'SalePrice']].head(10), end='\n\n')
# Визуализируем зависимость между закодированной переменной и стоимостью
plt.figure(figsize=(12, 6))
plt.scatter(df['NeighborhoodEncoded'], df['SalePrice'], alpha=0.5)
plt.title('Зависимость между закодированным соседством и стоимостью')
plt.xlabel('Закодированное значение соседства')
plt.ylabel('Стоимость')
plt.show()
# Разделяем исходные данные на обучающую и тестовую выборки
X = df[['NeighborhoodEncoded']]
y = df['SalePrice']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# Обучаем простую модель линейной регрессии
model = LinearRegression()
model.fit(X_train, y_train)
# Делаем предсказания на тестовой выборке
y_pred = model.predict(X_test)
# Рассчитываем и выводим среднеквадратическую ошибку
mse = mean_squared_error(y_test, y_pred)
print(f"Среднеквадратическая ошибка: {mse}")
# Выводим коэффициент влияния нового признака на отклик
print(f"Коэффициент для NeighborhoodEncoded: {model.coef_[0]}")
# Функция для обработки новых категорий
def encode_new_neighborhood(neighborhood, neighborhood_avg_price, global_avg_price):
return neighborhood_avg_price.get(neighborhood, global_avg_price)
# Пример обработки новой категории
global_avg_price = df['SalePrice'].mean()
new_neighborhood = "New Development"
encoded_value = encode_new_neighborhood(new_neighborhood, neighborhood_avg_price, global_
avg_price)
print(f"Закодированное значение для '{new_neighborhood}': {encoded_value}")
192 Роль конструирования признаков в машинном обучении
Вывод:
count
1439.000000
mean
180893.934677
std
79493.535892
min
34900.000000
25%
129950.000000
50%
163000.000000
75%
214000.000000
max
755000.000000
Name: SalePrice, dtype: float64
Neighborhood
NAmes
223
CollgCr 149
OldTown
112
...
Veenker
10
NPkVill
9
Blueste
2
Name: count, dtype: int64
Neighborhood NeighborhoodEncoded SalePrice
0
CollgCr
197683.664430 208500.0
1
Veenker
224150.000000 181500.0
2
CollgCr
197683.664430 223500.0
3
Crawfor
209507.220000 140000.0
4
NoRidge
335295.317073 250000.0
5
Mitchel
157762.468085 143000.0
6
Somerst
225125.904762 307000.0
7
NWAmes
188759.097222 200000.0
8
OldTown
128084.455357 129900.0
9
BrkSide
124834.051724 118000.0
Среднеквадратическая ошибка: 3029587667.27044
Коэффициент для NeighborhoodEncoded: 1.0137377054899166
Закодированное значение для 'New Development': 180893.93467685892
Здесь мы сначала вывели информацию о столбцах NeighborhoodEncoded и Sa-
lePrice, после чего выполнили процедуру кодирования на основе целевой
переменной. Она заключалась в том, чтобы рассчитать среднюю стоимость
для каждого вида соседства и создать новый столбец с именем Neighborhoo-
dEncoded с закодированными значениями с помощью функции map(). Далее
мы при помощи диаграммы рассеяния визуализировали зависимость между
закодированным соседством и стоимостью. Это позволило нам понять, как
кодирование может выявить изменение ценовых диапазонов в зависимости
от соседствующих с домом объектов.
После этого мы разбили исходные данные на обучающую и тестовую вы-
борки, обучили простую модель линейной регрессии на нашем закодирован-
ном признаке соседства, сделали предсказания на тестовом наборе и оцени-
Практические упражнения 193
ли качество модели с помощью среднеквадратической ошибки. На экран мы
вывели коэффициент нашего предиктора, чтобы понять, как от него зависит
наша целевая переменная.
Рис. 3 .13 Зависимость между закодированным соседством и стоимостью
В завершение мы предусмотрели механизм для обработки новых катего-
рий в виде функции, возвращающей при отсутствии переданной категории
в наборе данных среднее значение стоимости, а также опробовали его на
практике.
3.3. Практические упражнения
Теперь, когда вы завершили чтение главы, вы можете попробовать приме-
нить полученные знания о конструировании признаков на практике при
решении упражнений. Постарайтесь решать задачи самостоятельно, прежде
чем смотреть предложенный вариант ответа.
Упражнение 1. Создание переменной взаимодействия
Есть набор данных о продажах автомобилей со столбцами EngineSize (объем
двигателя в литрах) и HorsePower (мощность в л. с.):
194 Роль конструирования признаков в машинном обучении
import pandas as pd
# Простые данные о продаже автомобилей
data = {'CarID': [1, 2, 3, 4, 5],
'EngineSize': [2.0, 3.0, 4.0, 2.5, 3.5],
'HorsePower': [150, 200, 250, 180, 220]}
df = pd.DataFrame(data)
Ваша задача состоит в том, чтобы создать простую переменную взаимо-
действия с именем PowerToEngineRatio, выражающую отношение мощности
к объему двигателя.
Решение
# Создаем переменную взаимодействия PowerToEngineRatio
df['PowerToEngineRatio'] = df['HorsePower'] / df['EngineSize']
# Выводим результаты
print(df[['EngineSize', 'HorsePower', 'PowerToEngineRatio']])
Вывод:
EngineSize HorsePower PowerToEngineRatio
0
2.0
150
75.000000
1
3.0
200
66.666667
2
4.0
250
62.500000
3
2.5
180
72.000000
4
3.5
220
62.857143
Упражнение 2. Обработка переменных на основе
временных рядов
Есть набор данных с информацией о транзакциях, в котором присутствует
столбец TransactionDate:
# Простой набор данных с транзакциями
data = {'TransactionID': [101, 102, 103, 104, 105],
'TransactionDate': ['2022-05-15', '2023-03-10', '2023-07-22', '2022-12-01',
'2023-01-14']}
df = pd.DataFrame(data)
Ваша задача состоит в том, чтобы:
1) привести столбец TransactionDate к формату datetime;
2) извлечь из этого столбца компоненты года, месяца и дня недели.
Решение
# Приводим столбец TransactionDate к типу datetime
df['TransactionDate'] = pd.to_datetime(df['TransactionDate'])
# Извлекаем компоненты года, месяца и дня недели
df['Year'] = df['TransactionDate'].dt.year
df['Month'] = df['TransactionDate'].dt.month
Практические упражнения 195
df['DayOfWeek'] = df['TransactionDate'].dt.dayofweek
# Выводим результаты
print(df[['TransactionDate', 'Year', 'Month', 'DayOfWeek']])
Вывод:
TransactionDate Year Month DayOfWeek
0
2022-05-15 2022
5
6
1
2023-03 -10 2023
3
4
2
2023-07-22 2023
7
5
3
2022-12-01 2022
12
3
4
2023-01-14 2023
1
5
Упражнение 3. Разбиение числовой переменной
на интервалы
Есть набор данных с информацией о покупках, в котором присутствует стол-
бец PurchaseAmount:
# Простой набор данных
data = {'CustomerID': [1, 2, 3, 4, 5],
'PurchaseAmount': [50, 150, 700, 300, 600]}
df = pd.DataFrame(data)
Ваша задача состоит в том, чтобы разбить столбец PurchaseAmount на три
категории: Low, Medium и High со следующими диапазонами значений:
Low: меньше $100;
Medium: от $100 до $500;
High: больше $500.
Решение
# Определяем интервалы и метки
bins = [0, 100, 500, float('inf')]
labels = ['Low', 'Medium', 'High']
# Разделяем столбец PurchaseAmount на категории
df['PurchaseCategory'] = pd.cut(df['PurchaseAmount'], bins=bins, labels=labels)
# Выводим результаты
print(df[['PurchaseAmount', 'PurchaseCategory']])
Вывод:
PurchaseAmount PurchaseCategory
0
50
Low
1
150
Medium
2
700
High
3
300
Medium
4
600
High
196 Роль конструирования признаков в машинном обучении
Упражнение 4. Кодирование на основе целевой переменной
Есть набор данных с информацией о стоимости домов, в котором присут-
ствуют столбцы Neighborhood и SalePrice:
# Простой набор данных со стоимостью домов
data = {'HouseID': [1, 2, 3, 4, 5],
'Neighborhood': ['A', 'B', 'A', 'C', 'B'],
'SalePrice': [300000, 450000, 350000, 500000, 470000]}
df = pd.DataFrame(data)
Ваша задача состоит в том, чтобы применить кодирование на основе це-
левой переменной к столбцу Neighborhood, воспользовавшись средней стои-
мостью домов для каждой категории.
Решение
# Рассчитываем среднее значение SalePrice для каждой категории соседства
neighborhood_avg_price = df.groupby('Neighborhood')['SalePrice'].mean()
# Выполняем кодирование на основе целевой переменной
df['NeighborhoodEncoded'] = df['Neighborhood'].map(neighborhood_avg_price)
# Выводим результаты
print(df[['Neighborhood', 'SalePrice', 'NeighborhoodEncoded']])
Вывод:
Neighborhood SalePrice NeighborhoodEncoded
0
A
300000
325000.0
1
B
450000
460000.0
2
A
350000
325000.0
3
C
500000
500000.0
4
B
470000
460000.0
Упражнение 5. Расчет временных интервалов
Есть набор данных с информацией о выставлении домов на продажу (столбец
ListingDate) и фактических датах их приобретения (столбец SaleDate).
# Набор данных о продаже домов
data = {'PropertyID': [1, 2, 3, 4, 5],
'ListingDate': ['2023-01-01', '2023-02-15', '2023-03 -01', '2023-04-01', '2023-05-01'],
'SaleDate': ['2023-03-15', '2023-04-01', '2023-03-20', '2023-05-15', '2023-06-01']}
df = pd.DataFrame(data)
Ваша задача состоит в том, чтобы вычислить количество дней пребыва-
ния каждого лота на рынке (разницу между значениями столбцов SaleDate
и ListingDate).
Возможные проблемы 197
Решение
# Приводим столбцы ListingDate и SaleDate к типу данных datetime
df['ListingDate'] = pd.to_datetime(df['ListingDate'])
df['SaleDate'] = pd.to_datetime(df['SaleDate'])
# Рассчитываем количество дней лота на рынке
df['DaysOnMarket'] = (df['SaleDate'] - df['ListingDate']).dt.days
# Выводим результаты
print(df[['ListingDate', 'SaleDate', 'DaysOnMarket']])
Вывод:
ListingDate SaleDate DaysOnMarket
0 2023-01-01 2023-03 -15
73
1 2023-02-15 2023-04-01
45
2 2023-03-01 2023-03 -20
19
3 2023-04-01 2023-05-15
44
4 2023-05-01 2023-06-01
31
3.4. Возможные проблемы
Хотя конструирование признаков может помочь существенно повысить ка-
чество ваших моделей, при неправильном использовании оно может таить
и определенные ловушки. Здесь мы постарались перечислить самые рас-
пространенные из них.
3.4.1. Переобучение из-за слишком большого
количества признаков
Создание чересчур большого количества признаков, особенно это касается
переменных взаимодействия и преобразований, может привести к пере-
обучению модели. Переобучение возникает, когда модель слишком сильно
подгоняется под обучающие данные и утрачивает обобщающую способность
на наблюдениях, не участвовавших в процессе обучения.
Что может пойти не так:
излишнее количество полиномиальных признаков, переменных взаи-
модействия и предикторов, полученных в результате применения пре-
образований, может привести к чрезмерной сложности модели, кото-
рая начнет улавливать шум вместо полезных шаблонов;
модель может обладать высокой точностью предсказаний на обучаю-
щей выборке, но плохо работать на тестовых данных вследствие пере-
обучения.
198 Роль конструирования признаков в машинном обучении
Решение:
воспользуйтесь техниками вроде кросс-валидации для оценки качест-
ва модели на множестве разных выборок;
примените регуляризацию (лассо или гребневую регрессию), призван-
ную штрафовать модель за излишнюю сложность;
воспользуйтесь техниками отбора признаков, такими как рекурсивное
исключение признаков (recursive feature elimination – RFE), для поиска
и удаления избыточных переменных.
3.4.2. Мультиколлинеарность
Мультиколлинеарность (multicollinearity) возникает в случае, когда два или
более признаков сильно коррелируют друг с другом. Это может нарушать
стройность модели и приводить к снижению точности предсказаний по при-
чине того, что модель не сможет определить, какой из этих признаков важнее.
Что может пойти не так:
в случае наличия сильной корреляции между несколькими признака-
ми модель может ошибочно определять их значимость, что приведет
к искажению прогнозов;
мультиколлинеарность может раздуть дисперсию коэффициентов мо-
дели, что сделает ее чересчур чувствительной к незначительным из-
менениям в данных.
Решение:
воспользуйтесь корреляционным анализом или фактором инфляции
дисперсии (Variance Inflation Factor – VIF) для определения наличия
мультиколлинеарности в ваших данных;
удалите или объедините сильно коррелирующие переменные во из-
бежание избыточности данных;
рассмотрите возможность применения анализа главных компонент
(Principal Component Analysis – PCA) для преобразования исходных
коррелирующих переменных в набор некоррелирующих признаков.
3.4.3. Утечка информации
Утечка информации (data leakage) возникает, когда информация из тестовой
выборки непреднамеренно влияет на процесс обучения модели, что приво-
дит к чересчур оптимистичной оценке ее качества.
Что может пойти не так:
если конструирование признаков производится на всем исходном на-
боре данных до его разделения на обучающую и тестовую выборки,
в модель могут просочиться сведения, которыми она располагать не
должна, что может приводить к смещенным оценкам ее качества;
Возможные проблемы 199
использование кодирования на основе целевой переменной без при-
менения кросс-валидации может приводить к утечке информации, по-
скольку целевая переменная напрямую влияет на признаки во время
обучения.
Решение:
всегда разделяйте ваши исходные данные на обучающую и тестовую
выборки перед применением техник конструирования признаков во
избежание утечки информации;
перед использованием техник вроде кодирования на основе целевой
переменной убедитесь, что эти процедуры выполняются в рамках
кросс-валидации. Это поможет предотвратить влияние данных из це-
левой переменной на процесс обучения.
3.4.4. Неправильная интерпретация признаков
на основе времени
При работе с временными рядами очень просто допустить ошибку, проиг-
норировав природу данных. К примеру, использование в признаке будущей
информации (например, будущих продаж) может привести к неправильной
оценке качества модели.
Что может пойти не так:
если в процессе конструирования признаков вы непреднамеренно ис-
пользуете будущую информацию (например, пытаетесь применять
знания о следующих месяцах для прогнозирования текущих продаж),
ваша итоговая модель может показывать отличное качество на обуча-
ющих данных, но будет абсолютно бесполезна на новых наблюдениях;
извлечение признаков на основе времени без учета сезонных или иных
периодических шаблонов может привести к появлению в модели не-
полных или ошибочных признаков.
Решение:
соблюдайте осторожность при работе с временными рядами. Убеди-
тесь, что все ваши признаки используют только доступную на конкрет-
ный момент времени информацию;
воспользуйтесь техникой кросс-валидации временных рядов, напри-
мер проверкой на основе скользящих окон, для подтверждения оценки
качества вашей модели с учетом особенностей временных рядов.
3.4.5. Неподобающее масштабирование признаков
Некоторые алгоритмы машинного обучения, в особенности те, которые по-
лагаются на метрики расстояний (такие как метод k-ближайших соседей или
метод опорных векторов), чрезвычайно чувствительны к масштабу входных
200 Роль конструирования признаков в машинном обучении
переменных. Если им на вход подать признаки в разных масштабах, качество
итоговой модели может оставлять желать лучшего.
Что может пойти не так:
признаки с большими диапазонами значений (в нашем наборе данных
по продаже домов это, например, переменная, отвечающая за площадь
дома) могут расцениваться как более значимые в сравнении с при-
знаками с меньшими диапазонами (количество спален или ванных
комнат), что будет приводить к смещенной оценке качества модели;
при наличии переменных с разными диапазонами значений модель
может не достигнуть схождения в процессе обучения.
Решение:
выполняйте нормализацию и стандартизацию ваших признаков, осо-
бенно при работе с такими алгоритмами, как метод k-ближайших со-
седей, метод опорных векторов или нейронные сети. Воспользуйтесь
классами MinMaxScaler и StandardScaler из библиотеки Scikit-learn для
обеспечения своим признакам сопоставимого масштаба;
для моделей на основе деревьев, таких как случайный лес или экстре-
мальный градиентный бустинг (XGBoost), масштабирование признаков
обычно выполнять не требуется, поскольку они не так чувствительны
к диапазонам значений.
3.4.6. Недооценка знаний о предметной области
Хотя автоматизированные техники конструирования признаков могут быть
чрезвычайно мощными и полезными, необходимо уделять повышенное вни-
мание знаниям о конкретной предметной области. Если полагаться только на
стандартные механизмы отбора и получения признаков без учета специфики
окружения, можно недобрать в качестве итоговой модели.
Что может пойти не так:
игнорирование информации о предметной области может привести
к пропуску ценных признаков, важность которых автоматические тех-
ники определить не смогли;
автоматически сгенерированные признаки могут не улавливать шаб-
лоны, характерные для вашего набора данных, что может негативно
сказаться на качестве полученной модели.
Решение:
воспользуйтесь знаниями о конкретной предметной области в про-
цессе конструирования и отбора признаков. При необходимости про-
консультируйтесь с экспертами, которые могут помочь в определении
предикторов, важность которых не очевидна по одним лишь исходным
данным;
используйте техники отбора признаков совместно с экспертными зна-
ниями о предметной области для получения оптимального набора пре-
дикторов в итоговой модели.
Заключение 201
Заключение
В этой главе мы поговорили о том, почему так важно уделять повышенное
внимание процедуре конструирования и отбора признаков. Мы рассмотрели
пример с двумя моделями, в одной из которых оставили только исходные
предикторы, а во второй произвели некоторые действия по конструирова-
нию новых признаков, и сравнили их качество.
Затем мы поговорили о некоторых важных разновидностях конструиро-
вания признаков. В частности, мы обсудили процессы создания переменных
взаимодействия и признаков на основе временных рядов, а также рассмотре-
ли техники разбиения числовых переменных на интервалы и кодирования
на основе целевой переменной.
Прочитав эту главу, вы должны были понять, что процедура конструиро-
вания и отбора признаков не ограничивается одним лишь созданием но-
вых предикторов, а включает в себя преобразование переменных с целью
повышения качества итоговой модели машинного обучения. Кроме того,
правильно проведенный процесс конструирования признаков обеспечивает
модели высокую обобщающую способность и устойчивость на новых данных,
не участвовавших в процессе обучения.
В следующей главе мы поговорим об интеллектуальных техниках запол-
нения пропущенных значений в наборах данных.
Глава 4
Сложные техники
заполнения пропусков
в данных
Обработка пропущенных значений играет важнейшую роль при подготовке
исходных данных для анализа при помощи моделей машинного обучения.
Мы уже не раз говорили, что в реальных наборах данных пропуски встреча-
ются повсеместно, и необходимо уметь к каждому отдельному случаю под-
ходить индивидуально, что позволит повысить качество итоговой модели.
О простых способах подстановки пропущенных значений мы уже говори-
ли в первых главах книги, а здесь затронем более интеллектуальные техники,
позволяющие извлекать сложные шаблоны и зависимости внутри набора
данных с целью повышения качества предсказаний.
4.1. Использование продвинутых
техник заполнения пропущенных
значений
Применение нестандартных техник подстановки пропущенных значений
предполагает использование статистических методов и алгоритмов машин-
ного обучения с целью получения более осмысленных значений, которыми
можно заменить пропуски. Иногда это позволяет значительно повысить ка-
чество последующих предсказаний и надежность модели в целом. Особенно
полезными эти техники бывают при работе с наборами данных, обладающи-
ми сложной структурой, нелинейными зависимостями между переменными
и множеством коррелирующих предикторов.
Использование продвинутых техник заполнения пропущенных значений 203
В этом разделе мы рассмотрим три следующие техники подстановки про-
пущенных значений.
1. Подстановка с помощью метода k-ближайших соседей. Этот метод
предполагает использование сходств между наблюдениями с целью
подбора значений для замены пропусков и бывает особенно эффекти-
вен при наличии явных шаблонов в данных.
2. Метод множественной подстановки с помощью цепных уравне-
ний (Multiple Imputation by Chained Equations – MICE). Эта техника
предполагает генерирование множества подстановочных значений
для каждого пропуска с учетом зависимостей между переменными
в наборе данных. Особенно полезен этот метод может оказаться при
обработке пропущенных значений со сложными шаблонами.
3. Использование моделей машинного обучения для подстановки.
Эта техника подразумевает обучение предсказательных моделей на
основе существующих данных с целью вычисления значений для под-
становки. Данный метод помогает учесть сложные нелинейные зависи-
мости между переменными и может применяться в различных наборах
данных.
Каждый из этих методов обладает своим набором достоинств и может
применяться в разных сценариях.
4.1.1. Подстановка с помощью метода k-ближайших
соседей
Метод k-ближайших соседей (K-Nearest Neighbors – KNN) представляет собой
универсальный алгоритм, активно применяющийся за пределами основ-
ной области, связанной с регрессией и классификацией. В контексте под-
становки пропущенных значений этот метод предлагает гибкое решение,
учитывающее структуру и зависимости в наборе данных. Главный принцип
применения метода k-ближайших соседей для подстановки пропущенных
значений основывается на предположении о том, что наблюдения с похо-
жими значениями предикторов с большой долей вероятности будут иметь
схожие значения целевой переменной.
Эффективность этого метода на практике обусловлена следующими фак-
торами:
локальность контекста. Метод k-ближайших соседей хорошо справ-
ляется с обнаружением локализованных шаблонов и зависимостей
в данных. Ориентируясь на схожие наблюдения, он способен улавли-
вать малейшие тенденции, недоступные для общеупотребимых статис-
тических методов. Этот подход особенно оправдывает себя при работе
с наборами данных с региональными особенностями или содержимым,
характерным для наличия кластеров;
204 Сложные техники заполнения пропусков в данных
непараметрическая природа. В отличие от многих других статисти-
ческих методов, метод k-ближайших соседей не полагается на допуще-
ния о распределении исходных данных. Такая гибкость делает его при-
менимым к самым разным наборам данных: от наборов с нормальным
распределением до сложных смешанных структур. Этот метод хорошо
работает с реальными наборами данных, распределения в которых за-
частую сильно отличаются от теоретических распределений;
многомерность. Одним из главных достоинств метода k-ближайших
соседей является его способность работать одновременно с множест-
вом признаков. Это позволяет отслеживать сложные зависимости меж-
ду разными переменными. К примеру, в клиническом наборе данных
этот метод мог бы выполнить подстановку пропущенных значений
кровяного давления не только на основании возраста, но также с уче-
том веса и множества других сопутствующих факторов;
адаптивность к сложности данных. Метод k-ближайших соседей
способен приспосабливаться к наборам данных разной степени слож-
ности. В простых наборах он может вести себя подобно обычным ба-
зовым методам подстановки, а в более сложных сценариях может про-
явить способность к обнаружению труднообнаружимых зависимостей
между предикторами, недоступных для простых методов. Это делает
метод k-ближайших соседей универсальным и позволяет применять
его в самых разных ситуациях.
В то же время стоит отметить, что эффективность этого метода напря-
мую зависит от выбора гиперпараметра k (количество соседей), метрики
расстояния, используемой для определения сходства наблюдений, а также
от наличия выбросов в наборе данных. Таким образом, при использовании
этой техники важно правильно настраивать метод k-ближайших соседей
и оценивать его эффективность.
Давайте посмотрим на примере, как можно воспользоваться этим методом
с помощью специального класса KNNImputer, входящего в состав библиотеки
Scikit-learn:
import numpy as np
import pandas as pd
from sklearn.impute import KNNImputer
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
import matplotlib.pyplot as plt
# Простой набор данных с пропущенными значениями
data={
'Age': [25, 27, 22, 35, 26, 28, 40, 32, 29, 45],
'Salary': [50000, 60000, 52000, 63000, 58000, 55000, 70000, 48000, 62000, 75000],
'Experience': [2, 4, 1, 5, 3, 5, 8, 6, 4, 9]
}
df = pd.DataFrame(data)
Использование продвинутых техник заполнения пропущенных значений 205
# Выводим исходный набор данных
print("Исходный набор данных:")
print(df)
print("\n")
# Функция для расчета процента пропущенных значений
def missing_percentage(df):
return df.isnull().mean() * 100
print("Процент пропущенных значений:")
print(missing_percentage(df))
print("\n")
# Разделяем исходный набор данных на обучающую и тестовую выборки
df_train, df_test = train_test_split(df, test_size=0.3, random_state=42)
# Создаем копию тестовой выборки и искусственно включаем в нее пропущенные значения
df_test_missing = df_test.copy()
np.random.seed(42)
for column in df_test_missing.columns:
mask = np.random.rand(len(df_test_missing)) < 0.2
df_test_missing.loc[mask, column] = np.nan
# Просматриваем датафрейм с пропусками
print("\nДатафрейм с пропусками:")
print(df_test_missing)
# Создаем экземпляр класса KNNImputer с k=2 (будем опираться на двух ближайших соседей)
knn_imputer = KNNImputer(n_neighbors=2)
# Обучаем модель на созданной для этого выборке
knn_imputer.fit(df_train)
# Применяем метод KNN к тестовой выборке с пропусками
df_imputed = pd.DataFrame(knn_imputer.transform(df_test_missing), columns=df.columns,
index=df_test.index)
# Рассчитываем ошибку подстановки
mse = mean_squared_error(df_test, df_imputed)
print(f"Среднеквадратическая ошибка подстановки: {mse:.2f}")
# Визуализируем результаты подстановки
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
for i, column in enumerate(df.columns):
axes[i].scatter(df_test[column], df_imputed[column], alpha=0.5)
axes[i].plot([df_test[column].min(), df_test[column].max()], [df_test[column].min(),
df_test[column].max()], 'r--', lw=2)
axes[i].set_xlabel(f'{column} (факт)')
axes[i].set_ylabel(f'{column} (предсказание)')
axes[i].set_title(f'Подстановка для поля {column}')
plt.tight_layout()
plt.show()
206 Сложные техники заполнения пропусков в данных
# Просматриваем датафрейм с подстановками
print("\nДатафрейм с подстановками:")
print(df_imputed)
Вывод:
Исходный набор данных:
Age Salary Experience
0 25 50000
2
1 27 60000
4
2 22 52000
1
3 35 63000
5
4 26 58000
3
5 28 55000
5
6 40 70000
8
7 32 48000
6
8 29 62000
4
9 45 75000
9
Процент пропущенных значений:
Age
0.0
Salary
0.0
Experience 0.0
dtype: float64
Датафрейм с пропусками:
Age Salary Experience
8 29.0 62000.0
NaN
1 27.0
NaN
4.0
5 28.0
NaN
5.0
Среднеквадратическая ошибка подстановки: 4444444.44
Датафрейм с подстановками:
Age Salary Experience
8 29.0 62000.0
4.0
1 27.0 54000.0
4.0
5 28.0 53000.0
5.0
Что здесь происходит? Сначала мы создали простой набор данных, состо-
ящий из трех столбцов: Age, Salary и Experience. После этого убедились, что
в нем отсутствуют пропущенные значения, с помощью написанной функ-
ции missing_percentage(). Далее мы разбили набор данных на обучающую
и тестовую выборки в соотношении 70 на 30. Мы собираемся предсказывать
пропущенные значения в тестовой выборке, но затем нам нужно будет как-то
определить качество наших предсказаний. В этой связи мы создали копию
тестовой выборки и искусственно включили в нее определенный процент
пропущенных значений. Получился такой датафрейм:
Датафрейм с пропусками:
Age Salary Experience
8 29.0 62000.0
NaN
Использование продвинутых техник заполнения пропущенных значений 207
1 27.0
NaN
4.0
5 28.0
NaN
5.0
Теперь нам нужно заполнить появившиеся пропуски. Для этого мы соз-
дали экземпляр класса KNNImputer со значением параметра k = 2, т. е . свои
заключения мы будем основывать на двух ближайших соседях. Далее мы
обучили нашу модель на обучающей выборке и применили ее к тестовой
выборке с пропусками, получив следующий датафрейм df_imputed:
Датафрейм с подстановками:
Age Salary Experience
8 29.0 62000.0
4.0
1 27.0 54000.0
4.0
5 28.0 53000.0
5.0
После этого мы рассчитали ошибку подстановки в абсолютных единицах
и вывели на графиках результаты подстановки значений.
Рис. 4 .1 Результаты подстановки пропущенных значений
На правом графике видно, что с подстановкой значения в поле Experience
мы угадали идеально, а в поле Salary (график по центру) немного ошиблись
с предсказаниями, подставив значения 54 000 и 53 000 вместо 60 000 и 55 000,
что для такого маленького набора данных вполне приемлемо.
На этом примере мы показали не только способ применения метода KNN
для подстановки пропущенных значений, но и вариант вывода оценки ка-
чества с графиками.
Метод k-ближайших соседей бывает особенно полезен, когда в наборе
данных присутствуют ярко выраженные зависимости и корреляции между
столбцами. К примеру, если бы в вашем наборе данных столбец с возрастом
сотрудников имел пропуски, то вы могли бы легко восстановить значения
в нем, воспользовавшись двумя другими столбцами с зарплатой и стажем
работы.
Потенциал этого метода заключается в его способности улавливать шабло-
ны сразу в нескольких переменных. Он не смотрит на один предиктор от-
дельно, а анализирует зависимости сразу во всех столбцах. Кроме того, метод
208 Сложные техники заполнения пропусков в данных
k-ближайших соседей хорошо показывает себя в сценариях, в которых ло-
кальные шаблоны являются более информативными в сравнении с глобаль-
ными трендами. В отличие от методов, целиком полагающихся на средние
значения или распределения, метод KNN базируется на данных о наиболее
похожих наблюдениях, или соседях. Это позволяет ему улавливать гораздо
менее заметные шаблоны в данных. К примеру, в наборе данных, посвящен-
ном географии, этот метод мог бы довольно точно восстановить утраченные
данные о температуре воздуха в конкретном районе, основываясь на данных
из близлежащих областей.
4.1.2. Метод множественной подстановки
с помощью цепных уравнений (MICE)
Метод множественной подстановки с помощью цепных уравнений (Multiple Im-
putation by Chained Equations – MICE) представляет собой продвинутую тех-
нику подстановки пропущенных значений, в процессе применения которой
строится полноценная модель на основе набора данных. Каждая переменная
с пропусками при этом рассматривается как зависимая переменная в моде-
ли, а в качестве независимых используются все остальные предикторы.
Алгоритм MICE выполняется итеративно следующим образом.
1. Первоначальная подстановка
Алгоритм MICE начинается с заполнения пропущенных значений простыми
статистиками вроде среднего значения, медианы или моды по соответству-
ющему столбцу. Этот шаг является первым в итеративном процессе запол-
нения значений. К примеру, если в наборе данных содержатся пропущенные
значения в столбце с возрастом, на этом шаге мы могли бы заполнить про-
пуски средним возрастом по всему набору данных. Но это только первый
шаг, и в дальнейшем мы будем обновлять значения, которыми заполнили
пустоты.
Способ заполнения на первом шаге может зависеть от природы данных
или конкретной реализации алгоритма MICE. В некоторых реализациях на
этом шаге пропуски в категориальных столбцах могут заполняться наиболее
часто встречающимися значениями, а может применяться и простая модель
линейной регрессии.
Цель этого первого шага в том, чтобы хоть как-то заполнить все пропущен-
ные значения в наборе данных.
2. Итеративное обновление
Суть алгоритма MICE кроется именно в этом шаге, где мы постепенно уточ-
няем значения, используемые вместо исходных пропусков. Здесь мы для
каждого предиктора, в котором присутствовали пропущенные значения,
Использование продвинутых техник заполнения пропущенных значений 209
строим регрессионную модель, в которой остальные переменные исполь-
зуются в качестве независимых переменных. Это позволяет использовать
в процессе подстановки сложные зависимости между переменными в наборе
данных.
Последовательность действий здесь выглядит так:
алгоритм MICE выбирает переменную, в которой изначально присут-
ствовали пропущенные значения, в качестве целевой переменной мо-
дели;
строится модель линейной регрессии, в которой все остальные пере-
менные используются в качестве предикторов;
с помощью созданной модели предсказываются пропущенные значе-
ния в целевой переменной;
новые значения заменяют собой значения, установленные на преды-
дущей итерации.
Итеративный процесс продолжается для каждой переменной, в которой
изначально присутствовали пропуски. С каждым очередным шагом подстав-
ленные значения становятся более точными с учетом всех существующих
зависимостей в наборе данных.
Потенциал этого подхода объясняется способностью использовать в про-
цессе заполнения пропусков всю полноту информации о данных, включая
очевидные и скрытые зависимости.
3. Проверка на сходимость
Итеративный процесс повторяется множество раз с постоянным улучшени-
ем качества предсказанных значений. Завершается этот процесс при дости-
жении заранее заданного количества итераций или при выполнении условия
сходимости, т. е . тогда, когда подставленные значения перестают значимо
улучшаться.
Предельное количество итераций для этого алгоритма может варьиро-
ваться в зависимости от сложности набора данных и количества пропусков
в нем. На практике обычно используется фиксированное число итераций (на-
пример, 10 или 20) с последующей проверкой сходимости. Если за заданное
количество итераций алгоритм не достигает сходимости, можно добавить
дополнительные итерации.
Стоит отметить, что сходимость, достигнутая в результате применения
алгоритма MICE, не гарантирует получения оптимальных значений для про-
пусков, а позволяет добиться стабильного набора оценок. Качество итоговой
подстановки можно оценивать при помощи разных диагностических техник,
например сравнивая распределения фактических и подставленных значений
или проверяя новые значения на достоверность с учетом знаний о предмет-
ной области.
Итеративная природа метода MICE позволяет подбирать подходящие зна-
чения для пропусков в наборах данных со сложной структурой или при на-
личии определенного шаблона заполнения.
210 Сложные техники заполнения пропусков в данных
Более того, этот алгоритм способен одновременно обрабатывать перемен-
ные разных типов – категориальные, бинарные и непрерывные числовые –
путем использования соответствующих моделей регрессии. Такая гибкость
позволяет в результате подстановки пропусков сохранить основные статис-
тические характеристики исходного набора данных.
Хотя алгоритм MICE является более ресурсоемким в сравнении с простыми
методами подстановки, он помогает довольно точно заполнить пропущен-
ные значения и является незаменимым при работе со сложными наборами
данных со множеством пропусков.
В библиотеке Scikit-learn присутствует класс IterativeImputer, реализую-
щий алгоритм MICE, которым мы сейчас и воспользуемся:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
# Набор данных размером побольше
np.random.seed(42)
n_samples = 1000
age = np.random.randint(18, 65, n_samples)
salary = 30000 + 1000 * age + np.random.normal(0, 5000, n_samples)
experience = np.clip(age - 18, 0, None) + np.random.normal(0, 2, n_samples)
data={
'Age': age,
'Salary': salary,
'Experience': experience
}
df = pd.DataFrame(data)
# Функция для подсчета количества пропущенных значений
def missing_percentage(df):
return df.isnull().mean() * 100
print("Исходный набор данных:")
print(df.head())
# Разделяем исходный набор данных на обучающую и тестовую выборки
df_train, df_test = train_test_split(df, test_size=0.2, random_state=42)
# Добавляем в обучающий набор пропущенные значения
for col in df_train.columns:
mask = np.random.rand(len(df_train)) < 0.2
df_train.loc[mask, col] = np.nan
print("\nПроцент пропущенных значений в обучающей выборке:")
print(missing_percentage(df_train))
Использование продвинутых техник заполнения пропущенных значений 211
# Создаем копию тестовой выборки и искусственно включаем в нее пропущенные значения
df_test_missing = df_test.copy()
np.random.seed(42)
for column in df_test_missing.columns:
mask = np.random.rand(len(df_test_missing)) < 0.2
df_test_missing.loc[mask, column] = np.nan
# Создаем экземпляр класса MICE (IterativeImputer)
mice_imputer = IterativeImputer(random_state=42, max_iter=10)
# Обучаем алгоритм на обучающей выборке
mice_imputer.fit(df_train)
# Применяем алгоритм MICE на тестовой выборке с пропусками
df_imputed = pd.DataFrame(mice_imputer.transform(df_test_missing), columns=df.columns,
index=df_test.index)
# Рассчитываем ошибку подстановки
mse = mean_squared_error(df_test, df_imputed)
print(f"\nСреднеквадратическая ошибка подстановки: {mse:.2f}")
# Визуализируем результаты подстановки
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
for i, column in enumerate(df.columns):
axes[i].scatter(df_test[column], df_imputed[column], alpha=0.5)
axes[i].plot([df_test[column].min(), df_test[column].max()], [df_test[column].min(),
df_test[column].max()], 'r--', lw=2)
axes[i].set_xlabel(f'{column} (факт)')
axes[i].set_ylabel(f'{column} (предсказание)')
axes[i].set_title(f'Подстановка для поля {column}')
plt.tight_layout()
plt.show()
# Просматриваем датафрейм с подстановками
print("\nДатафрейм с подстановками:")
print(df_imputed.head())
Вывод:
Исходный набор данных:
Age
Salary Experience
0 56 91031.464046 35.660166
1 46 73115.540652 22.255476
2 32 66178.460560 13.944970
3 60 84351.465727 45.544503
4 25 57649.020890 10.322518
Процент пропущенных значений в обучающей выборке:
Age
20.250
Salary
20.875
Experience 19.375
dtype: float64
212 Сложные техники заполнения пропусков в данных
Среднеквадратическая ошибка подстановки: 2267515.62
Датафрейм с подстановками:
Age
Salary Experience
521 25.000000 53146.928340 7.019677
737 38.000000 69449.091183 17.828350
740 34.000000 63561.233315 17.332625
660 38.000000 69119.570670 18.717026
411 60.590668 92216.697247 42.897213
Рис. 4 .2 Результаты подстановки пропущенных значений
с помощью алгоритма MICE
Чем этот фрагмент кода отличается от предыдущего? Здесь мы создали
датафрейм размером побольше. После разделения набора данных на обуча-
ющую и тестовую выборки мы случайным образом добавили 20 % пропусков
в обучающие данные, после чего создали копию тестовой выборки и в нее
также добавили пропуски, которые и будем заполнять. Оценивать процедуру
подстановки мы будем путем сравнения с нетронутой тестовой выборкой без
пропущенных значений.
Далее мы создали экземпляр класса IterativeImputer (MICE), передав ему
аргумент max_iter=10 с количеством итераций. После обучения модели под-
становки на обучающей выборке мы применили алгоритм к тестовым дан-
ным, а затем сравнили результаты с оригиналом, вычислив метрику MSE.
В заключение мы вывели три графика с качеством подстановки значений.
Как видите, по всем трем переменным замещенные значения находятся
довольно близко к идеальной диагонали, что говорит о довольно точно вы-
полненной подстановке.
4.1.3. Использование моделей машинного обучения
для подстановки
Еще одним распространенным способом замены пропущенных значений
является использование с этой целью моделей машинного обучения. В этом
Использование продвинутых техник заполнения пропущенных значений 213
случае мы рассматриваем процесс подстановки как задачу обучения с учи-
телем, в которой каждая переменная с пропусками фигурирует в качестве
целевой переменной, а предсказание выполняется на основе остальных пре-
дикторов.
В отличие от простой процедуры подстановки, этот метод так же, как и рас-
смотренный выше алгоритм MICE, позволяет выполнять замену интеллекту-
ально, основываясь на сложных зависимостях между переменными. К при-
меру, мы могли бы заполнить пропуски в столбце с зарплатой на основе
данных в столбцах с возрастом, уровнем образования и видом деятельности.
При этом стоит помнить, что выбор модели машинного обучения в этом
случае играет очень важную роль. Также для выполнения качественной под-
становки необходимо позаботиться о конструировании признаков и возмож-
ном переобучении модели, воспользовавшись техникой кросс-валидации.
К примеру, мы можем воспользоваться для заполнения пропусков в дан-
ных моделью регрессии на основе случайного леса, которая обычно показы-
вает хорошее качество при работе с наборами данных, в которых имеются
нелинейные зависимости между переменными. В процессе использования
этой модели создается множество деревьев решений, выходы которых ис-
пользуются для предсказания значений пропусков.
Давайте посмотрим на примере, как это работает:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
from sklearn.impute import SimpleImputer
# Набор данных размером побольше
np.random.seed(42)
n_samples = 1000
age = np.random.randint(18, 65, n_samples)
salary = 30000 + 1000 * age + np.random.normal(0, 5000, n_samples)
experience = np.clip(age - 18, 0, None) + np.random.normal(0, 2, n_samples)
data={
'Age': age,
'Salary': salary,
'Experience': experience
}
df = pd.DataFrame(data)
print("Исходный набор данных:")
print(df.head())
print("\nПроцент пропущенных значений в обучающей выборке:")
print(df_train.isnull().mean() * 100)
# Разделяем исходный набор данных на обучающую и тестовую выборки
df_train, df_test = train_test_split(df, test_size=0.2, random_state=42)
214 Сложные техники заполнения пропусков в данных
# Добавляем в обучающий набор пропущенные значения
for col in df_train.columns:
mask = np.random.rand(len(df_train)) < 0.2
df_train.loc[mask, col] = np.nan
# Создаем копию тестовой выборки и искусственно включаем в нее пропущенные значения
df_test_missing = df_test.copy()
np.random.seed(42)
for column in df_test_missing.columns:
mask = np.random.rand(len(df_test_missing)) < 0.2
df_test_missing.loc[mask, column] = np.nan
# Функция для выполнения подстановки с использованием модели случайного леса
def rf_impute(df, target_column):
# Разделяем данные на строки с пропущенными и непропущенными значениями для целевой
переменной
train_df = df[df[target_column].notna()]
test_df = df[df[target_column].isna()]
# Подготавливаем признаки и целевую переменную
X_train = train_df .drop(target_column, axis=1)
y_train = train_df[target_column]
X_test = test_df .drop(target_column, axis=1)
# Выполняем простую подстановку для других признаков (требуется для модели случайного
леса)
imp = SimpleImputer(strategy='mean')
X_train_imputed = pd.DataFrame(imp.fit_transform(X_train), columns=X_train.columns)
X_test_imputed = pd.DataFrame(imp.transform(X_test), columns=X_test.columns)
# Обучаем модель
rf_model = RandomForestRegressor(n_estimators=100, random_state=42)
rf_model.fit(X_train_imputed, y_train)
# Предсказываем пропущенные значения
predicted_values = rf_model.predict(X_test_imputed)
# Заполняем пропуски в исходном датафрейме
df.loc[df[target_column].isna(), target_column] = predicted_values
return df
# Выполняем метод подстановки с использованием модели случайного леса для каждого столбца
for column in df_test_missing.columns:
df_test_missing = rf_impute(df_test_missing, column)
# Рассчитываем ошибку подстановки
mse = mean_squared_error(df_test, df_test_missing)
print(f"\nСреднеквадратическая ошибка подстановки: {mse:.2f}")
# Визуализируем результаты подстановки
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
for i, column in enumerate(df.columns):
axes[i].scatter(df_test[column], df_test_missing[column], alpha=0.5)
axes[i].plot([df_test[column].min(), df_test[column].max()], [df_test[column].min(),
Использование продвинутых техник заполнения пропущенных значений 215
df_test[column].max()], 'r--', lw=2)
axes[i].set_xlabel(f'{column} (факт)')
axes[i].set_ylabel(f'{column} (предсказание)')
axes[i].set_title(f'Подстановка для поля {column}')
plt.tight_layout()
plt.show()
# Просматриваем датафрейм с подстановками
print("\nДатафрейм с подстановками:")
print(df_test_missing.head())
Вывод:
Исходный набор данных:
Age
Salary Experience
0 56 91031.464046 35.660166
1 46 73115.540652 22.255476
2 32 66178.460560 13.944970
3 60 84351.465727 45.544503
4 25 57649.020890 10.322518
Процент пропущенных значений в обучающей выборке:
Age
20.250
Salary
20.875
Experience 19.375
dtype: float64
Среднеквадратическая ошибка подстановки: 3829075.50
Датафрейм с подстановками:
Age
Salary Experience
521 25.00 53146.928340 7.781664
737 38.00 67978.130766 17.828350
740 34.00 64113.302888 17.332625
660 38.00 69119.570670 18.717026
411 59.54 92216.697247 42.897213
Рис. 4 .3 Результаты подстановки пропущенных значений
с помощью модели случайного леса
216 Сложные техники заполнения пропусков в данных
Из нового здесь появилась написанная функция rf_impute(), принима-
ющая на вход датафрейм и столбец и выполняющая подстановку пропу-
щенных значений. В этой функции мы сначала подготавливаем признаки
и целевую переменную для будущей модели на основе входных аргументов,
затем выполняем простую подстановку на основе средних значений для
других предикторов, после чего создаем модель на основе класса RandomFo-
restRegressor, обучаем ее и предсказываем значения для пропусков в пере-
данном столбце.
Эта функция вызывается для всех столбцов в тестовой выборке в цикле.
Оценка качества подстановки и графики здесь аналогичные предыдущим
примерам. На рис. 4 .3 вы видите, что точность подстановки значений, как
и в случае с использованием алгоритма MICE, оказалась на высоком уровне.
Ограничения у этого метода подстановки пропущенных значений при-
близительно такие же, как у алгоритма MICE. Они включают в себя высокое
потребление ресурсов при работе с объемными наборами данных и риск
переобучения.
Выбор наиболее подходящего способа замены пропусков с учетом всех
особенностей конкретного сценария обеспечит вам наилучшую подготовку
исходных данных для дальнейшего анализа.
В следующем разделе мы, в частности, поговорим о продвинутых методах
подстановки пропущенных значений в категориальных переменных.
4.2. Обработка пропущенных значений
в больших наборах данных
Обработка пропусков в больших данных обычно связана с отдельным набо-
ром сложностей, не характерных для маленьких наборов. При росте количест-
ва наблюдений и переменных влияние пропущенных значений увеличива-
ется. В то же время размер и сложность датафреймов зачастую не позволяют
использовать традиционные техники подстановки пропусков – они могут
занимать много времени и не приводить к ожидаемым результатам.
Если говорить о проблемах, связанных с заменой пропусков в больших
наборах данных, можно выделить следующие из них:
вычислительная сложность. С ростом объема данных увеличивается
и время, требующееся на применение сложных алгоритмов, отвеча-
ющих за подстановку пропущенных значений. Техники, работающие
молниеносно на маленьких наборах данных, бывают абсолютно непри-
менимы к большим;
сложные зависимости. В больших наборах данных зачастую присут-
ствуют очень сложные зависимости между переменными, что не по-
зволяет применять прямолинейные способы подстановки значений
без риска потерять важные сведения об имеющихся шаблонах;
Обработка пропущенных значений в больших наборах данных 217
неоднородность данных. В больших наборах данных информация
часто поступает из разных источников, что приводит к образованию
разнородности структуры. Это может затруднять применение типовых
стратегий по замене пропущенных значений ко всему набору данных;
чувствительность ко времени. Зачастую в больших наборах данных
(с потоковой информацией или аналитикой в реальном времени) на
первый план выходит скорость, необходимая для выполнения опера-
ции замены пропусков. В таких условиях использовать традиционные
техники подстановки бывает очень проблематично.
В связи с озвученными проблемами мы в этом разделе рассмотрим не-
сколько специальных техник по замене пропущенных значений, примени-
мых к большим наборам данных. Мы затронем три следующих ключевых
аспекта.
1. Оптимизация техник подстановки с целью обеспечения масшта-
бируемости. В этом разделе мы научимся адаптировать и оптимизи-
ровать существующие техники подстановки для работы с большими
наборами данных. Мы рассмотрим способы разбиения данных на бло-
ки, использование приблизительных методов и задействование со-
временных аппаратных средств.
2. Обработка столбцов с большим количеством пропусков. Здесь мы
поговорим о способах обработки переменных, содержащих приличную
долю пропусков. Мы научимся оставлять или удалять столбцы по усло-
вию и выполнять замену пропусков в разреженных данных.
3. Использование распределенных вычислений для обработки про-
пущенных значений. В этом разделе мы посмотрим, как можно за-
действовать фреймворки распределенных вычислений для распарал-
леливания задач по замене пропусков с применением нескольких
ядер и рабочих станций. Этот подход способен существенно снизить
время выполнения процедуры подстановки при работе с объемными
данными.
4.2.1. Оптимизация техник подстановки
с целью обеспечения масштабируемости
При работе с большими наборами данных применение метода k-ближайших
соседей или метода множественной подстановки с помощью цепных урав-
нений (MICE) с целью подстановки пропущенных значений может оказаться
неэффективным с точки зрения затрачиваемого времени. Вычислительная
сложность этих алгоритмов напрямую связана с количеством наблюдений
в наборе данных, поскольку они так или иначе завязаны на определение рас-
стояний между наблюдениями или выполнение определенного количества
итераций по обработке данных. С целью масштабирования этих методов
можно применить следующие стратегии.
218 Сложные техники заполнения пропусков в данных
1. Разделение набора данных на блоки
Эта техника предполагает применение техник по замене пропущенных зна-
чений отдельно к разным блокам набора данных. Это позволяет сэкономить
память и время обработки данных. Особенно эффективным этот способ мо-
жет оказаться при работе с данными, не помещающимися в памяти, или
в распределенной вычислительной системе.
Разделение данных на блоки позволяет обрабатывать разные сегменты
данных параллельно, что также положительно сказывается на вычислитель-
ной эффективности процесса. Кроме того, такой подход обеспечивает до-
полнительную гибкость при работе с наборами данных с отличающимися
сегментами, поскольку позволяет к каждому блоку применить свою технику
в зависимости от его особенностей.
К примеру, в наборе данных с географическими областями вы могли бы
применить к каждому региону свою технику замены пропусков, учитываю-
щую местные особенности и шаблоны.
2. Приблизительные методы
При работе с большими данными вы можете воспользоваться приблизи-
тельными методами расчета, предполагающими некий компромисс между
точностью вычислений и их скоростью. К примеру, при использовании ме-
тода KNN для заполнения пропусков можно искать соседние наблюдения
приблизительно, а не точно. Такие подходы бывают особенно эффектив-
ны при работе с данными, содержащими огромное количество переменных
и/или наблюдений, где использование традиционных способов подстановки
пропусков может быть очень затратным в плане ресурсов.
Одним из наиболее популярных приблизительных методов является метод
локально-чувствительного хеширования (Locality-Sensitive Hashing – LSH),
с помощью которого можно существенно ускорить процесс нахождения бли-
жайших соседей. Этот метод разделяет похожие наблюдения на группы при
помощи хеширования, что позволяет довольно быстро извлекать их при
необходимости. В контексте замены пропусков с использованием метода
k-ближайших соседей это означает, что мы можем за достаточно малое вре-
мя выбрать схожие наблюдения и по ним рассчитать нужные нам значения.
Еще одна техника полагается на использование случайных проекций, что
позволяет снизить размерность данных с более или менее точным сохране-
нием расстояний между наблюдениями. Эта техника позволяет нивелировать
влияние проклятия размерности, нависающего над нами при использовании
методов, подобных KNN.
Хотя эти приблизительные методы могут и будут уступать в точности рас-
четов традиционным способам замены пропущенных значений, при их ис-
пользовании мы можем существенно сократить время, необходимое для об-
работки больших массивов данных.
Обработка пропущенных значений в больших наборах данных 219
3. Отбор признаков
При работе с большими наборами данных вы можете в процессе подстановки
пропусков использовать не все имеющиеся переменные, а только наиболее
значимые. Этот подход связан с анализом зависимостей между предикто-
рами в процессе выбора наиболее информативных из них для предсказания
значений для пропусков и также связан с компромиссом между точностью
и скоростью расчетов.
Для отбора признаков вы можете воспользоваться разными методами,
включая следующие:
корреляционный анализ: поиск сильно коррелирующих переменных
в наборе данных поможет при выборе достаточно информативного
подмножества признаков для подстановки;
взаимная информация: эта техника связана с вычислением взаимных
зависимостей между переменными, помогающих определить набор
предикторов, необходимый и достаточный для качественной подста-
новки пропущенных значений;
рекурсивное исключение признаков: этот итеративный метод после-
довательно исключает наименее важные переменные из набора на
основе качества предсказаний.
Оставив только наиболее значимые переменные из набора, вы можете
воспользоваться традиционными методами KNN или MICE для замены про-
пусков.
Кроме того, отбор признаков может даже повысить качество подстановки
пропусков за счет уменьшения шума и склонности модели к переобучению.
В результате ваша модель сможет сосредоточиться на наиболее информа-
тивных зависимостях в данных, что может пойти на пользу при вычислении
пропущенных значений.
4. Параллельная обработка
Одновременное использование нескольких ядер процессора или задейство-
вание распределенных вычислительных систем может существенно повы-
сить быстродействие при подстановке пропущенных значений в больших
наборах данных за счет распределения нагрузки. К примеру, при работе с на-
бором данных, состоящим из многих миллионов строк, вы могли бы разбить
наблюдения на блоки и запустить процесс замены пропусков в них парал-
лельно на нескольких ядрах или узлах в кластере.
Параллельная обработка данных может быть запущена с помощью разных
механизмов и фреймворков, включая следующие:
многопоточность: использование разных потоков в рамках одной ма-
шины для конкурентной обработки разных частей набора данных;
многопроцессность: задействование нескольких ядер центрального
процессора для параллельного выполнения операции подстановки
220 Сложные техники заполнения пропусков в данных
пропущенных значений, что бывает удобно при использовании таких
ресурсоемких алгоритмов, как метод k-ближайших соседей;
фреймворки распределенных вычислений: инструменты, подобные
Apache Spark или Dask, позволяют выполнять подстановку пропущен-
ных значений с использованием ресурсов множества машин в класте-
ре, что бывает очень удобно при работе с большими наборами данных.
Кроме того, применение распределенных систем вычислений позволяет
воспользоваться и более сложными техниками замены пропусков, что было
бы невозможно при задействовании ресурсов одной рабочей станции. К при-
меру, при использовании кластера с распределенной обработкой вы могли
бы прибегнуть к помощи довольно требовательного к ресурсам метода MICE
для обработки большого набора данных.
В то же время важно знать, что не все методы подстановки пропущенных
значений можно легко распараллелить. Некоторые техники полагаются на
данные из всего набора или учитывают последовательности значений. В та-
ких случаях можно изменить применяемые алгоритмы или воспользоваться
гибридными подходами для сохранения целостности данных и получения
эффекта от распределенных вычислений.
Пример применения простой подстановки пропусков
для отдельных столбцов
При работе с большими наборами данных бывает удобно применять более
простые техники подстановки пропущенных значений к отдельным столб-
цам, к примеру к тем, в которых пропусков не так много. Это позволяет
сократить время обработки данных без заметного влияния на качество под-
становки. Простые методы замены с использованием среднего значения, ме-
дианы или моды являются довольно эффективными и быстро выполняются
даже при наличии большого количества наблюдений.
Подобные методы подстановки хорошо работают со столбцами с относи-
тельно небольшим количеством пропущенных значений, где влияние за-
мен на распределение будет минимальным. К примеру, если в колонке есть
всего 5 % пропусков, применение метода подстановки на основе среднего
или медианы в большинстве случаев существенно не повлияет на основные
статистические характеристики данных.
Кроме того, простые методы подстановки часто являются хорошо масшта-
бируемыми и легко распараллеливаются в распределенных системах вычис-
лений. Применяя такие методы к подходящим для этого столбцам, аналитик
может обеспечить целостность данных, не жертвуя при этом скоростью вы-
числений.
import pandas as pd
import numpy as np
from sklearn.impute import SimpleImputer
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
from sklearn.ensemble import RandomForestRegressor
Обработка пропущенных значений в больших наборах данных 221
import time
# Генерируем большой набор данных
np.random.seed(42)
n_samples = 100000
data={
'Age': np.random.randint(18, 80, n_samples),
'Salary': np.random.randint(30000, 150000, n_samples),
'Experience': np.random.randint(0, 40, n_samples),
'Education': np.random.choice(['High School', 'Bachelor', 'Master', 'PhD'], n_samples)
}
# Добавляем пропуски
for col in data:
mask = np.random.random(n_samples) < 0.2 # 20% пропущенных значений
data[col] = np.where(mask, None, data[col])
df_large = pd.DataFrame(data)
# 1. Простая подстановка
start_time = time.time()
simple_imputer = SimpleImputer(strategy='mean')
numeric_cols = ['Age', 'Salary', 'Experience']
df_simple_imputed = df_large.copy()
df_simple_imputed[numeric_cols] = simple_imputer.fit_transform(df_large[numeric_cols])
df_simple_imputed['Education'] = df_simple_imputed['Education'].fillna(df_simple_
imputed['Education'].mode()[0])
time_simple_imputer = time.time() - start_time
# 2. Метод множественной подстановки с помощью цепных уравнений (MICE)
start_time = time.time()
mice_imputer = IterativeImputer(estimator=RandomForestRegressor(), max_iter=10, random_
state=42)
df_mice_imputed = df_large.copy()
df_mice_imputed[numeric_cols] = mice_imputer.fit_transform(df_large[numeric_cols])
df_mice_imputed['Education'] = df_mice_imputed['Education'].fillna(df_mice_
imputed['Education'].mode()[0])
time_mice_imputer = time.time() - start_time
# 3. Собственный метод подстановки на основе бизнес-правил
def custom_impute(df):
df = df.copy()
df['Age'] = df['Age'].fillna(df.groupby('Education')['Age'].transform('median'))
df['Salary'] = df['Salary'].fillna(df.groupby(['Education', 'Experience'])['Salary'].
transform('median'))
# Предполагаем, что работать сотрудники начинают с 22 лет
df['Experience'] = df['Experience'].fillna(df['Age'] - 22)
# Уровень образования по умолчанию
df['Education'] = df['Education'].fillna('High School')
return df
start_time = time.time()
df_custom_imputed = custom_impute(df_large)
time_custom_imputed = time.time() - start_time
222 Сложные техники заполнения пропусков в данных
# Сравним результаты
print(f"Исходные данные (первые 5 строк).")
print(df_large.head())
print(f"\nПростая подстановка (первые 5 строк). Время: {time_simple_imputer:.2f} с.")
print(df_simple_imputed.head())
print(f"\nПодстановка MICE (первые 5 строк). Время: {time_mice_imputer:.2f} с.")
print(df_mice_imputed.head())
print(f"\nСобственный метод подстановки (первые 5 строк). Время: {time_custom_imputed:.2f}
с.")
print(df_custom_imputed.head())
# Рассчитываем и выводим доли пропущенных значений
def missing_percentage(df):
return (df.isnull().sum() / len(df)) * 100
print("\nПроцент пропущенных значений:")
print("\nИсходные данные:", missing_percentage(df_large))
print("\nПростая подстановка:", missing_percentage(df_simple_imputed))
print("\nПодстановка MICE:", missing_percentage(df_mice_imputed))
print("\nСобственный метод подстановки:", missing_percentage(df_custom_imputed))
Вывод:
Исходные данные (первые 5 строк).
Age Salary Experience Education
0 56 None
None Bachelor
1 69 None
14 Bachelor
2 None None
None Master
3 32 147159
2 Master
4 60 None
4 Bachelor
Простая подстановка (первые 5 строк). Время: 0.06 с.
Age
Salary Experience Education
0 56.000000 89873.840665 19.547377 Bachelor
1 69.000000 89873.840665 14.000000 Bachelor
2 48.512202 89873.840665 19.547377 Master
3 32.000000 147159.000000 2.000000 Master
4 60.000000 89873.840665 4.000000 Bachelor
Подстановка MICE (первые 5 строк). Время: 499.96 с.
Age
Salary Experience Education
0 56.00 106294.768723
9.69 Bachelor
1 69.00 85746.466535
14.00 Bachelor
2 53.96 116732.680000
12.35 Master
3 32.00 147159.000000
2.00 Master
4 60.00 95657.299284
4.00 Bachelor
Собственный метод подстановки (первые 5 строк). Время: 0.12 с.
Age Salary Experience Education
0 56.0
NaN
34.0 Bachelor
1 69.0 94140.0
14.0 Bachelor
2 48.0
NaN
26.0 Master
3 32.0 147159.0
2.0 Master
4 60.0 91876.0
4.0 Bachelor
Обработка пропущенных значений в больших наборах данных 223
Процент пропущенных значений:
Исходные данные:
Age
20.096
Salary
20.049
Experience 19.772
Education
19.999
dtype: float64
Простая подстановка:
Age
0.0
Salary
0.0
Experience 0.0
Education
0.0
dtype: float64
Подстановка MICE:
Age
0.0
Salary
0.0
Experience 0.0
Education
0.0
dtype: float64
Собственный метод подстановки:
Age
3.940
Salary
7.209
Experience 0.812
Education
0.000
dtype: float64
Что мы здесь сделали? Сначала сгенерировали набор данных, состоящий
из 100 000 строк, со столбцами Age, Salary, Experience и Education, после чего
разбавили его пропущенными значениями в объеме 20 %, чтобы сымити-
ровать реальный сценарий. После этого мы воспользовались классом Sim-
pleImputer и заменили пропуски в числовых столбцах на средние значения,
а в категориальном – на самый распространенный класс. Это очень простой
и быстрый метод (он занял всего 0.06 с), но он не учитывает имеющиеся за-
висимости между переменными.
Далее мы применили метод множественной подстановки с помощью цеп-
ных уравнений (MICE), воспользовавшись классом IterativeImputer. Для учета
нелинейных зависимостей в данных мы выбрали модель RandomForestRegres-
sor. Это уже более сложный подход к замене пропусков, и за его использо-
вание нам пришлось расплачиваться потраченным временем в количестве
499.96 с.
Затем мы применили пользовательский подход к замене пропущенных
значений, воспользовавшись знанием предметной области. В столбце Age
мы заполнили пропуски медианными значениями возраста для каждого
уровня образования. Для столбца Salary мы взяли медианную зарплату для
сочетаний столбцов E ducation и Experience. В столбце Experience мы вместо
пропущенных значений вставляем возраст сотрудника за вычетом 22, по-
224 Сложные техники заполнения пропусков в данных
скольку считаем этот возраст средним для приема на работу. В столбце
Education мы заменяем пропуски на значение по умолчанию – 'High School'.
Такой подход позволяет реализовать при подстановке пропущенных зна-
чений накопленные знания о конкретной предметной области, что бывает
вполне уместно. К тому же данная реализация оказалась очень быстрой
и заняла всего 0.12 с.
Здесь мы рассмотрели три способа заполнения пропущенных значений на
практике, каждый из которых обладает своими достоинствами и недостатка-
ми. Выбор конкретного способа должен производиться с учетом требований
и обстоятельств вашего сценария.
4.2.2. Обработка столбцов с большим количеством
пропущенных значений
При работе с большими наборами данных мы зачастую встречаемся со столб-
цами с большим количеством пропущенных значений. В задачах анализа
данных и машинного обучения переменные с 50 % и более пропусков до-
ставляют немало хлопот.
Проблемы, связанные с такими столбцами, объясняются следующими при-
чинами:
ограниченная информация. В столбце с большим количеством про-
пусков содержится не так много значимой информации, что может
привести к искажению анализа и прогноза модели. Недостаток инфор-
мации также способствует неправильной оценке значимости перемен-
ных и потенциальному пропуску важных шаблонов в данных;
недостаток статистической мощности. Большая потеря информа-
ции в исходных данных может приводить к получению менее точных
статистических выводов и созданию слабых предсказательных моде-
лей. Недостаток статистической мощности может обернуться появ-
лением ошибок II рода, когда игнорируются истинные зависимости
в данных. Кроме того, это может приводить к расширению довери-
тельных интервалов, что затрудняет получение достоверных выводов
на основе данных;
потенциальные систематические ошибки оценки. Если пропуски
в данных не являются абсолютно случайными (MCAR), подстановка
значений вместо них может приводить к появлению систематических
ошибок в наборе данных. Особенно опасно это может быть, если про-
пуски зависят от данных, не представленных в наблюдениях (MNAR),
поскольку может вести к систематическим ошибкам в итоговом ана-
лизе. К примеру, если в наборе данных пропущенные значения чаще
появляются у клиентов с большими доходами, подстановка значений
на основе существующих данных может привести к занижению общего
уровня дохода;
Обработка пропущенных значений в больших наборах данных 225
вычислительная сложность. Попытки заполнить пропуски или про-
анализировать такие столбцы могут быть связаны с большими вы-
числительными расходами без особой выгоды. Это особенно важно
учитывать при работе с большими наборами данных, где сложные алго-
ритмы подстановки пропусков вроде MICE или KNN могут значительно
увеличивать время обработки и расход ресурсов. В результате потери
на ресурсах могут превысить пользу от повышения качества модели,
особенно если полученные в результате замены значения не вызывают
особого доверия;
качество данных. Высокая доля пропусков в столбце может говорить об
ошибках в процессе сбора информации или общем недостатке качест-
ва данных. В таком случае больше пользы может быть от исправления
проблем, связанных с получением данных, чем от попыток восстано-
вить пропущенные значения на основе чересчур скудной информации.
В таких случаях аналитики вынуждены принимать важное решение о том,
избавиться от столбца или применять к нему сложные техники подстановки
пропусков. Это решение должно основываться на следующих факторах:
значимость переменной для анализа или модели;
тип пропущенных значений (абсолютно случайно пропущенные значе-
ния (Missing completely at random – MCAR), случайно пропущенные зна-
чения (Missing at random – MAR) или неслучайно пропущенные значения
(Missing, not at random – MNAR);
доступные вычислительные ресурсы;
потенциальное влияние на следующие этапы анализа.
Если вы считаете столбец критически важным, то необходимо рассмотреть
возможность применения к нему сложных методов подстановки вроде MICE
или техники на основе моделей машинного обучения. Но не стоит забывать
о вычислительной сложности и ресурсоемкости этих методов.
Наоборот, если вам кажется, что польза от восстановления пропущенных
значений в столбце не перекроет потери, связанные с применением сложных
методов подстановки, лучше будет полностью исключить этот столбец. Это
поможет упростить будущую модель и повысить ее качество и надежность.
Иногда применяется смешанный подход, при котором столбцы, содер-
жащие чрезмерное количество пропущенных значений, исключаются из
анализа, а переменные с умеренным числом пропусков подвергаются рас-
ширенному анализу на предмет восстановления значений.
Когда стоит исключать столбцы из анализа
Если в столбце отсутствует больше половины значений, вряд ли он сможет
помочь модели лучше предсказывать целевую переменную. В таких случаях
может быть лучше избавиться от него на этапе подготовки данных, особенно
если характер пропусков в столбце является случайным. Такой подход, на-
зываемый удалением столбцов, или исключением признаков, может сущест-
226 Сложные техники заполнения пропусков в данных
венно упростить будущую модель и положительно сказаться на эффектив-
ности анализа.
Но перед тем как расставаться со столбцом, ответьте себе на вопросы, ка-
сающиеся характера пропусков (MCAR, MAR или MNAR), важности столбца
для решения бизнес-задачи, внесения неразберихи в модель вследствие уда-
ления столбца и возможности использования знаний о предметной области
для заполнения пропусков.
Иногда бывает так, что столбец с большим количеством пропущенных
значений тем не менее может содержать важную информацию для будущей
модели. Более того, даже сам факт отсутствия значений зачастую может быть
информативным. В таких случаях вместо отказа от столбца стоит рассмот-
реть возможность создания дополнительного бинарного признака, говоря-
щего о наличии или отсутствии значения.
Так или иначе, решение об исключении столбцов из анализа стоит при-
нимать отдельно в каждом конкретном случае, принимая во внимание все
перечисленные выше факторы.
Рассмотрим следующий пример:
import pandas as pd
import numpy as np
# Создаем объемный набор данных
np.random.seed(43)
n_samples = 1000000
data={
'Age': np.random.randint(18, 80, n_samples),
'Salary': np.random.randint(30000, 150000, n_samples),
'Experience': np.random.randint(0, 40, n_samples),
'Education': np.random.choice(['High School', 'Bachelor', 'Master', 'PhD'], n_samples),
'Department': np.random.choice(['Sales', 'Marketing', 'IT', 'HR', 'Finance'],
n_samples)
}
# Добавляем пропущенные значения в количестве от 10% до 70%
for col in data:
mask = np.random.random(n_samples) < np.random.uniform(0.1, 0.7) # от 10% до 70%
data[col] = np.where(mask, None, data[col])
df_large = pd.DataFrame(data)
df_large[['Age', 'Salary', 'Experience']] = df_large[['Age', 'Salary', 'Experience']].
apply(pd.to_numeric)
# Определяем порог для доли пропущенных значений
threshold = 0.5
# Рассчитываем доли пропусков в каждом столбце
missing_proportion = df_large.isnull().mean()
print("Проценты пропущенных значений:")
print(missing_proportion)
Обработка пропущенных значений в больших наборах данных 227
# Удаляем столбцы с долей пропущенных значений, превышающей заданный порог
df_large_cleaned = df_large.drop(columns=missing_proportion[missing_proportion >
threshold].index)
print("\nИсключенные столбцы:")
print(set(df_large.columns) - set(df_large_cleaned.columns))
# Просмотр очищенного датафрейма
print("\nОчищенный датафрейм:")
print(df_large_cleaned.head())
# Рассчитываем количество строк с как минимум одним пропуском
rows_with_missing = df_large_cleaned.isnull().any(axis=1).sum()
print(f"\nСтрок с пропущенными значениями: {rows_with_missing} ({rows_with_missing/len(df_
large_cleaned):.2%})")
# Опционально: выполняем подстановку оставшихся пропусков
from sklearn.impute import SimpleImputer
# Разделяем числовые и категориальные столбцы
numeric_cols = df_large_cleaned.select_dtypes(include=[np.number]).columns
categorical_cols = df_large_cleaned.select_dtypes(exclude=[np.number]).columns
# Заполняем пропуски в числовых столбцах с помощью медианы
num_imputer = SimpleImputer(strategy='median')
df_large_cleaned[numeric_cols] = num_imputer.fit_transform(df_large_cleaned[numeric_cols])
# Заполняем пропуски в категориальных столбцах с помощью самого часто встречающегося
значения
cat_imputer = SimpleImputer(missing_values=None, strategy='most_frequent')
df_large_cleaned[categorical_cols] = cat_imputer.fit_transform(df_large_
cleaned[categorical_cols])
print("\nИтоговый датафрейм после замены пропусков:")
print(df_large_cleaned.head())
print("\nПропущенных значений после подстановки:")
print(df_large_cleaned.isnull().sum())
Вывод:
Проценты пропущенных значений:
Age
0.591153
Salary
0.448150
Experience 0.340927
Education
0.346275
Department 0.640741
dtype: float64
Исключенные столбцы:
{'Department', 'Age'}
Очищенный датафрейм:
Salary Experience Education
0 57607.0
8.0 Master
228 Сложные техники заполнения пропусков в данных
1 129667.0
6.0
PhD
2
NaN
20.0
None
3 91224.0
NaN Bachelor
4
NaN
NaN Master
Строк с пропущенными значениями: 762384 (76.24%)
Итоговый датафрейм после замены пропусков:
Salary Experience Education
0 57607.0
8.0 Master
1 129667.0
6.0
PhD
2 90081.0
20.0 Master
3 91224.0
19.0 Bachelor
4 90081.0
19.0 Master
Пропущенных значений после подстановки:
Salary
0
Experience 0
Education
0
dtype: int64
Что мы здесь делаем? Сначала мы сгенерировали набор данных, состоя-
щий из 1 млн записей с пятью переменными: Age, Salary, Experience, Education
и Department. Затем случайным образом добавили в столбцы от 10 % до 70 %
пропущенных значений, чтобы имитировать реальный набор данных с раз-
ным количеством пропусков в разных столбцах. После этого вывели долю
пропусков в каждом столбце с помощью выражения df_large.isnull().mean().
Далее мы определили пороговое значение 0.5 и удалили из набора данных
столбцы, в которых доля пропущенных значений превышает эту отметку.
Попутно мы вывели список имен удаленных столбцов, а также первые не-
сколько строк в сокращенном датафрейме.
После этого мы вывели количество и долю строк, содержащих как мини-
мум одно пропущенное значение. Оказалось, что таких строк у нас 76.24 % .
Далее мы разделили наш датафрейм на числовые и категориальные столб-
цы, воспользовавшись методом select_dtypes(), и к каждому типу приме-
нили свое заполнение пропусков. Для числовых столбцов мы выполнили
подстановку на основе медианы, а для категориальных – на основе самых
часто встречающихся значений. В заключение мы вывели первые несколь-
ко строк итогового датафрейма и убедились в том, что пропусков в нем не
осталось.
Подстановка в столбцах с большим количеством пропусков
Если в столбце присутствует очень много пропущенных значений, но вы по
каким-то причинам считаете, что он очень важен для анализа, вы можете
применить к нему продвинутые техники подстановки вроде MICE или ме-
тода множественного восстановления. С помощью них вы можете извлечь
полезную информацию из столбца с учетом присутствующей неопределен-
ности. В частности, при использовании метода MICE для вас будет создано
Обработка пропущенных значений в больших наборах данных 229
несколько наборов значений для заполнения, и на основе них будет выбран
наиболее устойчивый вариант подстановки.
В то же время при работе с большими наборами данных очень важно со-
хранять баланс между точностью подстановки пропусков и вычислительной
эффективностью. В случаях, когда процедура подстановки оказывается из-
лишне ресурсозатратной, следует рассмотреть следующие варианты:
использовать более простые методы замены пропусков для определен-
ных столбцов с проверкой влияния на анализ;
воспользоваться распределенными вычислительными системами для
параллельного запуска процедуры подстановки;
рассмотреть альтернативы в виде применения методов матричной
факторизации, способных работать с пропущенными значениями на-
прямую.
4.2.3. Использование распределенных
вычислительных систем для заполнения пропусков
При работе с очень большими наборами данных процедура подстановки
пропущенных значений может стать настоящим камнем преткновения на
пути к очистке данных, особенно при желании использовать сложные методы
замены вроде MICE или KNN. Подобные методы часто связаны с итератив-
ными операциями или сложными вычислениями на основе большой части
набора данных, которые требуют немало времени и ресурсов. Для решения
проблем масштабирования таких задач аналитики часто прибегают к по-
мощи распределенных вычислительных систем, таких как фреймворки Dask
и Apache Spark.
С помощью этих инструментов можно обеспечить параллельное выпол-
нение процесса подстановки пропущенных значений, распределяя нагрузку
по разным машинам в рамках кластера. Процедура использования подобных
фреймворков может сводиться к следующей последовательности действий:
разделение набора данных на небольшие блоки данных (chunk), или
партиции (partition);
конкурентная обработка партиций с использованием ресурсов мно-
жества машин в кластере;
агрегирование результатов для представления в виде единого набора
данных.
Такой подход не только позволяет значительно ускорить процедуру заме-
ны пропущенных значений в наборе данных, но и в принципе дает возмож-
ность работать с большими наборами, с которыми в рамках одной рабочей
станции справиться невозможно. Кроме того, в подобных фреймворках за-
частую имеются встроенные механизмы обеспечения отказоустойчивости
и балансировки нагрузки, позволяющие поддерживать надежность и эффек-
тивность процессов при работе с большими данными.
230 Сложные техники заполнения пропусков в данных
При задействовании распределенных систем для заполнения пропущен-
ных значений необходимо учитывать компромиссы между вычислительной
эффективностью и точностью подстановки. Если простые методы, использу-
ющие для подстановки среднее или медиану, могут быть легко распараллеле-
ны, то более сложные алгоритмы могут потребовать определенной настройки
для поддержания своих статистических свойств в условиях распределенного
выполнения. В связи с этим делать выбор в пользу того или иного мето-
да стоит с учетом статистических требований к анализу и вычислительных
ограничений вашей инфраструктуры.
Использование библиотеки Dask для распределенной
подстановки пропусков
Dask представляет собой мощную библиотеку для выполнения распреде-
ленных вычислений, расширяющую функционал традиционных библиотек
для работы с данными, таких как Pandas и Scikit-learn. С помощью этой биб-
лиотеки можно эффективно распределять вычислительные операции по
ядрам центрального процессора и машинам в рамках кластера, что делает
ее незаменимой при обработке больших наборов данных. Архитектура Dask
позволяет без лишних усилий осуществлять распределение данных и вы-
числений, что дает возможность комфортно работать с наборами данных,
размер которых превышает объем памяти рабочей станции.
Одним из достоинств библиотеки Dask является наличие API, практи-
чески в точности повторяющего синтаксис Pandas и NumPy, что позволяет
практически без изменений обрабатывать локальные наборы данных и рас-
пределенные. Это бывает особенно удобно при выполнении подстановки
пропущенных значений, поскольку позволяет использовать существующие
алгоритмы замены с распределением нагрузки по разным узлам.
К примеру, при выполнении подстановки пропусков на основе среднего
значения или медианы библиотека Dask позволяет эффективно делать эти
расчеты применительно к разным партициям. Также эта библиотека хорошо
интегрирована с более сложными методами подстановки на основе KNN или
моделей машинного обучения, что делает возможным их применение к раз-
ным партициям и агрегирование полученных результатов.
Более того, гибкость Dask позволяет этой библиотеке эффективно адап-
тироваться к разным вычислительным окружениям, начиная с многоядер-
ных ноутбуков и заканчивая большими кластерами, что делает его универ-
сальным инструментом для масштабирования процесса обработки данных
в целом и операции по подстановке пропущенных значений в частности.
# !pip install dask
# !pip install pyarrow
import dask.dataframe as dd
import pandas as pd
import numpy as np
Обработка пропущенных значений в больших наборах данных 231
from sklearn.impute import SimpleImputer
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
from sklearn.ensemble import RandomForestRegressor
import time
# Генерируем набор данных
def create_sample_data(n_samples=1000):
np.random.seed(42)
data={
'Age': np.random.randint(18, 80, n_samples),
'Salary': np.random.randint(30000, 150000, n_samples),
'Experience': np.random.randint(0, 40, n_samples),
'Education': np.random.choice(['High School', 'Bachelor', 'Master', 'PhD'],
n_samples),
'Department': np.random.choice(['Sales', 'Marketing', 'IT', 'HR', 'Finance'],
n_samples)
}
df = pd.DataFrame(data)
# Добавляем пропущенные значения
for col in df.columns:
mask = np.random.random(n_samples) < 0.2 # 20% пропусков
df.loc[mask, col] = np.nan
return df
# Создаем набор данных
df_large = create_sample_data()
print("Исходный датафрейм:")
print(df_large.head())
# Преобразовываем датафрейм Pandas в датафрейм Dask
df_dask = dd.from_pandas(df_large, npartitions=10)
# 1. Простая замена пропусков с помощью средних значений
simple_imputer = SimpleImputer(strategy='mean')
def apply_simple_imputer(df):
# Отделяем числовые столбцы от категориальных
numeric_cols = df.select_dtypes(include=[np.number]).columns
categorical_cols = df.select_dtypes(exclude=[np.number]).columns
# Заменяем пропуски в числовых столбцах на средние значения
df[numeric_cols] = simple_imputer.fit_transform(df[numeric_cols])
# Заменяем пропуски в категориальных столбцах на наиболее часто встречающиеся значения
for col in categorical_cols:
df[col] = df[col].fillna(df[col].mode().iloc[0])
return df
df_dask_simple_imputed = df_dask.map_partitions(apply_simple_imputer)
232 Сложные техники заполнения пропусков в данных
# 2. Итеративная подстановка пропусков (MICE)
def apply_iterative_imputer(df):
numeric_cols = df.select_dtypes(include=[np.number]).columns
categorical_cols = df.select_dtypes(exclude=[np.number]).columns
# Заменяем пропуски в числовых столбцах с помощью модели случайного леса
iterative_imputer = IterativeImputer(estimator=RandomForestRegressor(), max_iter=10,
random_state=0)
df[numeric_cols] = iterative_imputer.fit_transform(df[numeric_cols])
# Заменяем пропуски в категориальных столбцах на наиболее часто встречающиеся значения
for col in categorical_cols:
df[col] = df[col].fillna(df[col].mode().iloc[0])
return df
df_dask_iterative_imputed = df_dask.map_partitions(apply_iterative_imputer)
# Рассчитываем результаты (запуская вычисления применительно к партициям)
start_time = time.time()
df_simple_imputed = df_dask_simple_imputed.compute()
time_simple_imputed = time.time() - start_time
start_time = time.time()
df_iterative_imputed = df_dask_iterative_imputed.compute()
time_iterative_imputed = time.time() - start_time
# Смотрим датафреймы с подставленными значениями
print(f"Результат простой подстановки ({time_simple_imputed:.2f} с.):")
print(df_simple_imputed.head())
print(f"\nРезультат итеративной подстановки ({time_iterative_imputed:.2f} с.):")
print(df_iterative_imputed.head())
# Сравниваем результаты
print("\nПропущенные значения после простой подстановки:")
print(df_simple_imputed.isnull().sum())
print("\nПропущенные значения после итеративной подстановки:")
print(df_iterative_imputed.isnull().sum())
# Опционально: анализируем влияние подстановки
print("\nСтатистика исходных данных:")
print(df_large.describe())
print("\nСтатистика данных после простой подстановки:")
print(df_simple_imputed.describe())
print("\nСтатистика данных после итеративной подстановки:")
print(df_iterative_imputed.describe())
Вывод:
Исходный датафрейм:
Age Salary Experience Education Department
0 56.0 92292.0
NaN Bachelor
IT
1 69.0 53833.0
0.0
PhD
NaN
2 46.0 34158.0
NaN Bachelor
Sales
3 32.0 92680.0
NaN Master
NaN
4 60.0 50309.0
14.0
NaN
IT
Обработка пропущенных значений в больших наборах данных 233
Результат простой подстановки (0.02 с.):
Age Salary Experience Education Department
0 56.0 92292.0 19.554217 Bachelor
IT
1 69.0 53833.0 0 .000000
PhD
IT
2 46.0 34158.0 19.554217 Bachelor
Sales
3 32.0 92680.0 19.554217 Master
IT
4 60.0 50309.0 14.000000 Master
IT
Результат итеративной подстановки (13.86 с.):
Age Salary Experience Education Department
0 56.0 92292.0
26.38 Bachelor
IT
1 69.0 53833.0
0.00
PhD
IT
2 46.0 34158.0
22.44 Bachelor
Sales
3 32.0 92680.0
26.01 Master
IT
4 60.0 50309.0
14.00 Master
IT
Пропущенные значения после простой подстановки:
Age
0
Salary
0
Experience 0
Education
0
Department 0
dtype: int64
Пропущенные значения после итеративной подстановки:
Age
0
Salary
0
Experience 0
Education
0
Department 0
dtype: int64
Статистика исходных данных:
Age
Salary Experience
count 816.000000
796.000000 786.000000
mean
49.675245 89324.604271 19.020356
std
18.029320 34948.457894 11.502626
min
18.000000 30055.000000 0 .000000
25%
35.000000 58742.500000 9.000000
50%
50.000000 88631.000000 19.000000
75%
66.000000 117910.000000 29.000000
max
79.000000 149803.000000 39.000000
Статистика данных после простой подстановки:
Age
Salary Experience
count 1000.000000 1000.000000 1000.000000
mean
49.643385 89397.909782 19.046767
std
16.296170 31202.927114 10.205499
min
18.000000 30055.000000
0.000000
25%
39.000000 66389.750000 12.000000
50%
49.025974 89946.512821 19.392405
75%
62.000000 111277.250000 26.250000
max
79.000000 149803.000000 39.000000
234 Сложные техники заполнения пропусков в данных
Статистика данных после итеративной подстановки:
Age
Salary Experience
count 1000.000000 1000.000000 1000.000000
mean
49.709810 89436.892115 19.031190
std
16.740464 32328.899169 10.577141
min
18.000000 30055.000000
0.000000
25%
37.567500 64019.000000 11.000000
50%
50.000000 89305.630000 19.000000
75%
63.000000 113818.185000 27.032500
max
79.000000 149803.000000 39.000000
Что мы здесь сделали?
1. Написали функцию create_sample_data() для создания датафрейма
с числовыми и категориальными переменными с пропущенными зна-
чениям в количестве 20 % от исходного числа строк.
2. Преобразовали датафрейм Pandas в датафрейм Dask с помощью функ-
ции f rom_pandas() с указанием десяти партиций, что позволяет вы-
полнять операции параллельно с использованием десяти ядер или
машин.
3. Написали функцию, выполняющую простую подстановку для числовых
и категориальных столбцов, и запустили ее для всех партиций при по-
мощи метода map_partitions().
4. Написали функцию, выполняющую сложную подстановку для число-
вых и категориальных столбцов с использованием алгоритма MICE
и также запустили ее для всех партиций.
5. Запустили вычисления при помощи метода compute().
6. Сравнили результаты простой и итеративной подстановок и вывели
информацию о пропущенных значениях и статистику по обоим дата-
фреймам.
Этот пример демонстрирует способ подстановки пропущенных значений
при помощи библиотеки Dask. Однако все преимущества этого метода можно
почувствовать только при работе с действительно большими данными, не
помещающимися в память одной рабочей станции.
Использование фреймворка Apache Spark
для распределенной подстановки пропусков
Apache Spark – это еще один мощный фреймворк, предназначенный для рас-
пределенной обработки данных. Библиотека MLlib, входящая в состав Apache
Spark, предлагает собственные инструменты для замены пропущенных зна-
чений в данных, адаптированные для работы в распределенной среде. В ос-
новном фреймворк Apache Spark используется в промышленной среде при
работе с действительно большими объемами данных.
Распределенная модель вычислений Apache Spark позволяет эффективно
обрабатывать большие массивы данных с использованием кластеров, со-
стоящих из множества машин, что идеально подходит для работы в про-
Обработка пропущенных значений в больших наборах данных 235
мышленной среде. Возможности этого фреймворка по обработке данных
в памяти помогают существенно ускорить выполнение итеративных алго-
ритмов, характерных для задач машинного обучения, таких как подстановка
пропущенных значений.
В состав библиотеки MLlib входит множество различных стратегий для
замены пропусков в данных, включая как простые алгоритмы с использова-
нием среднего значения, медианы или моды, так и более сложные техники,
такие как метод k-ближайших соседей. При этом все использующиеся мето-
ды адаптированы для выполнения в распределенной среде, что обеспечивает
им масштабируемость при работе с большими данными.
Кроме того, способность фреймворка Spark обрабатывать как пакетные
данные, так и потоковые делает его универсальным и незаменимым для
большинства сценариев, связанных с подстановкой пропущенных значений.
Работаете ли вы с историческими данными или с потоками в реальном вре-
мени, Spark поможет вам выполнить замену пропусков с использованием
любой стратегии.
# !pip install pyspark
from pyspark.sql import SparkSession
from pyspark.ml.feature import Imputer
from pyspark.sql.functions import col, when
from pyspark.ml.feature import StringIndexer, OneHotEncoder
from pyspark.ml import Pipeline
# Инициализируем сессию Spark
spark = SparkSession.builder.appName("MissingDataImputation").getOrCreate()
# Создаем датафрейм Spark с пропущенными значениями
data=[
(25, None, 2, "Sales", "Bachelor"),
(None, 60000, 4, "Marketing", None),
(22, 52000, 1, "IT", "Master"),
(35, None, None, "HR", "PhD"),
(None, 58000, 3, "Finance", "Bachelor"),
(28, 55000, 2, None, "Master")
]
columns = ['Age', 'Salary', 'Experience', 'Department', 'Education']
df_spark = spark.createDataFrame(data, columns)
# Выводим исходный датафрейм
print("Исходный датафрейм:")
df_spark.show()
# Определяем подстановку для числовых столбцов
numeric_cols = ['Age', 'Salary', 'Experience']
imputer = Imputer(
inputCols=numeric_cols,
outputCols=["{}_imputed".format(c) for c in numeric_cols]
)
236 Сложные техники заполнения пропусков в данных
# Определяем подстановку для категориальных столбцов
categorical_cols = ['Department', 'Education']
# Функция для замены пропусков в категориальных столбцах на наиболее часто встречающиеся
значения
def categorical_imputer(df, col_name):
mode = df.groupBy(col_name).count().orderBy('count', ascending=False).first()[col_name]
return when(col(col_name).isNull(), mode).otherwise(col(col_name))
# Применяем подстановку для категориальных столбцов
for cat_col in categorical_cols:
df_spark = df_spark.withColumn(f"{cat_col}_imputed", categorical_imputer(df_spark,
cat_col))
# Создаем StringIndexer и OneHotEncoder для категориальных столбцов
indexers = [StringIndexer(inputCol=f"{c}_imputed", outputCol=f"{c}_index") for c in
categorical_cols]
encoders = [OneHotEncoder(inputCol=f"{c}_index", outputCol=f"{c}_vec") for c in
categorical_cols]
# Создаем конвейер
pipeline = Pipeline(stages=[imputer] + indexers + encoders)
# Запускаем конвейер
df_imputed = pipeline.fit(df_spark).transform(df_spark)
# Выбираем нужные столбцы
columns_to_select = [f"{c}_imputed" for c in numeric_cols] + [f"{c}_vec" for c in
categorical_cols]
df_final = df_imputed.select(columns_to_select)
# Выводим датафрейм с подстановками
print("\nДатафрейм с подстановками:")
df_final.show()
# Выводим общую статистику
print("\nОбщая статистика:")
df_final.describe().show()
# Очистка
spark.stop()
Вывод:
Исходный датафрейм:
+----+------+----------+----------+---------+
| Age|Salary|Experience|Department|Education|
+----+------+----------+----------+---------+
| 25| NULL|
2|
Sales| Bachelor|
|NULL| 60000|
4| Marketing|
NULL|
| 22| 52000|
1|
IT| Master|
| 35| NULL|
NULL|
HR|
PhD|
|NULL| 58000|
3| Finance| Bachelor|
| 28| 55000|
2|
NULL| Master|
+----+------+----------+----------+---------+
Обработка пропущенных значений в больших наборах данных 237
Датафрейм с подстановками:
+-----------+--------------+------------------+--------------+-------------+
|Age_imputed|Salary_imputed|Experience_imputed|Department_vec|Education_vec|
+-----------+--------------+------------------+--------------+-------------+
|
25|
56250|
2| (4,[0],[1.0])|(2,[0],[1.0])|
|
27|
60000|
4|
(4,[],[])|(2,[0],[1.0])|
|
22|
52000|
1| (4,[3],[1.0])|(2,[1],[1.0])|
|
35|
56250|
2| (4,[2],[1.0])| (2,[],[])|
|
27|
58000|
3| (4,[1],[1.0])|(2,[0],[1.0])|
|
28|
55000|
2| (4,[0],[1.0])|(2,[1],[1.0])|
+-----------+--------------+------------------+--------------+-------------+
Общая статистика:
+-------+------------------+-----------------+------------------+
|summary|
Age_imputed| Salary_imputed|Experience_imputed|
+-------+------------------+-----------------+------------------+
| count|
6|
6|
6|
| mean|27.333333333333332|
56250.0|2.3333333333333335|
| stddev| 4.320493798938573|2711.088342345192|1.0327955589886444|
| min|
22|
52000|
1|
| max|
35|
60000|
4|
+-------+------------------+-----------------+------------------+
Что здесь происходит?
1. Импорт необходимых библиотек:
• здесь мы загружаем нужные модули и функции для манипулирова-
ния данными, подстановки значений и конструирования признаков
из библиотеки PySpark, которую предварительно нужно установить.
2. Создание сессии Spark:
• инициализируем класс SparkSession, представляющий собой входную
точку в функционал библиотеки Spark.
3. Генерирование данных:
• создаем простой набор данных со смешанными типами (числовыми
и категориальными) и добавляем в них пропуски.
4. Вывод исходных данных:
• выводим на экран исходный датафрейм с пропущенными значе-
ниями.
5. Подстановка в числовых столбцах:
• используем класс Imputer из модуля pyspark.ml.feature для замены
значений в числовых столбцах;
• для столбцов с подстановкой определяем суффикс _imputed.
6. Подстановка в категориальных столбцах:
• здесь мы написали функцию categorical_imputer() для замены про-
пусков в категориальных столбцах на наиболее часто встречающееся
значение;
• применили эту функцию ко всем категориальным столбцам с по-
мощью метода withColumn().
238 Сложные техники заполнения пропусков в данных
7. Конструирование признаков для категориальных столбцов:
• класс StringIndexer из модуля pyspark.ml.feature используется для
преобразования текстовых столбцов в числовые индексы;
• класс OneHotEncoder из того же модуля применяется для создания
векторного представления категориальных переменных.
8. Создание конвейера:
• создаем экземпляр класса Pipeline из модуля pyspark.ml, объединяю-
щий в себе стадии подстановки в числовых столбцах, преобразова-
ния текстовых столбцов в числовые индексы и создания векторного
представления категориальных переменных;
• это гарантирует выполнение предварительных шагов по обработке
данных как для обучающей, так и для тестовой выборки.
9. Применение конвейера:
• применяем созданный конвейер к нашим данным и запускаем его,
чтобы выполнились все шаги предварительной подготовки данных.
10. Выбор нужных столбцов:
• выбираем числовые столбцы с подстановкой, а также векторизован-
ные категориальные столбцы.
11. Вывод результатов:
• выводим на экран выбранные столбцы для проверки подстановки.
12. Общая статистика:
• выводим суммарную статистику по итоговому датафрейму для по-
нимания того, как подстановка пропусков отразилась на распреде-
лении данных.
13. Очистка:
• останавливаем сессию Spark для освобождения занимаемых ресурсов.
На этом примере мы продемонстрировали обработку пропущенных зна-
чений при помощи библиотеки Spark. Мы осуществили подстановку число-
вых и категориальных переменных, а также выполнили необходимые шаги,
связанные с конструированием признаков, которые часто приходится пред-
принимать в реальных сценариях. Как видите, библиотека Spark предлага-
ет богатые возможности по предварительной обработке данных, включая
подстановку пропущенных значений, с использованием распределенных
вычислительных систем.
4.2.4. Ключевые выводы
Оптимизация с целью масштабирования
При работе с большими наборами данных простые методы подстановки про-
пусков на основе среднего значения или медианы зачастую предлагают наи-
лучший компромисс между вычислительной эффективностью и точностью
Обработка пропущенных значений в больших наборах данных 239
подстановки. Подобные методы просты в реализации и могут достаточно
быстро обрабатывать большие массивы данных без особых накладных вы-
числительных расходов. Однако не стоит забывать, что эти методы вслед-
ствие своей простоты могут не учитывать сложные зависимости между пере-
менными, присутствующие в данных.
Столбцы с большим количеством пропущенных значений
Столбцы, в которых присутствует большое количество пропущенных зна-
чений (к примеру, больше 50 %), традиционно доставляют немало головной
боли аналитикам. Решение о том, удалять такие столбцы или пытаться вы-
полнить замену пропусков, необходимо принимать с учетом значимости
столбцов для анализа. Если столбцы очень важно сохранить, можно попробо-
вать применить продвинутые техники подстановки на основе множествен-
ного заполнения или методов машинного обучения. В противном случае вы
можете просто избавиться от этих проблемных столбцов.
Распределенные вычисления
Использование внешних инструментов, таких как фреймворки Dask и Apache
Spark, позволяет эффективно работать с большими наборами данных при
помощи распределения нагрузки по нескольким ядрам процессора или ма-
шинам в кластере. Библиотека Dask дает возможность масштабировать уже
написанный код на Python для работы с большими данными, а библиотека
MLlib из состава фреймворка Spark позволяет реализовывать разные алго-
ритмы обработки данных для распределенной среды.
Процедура подстановки пропущенных значений при работе с большими
наборами данных требует соблюдения баланса между точностью произво-
димых замен и затрачиваемыми на это ресурсами. Тщательный выбор ис-
пользуемых методов для подстановки позволит обработать исходные данные
без больших затрат по времени.
Кроме того, при работе с большими данными очень важно производить
обработку в рамках конвейеров. Процедура подстановки пропущенных зна-
чений должна быть включена в состав вашего конвейера, чтобы она могла
быть применена к обучающей и тестовой выборкам.
Наконец, процесс замены пропусков нуждается в документации и тща-
тельной проверке. Сюда включается перечень значений и используемых
стратегий, а также допущения и предположения, лежащие в основе подста-
новки. Регулярная проверка влияния процедуры подстановки пропущен-
ных значений на дальнейший анализ позволит вам обеспечить надежность
и устойчивость результатов даже при работе с данными, содержащими боль-
шое количество пропусков.
240 Сложные техники заполнения пропусков в данных
4.3. Практические упражнения
Теперь вы можете попробовать применить полученные знания на практике
при решении упражнений, касающихся подстановки значений с использо-
ванием как простых методов, так и более сложных.
Упражнение 1. Подстановка с помощью метода
k-ближайших соседей
Есть набор данных о сотрудниках со столбцами Age, Salary и Experience, со-
держащими пропущенные значения:
import numpy as np
import pandas as pd
from sklearn.impute import KNNImputer
# Простые данные с пропущенными значениями
data = {'Age': [25, np.nan, 22, 35, np.nan],
'Salary': [50000, 60000, 52000, np.nan, 58000],
'Experience': [2, 4, 1, np.nan, 3]}
df = pd.DataFrame(data)
print(df, end='\n\n')
Вывод:
Age Salary Experience
0 25.0 50000.0
2.0
1 NaN 60000.0
4.0
2 22.0 52000.0
1.0
3 35.0
NaN
NaN
4 NaN 58000.0
3.0
Ваша задача состоит в том, чтобы заполнить пропуски с помощью метода
k-ближайших соседей.
Решение
# Инициализируем класс KNN с k=2
knn_imputer = KNNImputer(n_neighbors=2)
# Применяем метод KNN к нашему набору данных
df_imputed = pd.DataFrame(knn_imputer.fit_transform(df), columns=df.columns)
# Просматриваем итоговый датафрейм
print(df_imputed)
Вывод:
Age Salary Experience
0 25.0 50000.0
2.0
Практические упражнения 241
1 23.5 60000.0
4.0
2 22.0 52000.0
1.0
3 35.0 51000.0
1.5
4 23.5 58000.0
3.0
Упражнение 2. Подстановка с помощью метода MICE
В этом упражнении мы будем работать с тем же исходным набором данных:
from sklearn.experimental import enable_iterative_imputer # для активации IterativeImputer
from sklearn.impute import IterativeImputer
import pandas as pd
# Простые данные с пропущенными значениями
data = {'Age': [25, np.nan, 22, 35, np.nan],
'Salary': [50000, 60000, 52000, np.nan, 58000],
'Experience': [2, 4, 1, np.nan, 3]}
df = pd.DataFrame(data)
print(df, end='\n\n')
Вывод:
Age Salary Experience
0 25.0 50000.0
2.0
1 NaN 60000.0
4.0
2 22.0 52000.0
1.0
3 35.0
NaN
NaN
4 NaN 58000.0
3.0
Ваша задача состоит в том, чтобы выполнить подстановку пропущенных
значений посредством метода множественной подстановки с помощью цеп-
ных уравнений (MICE).
Решение
# Инициализируем класс MICE
mice_imputer = IterativeImputer()
# Применяем подстановку MICE
df_mice_imputed = pd.DataFrame(mice_imputer.fit_transform(df), columns=df.columns)
# Просматриваем итоговый датафрейм
print(df_mice_imputed)
Вывод:
Age
Salary Experience
0 25.000000 50000.000000 2.000000
1 34.353759 60000.000000 4.000000
2 22.000000 52000.000000 1.000000
3 35.000000 59790.347709 3.930193
4 32.040559 58000.000000 3 .000000
242 Сложные техники заполнения пропусков в данных
Упражнение 3. Удаление столбцов с большим количеством
пропущенных значений
В этом упражнении мы будем работать с набором данных, содержащим раз-
ное количество пропущенных значений:
import pandas as pd
import numpy as np
# Простые данные с пропущенными значениями
data = {'Age': [25, np.nan, 22, 35, np.nan],
'Salary': [50000, np.nan, 52000, np.nan, 58000],
'Experience': [2, 4, 1, np.nan, 3],
'JobTitle': [np.nan, np.nan, 'Engineer', np.nan, 'Manager']}
df = pd.DataFrame(data)
print(df, end='\n\n')
Вывод:
Age Salary Experience JobTitle
0 25.0 50000.0
2.0
NaN
1 NaN
NaN
4.0
NaN
2 22.0 52000.0
1.0 Engineer
3 35.0
NaN
NaN
NaN
4 NaN 58000.0
3.0 Manager
Ваша задача состоит в том, чтобы удалить из набора столбцы с количест-
вом пропущенных значений, превышающим 50 %.
Решение
# Рассчитываем долю пропущенных значений в каждом столбце
missing_proportion = df.isnull().mean()
# Удаляем столбцы с количеством пропущенных значений, превышающим 50 %
df_cleaned = df.drop(columns=missing_proportion[missing_proportion > 0.5].index)
# Просматриваем итоговый датафрейм
print(df_cleaned)
Вывод:
Age Salary Experience
0 25.0 50000.0
2.0
1 NaN
NaN
4.0
2 22.0 52000.0
1.0
3 35.0
NaN
NaN
4 NaN 58000.0
3.0
Практические упражнения 243
Упражнение 4. Простая замена пропущенных значений
для больших наборов данных
В этом упражнении мы будем работать с большим набором сгенерированных
данных, содержащим столбцы Age, Salary и Experience:
import pandas as pd
from sklearn.impute import SimpleImputer
# Большой набор данных с пропущенными значениями
data = {'Age': [25, None, 22, 35, None] * 200000,
'Salary': [50000, 60000, None, 80000, 58000] * 200000,
'Experience': [2, 4, 1, None, 3] * 200000}
df_large = pd.DataFrame(data)
print(df_large, end='\n\n')
Вывод:
Age Salary Experience
0
25.0 50000.0
2.0
1
NaN 60000.0
4.0
2
22.0
NaN
1.0
3
35.0 80000 .0
NaN
4
NaN 58000.0
3.0
...
...
...
...
999995 25.0 50000.0
2.0
999996 NaN 60000.0
4.0
999997 22.0
NaN
1.0
999998 35.0 80000 .0
NaN
999999 NaN 58000.0
3.0
[1000000 rows x 3 columns]
Ваша задача состоит в том, чтобы с помощью класса SimpleImputer запол-
нить пропуски в столбцах с использованием средних значений.
Решение
# Используем класс SimpleImputer для замены пропусков в числовых столбцах
simple_imputer = SimpleImputer(strategy='mean')
df_large_imputed = pd.DataFrame(simple_imputer.fit_transform(df_large), columns=df_large.
columns)
# Просматриваем первые несколько строк итогового датафрейма
print(df_large_imputed.head())
Вывод:
Age Salary Experience
0 25.000000 50000.0
2.0
1 27.333333 60000.0
4.0
244 Сложные техники заполнения пропусков в данных
2 22.000000 62000.0
1.0
3 35.000000 80000 .0
2.5
4 27.333333 58000.0
3.0
Упражнение 5. Распределенная замена пропущенных
значений с помощью Dask
У вас есть большой набор данных, в котором необходимо выполнить под-
становку пропущенных значений:
import dask.dataframe as dd
from sklearn.impute import SimpleImputer
import pandas as pd
# Большой набор данных с пропущенными значениями
data = {'Age': [25, None, 22, 35, None] * 200000,
'Salary': [50000, 60000, None, 80000, 58000] * 200000,
'Experience': [2, 4, 1, None, 3] * 200000}
df_large = pd.DataFrame(data)
print(df_large, end='\n\n')
Вывод:
Age Salary Experience
0
25.0 50000.0
2.0
1
NaN 60000.0
4.0
2
22.0
NaN
1.0
3
35.0 80000 .0
NaN
4
NaN 58000.0
3.0
...
...
...
...
999995 25.0 50000.0
2.0
999996 NaN 60000.0
4.0
999997 22.0
NaN
1.0
999998 35.0 80000 .0
NaN
999999 NaN 58000.0
3.0
[1000000 rows x 3 columns]
Ваша задача состоит в том, чтобы с помощью класса SimpleImputer и фрейм-
ворка Dask заполнить пропуски в столбцах с использованием средних зна-
чений.
Решение
# Преобразовываем датафрейм Pandas в датафрейм Dask
df_dask = dd.from_pandas(df_large, npartitions=10)
# Задаем стратегию подстановки
simple_imputer = SimpleImputer(strategy='mean')
# Применяем подстановку к датафрейму Dask
df_dask_imputed = df_dask.map_partitions(lambda df: pd.DataFrame(simple_imputer.fit_
transform(df), columns=df.columns))
Возможные проблемы 245
# Вычисляем результат
df_dask_imputed = df_dask_imputed.compute()
# Просматриваем первые несколько строк итогового датафрейма
print(df_dask_imputed.head())
Вывод:
Age Salary Experience
0 25.000000 50000.0
2.0
1 27.333333 60000.0
4.0
2 22.000000 62000.0
1.0
3 35.000000 80000 .0
2.5
4 27.333333 58000.0
3.0
4.4. Возможные проблемы
Замена пропущенных значений – очень важный этап любого процесса пред-
варительной подготовки данных, но если на этом шаге сделать что-то не так,
это может негативно сказаться на качестве вашей будущей модели. В этом
разделе мы поговорим о возможных проблемах, которые могут ожидать вас
при подстановке пропусков в данных.
4.4.1. Погрешность модели при неправильной
подстановке пропусков
При выполнении замены пропущенных значений в наборе данных всегда
есть риск нарушить устойчивость модели, особенно если неправильно вы-
брать стратегию подстановки. К примеру, замена пропусков на среднее
значение или медиану может привести к искажению распределения дан-
ных, особенно если пропущенные значения распределены неслучайным
образом.
Что может пойти не так:
замена пропусков на среднее значение или медиану может выровнять
распределение и скрыть важную изменчивость в данных, приводящую
к ухудшению качества модели;
замена пропусков в категориальных переменных без учета их зависи-
мостей с другими переменными в наборе может привести к погреш-
ности модели и ухудшению качества ее предсказаний.
Решение:
воспользуйтесь более сложными алгоритмами замены пропусков вроде
методов KNN или MICE, способными отслеживать зависимости между
переменными;
246 Сложные техники заполнения пропусков в данных
тщательно проанализируйте шаблоны пропущенных значений, перед
тем как выбирать стратегию подстановки, – это позволит сделать пра-
вильный выбор с учетом распределения данных.
4.4.2. Переобучение модели вследствие замены
пропусков в тестовой выборке
Одной из самых распространенных ошибок является одновременное вы-
полнение подстановки пропущенных значений в обучающей и тестовой вы-
борках. Если сделать замену пропусков во всем наборе данных до его раз-
деления на обучающую и тестовую выборки, ваша модель может обучиться
на тестовом наборе, что приведет к ее переобучению.
Что может пойти не так:
использование тестовых данных в процессе обучения модели называ-
ется утечкой информации и обычно приводит к излишне оптимистич-
ной оценке качества модели;
ваша модель может утратить обобщающую способность на данных, не
использовавшихся в процессе обучения.
Решение:
всегда разделяйте набор данных на обучающую и тестовую выборки
перед выполнением процедуры подстановки пропущенных значений.
Применяйте подстановку к обучающему набору, после чего используй-
те полученные шаблоны при замене пропусков в тестовой выборке.
4.4.3. Удаление слишком большого количества
данных
При работе с данными, содержащими большое количество пропусков, бывает
велик соблазн просто взять и удалить строки или столбцы с пропущенны-
ми значениями. Однако это может привести к потере важной информации,
особенно если пропущенные значения распределены неслучайным образом.
Что может пойти не так:
исключение строк и столбцов с пропущенными значениями может
привести к погрешности модели, если эти данные несут ценную ин-
формацию, например если пропуски в основном присутствуют в опре-
деленных группах или появляются при определенных условиях;
при удалении большого количества данных ваш набор может оказаться
слишком маленьким для построения надежной модели.
Решение:
перед удалением строк и столбцов тщательно проанализируйте шабло-
ны, характерные для пропущенных значений. Если вы считаете, что
Возможные проблемы 247
пропуски в данных носят случайный характер (тип MCAR), то в боль-
шинстве случаев можете безболезненно расстаться с частью данных;
для ценных столбцов с большим количеством пропусков можно при-
менить сложные методы подстановки вроде MICE или выработать свои
алгоритмы на основе знаний о предметной области.
4.4.4. Неправильное интерпретирование данных
о временных рядах
При работе с большими данными, содержащими переменные на основе
временных рядов, неправильная подстановка пропусков может привести
к нарушению временных характеристик набора. К примеру, если заполнить
пропуски в данных будущих периодов на основе прошлых лет или наоборот,
качество предсказаний модели существенно снизится.
Что может пойти не так:
подстановка пропусков в переменных без учета их временных характе-
ристик может привести к тому, что модель будет предсказывать пред-
шествующие значения на основе будущих периодов;
использование при подстановке пропусков во временных рядах сред-
них значений или метода прямого заполнения может привести к по-
явлению нереалистичных шаблонов с нарушением хода времени.
Решение:
при подстановке пропущенных значений во временных рядах ис-
пользуйте технику интерполяции на основе времени или скользящие
средние;
при подстановке пропусков в наблюдениях, относящихся к будущим
периодам, используйте только данные из предыдущих периодов, что-
бы избежать утечки информации.
4.4.5. Вычислительная сложность при работе
с большими наборами данных
При работе с очень большими данными применение сложных алгоритмов
подстановки пропусков вроде KNN или MICE может оказаться нецелесо-
образным с точки зрения использования ресурсов и времени выполнения.
Это может сказаться на эффективности работы, особенно если вам необхо-
димо проходить итерациями по нескольким моделям.
Что может пойти не так:
подстановка методом KNN плохо масштабируется применительно
к объемным наборам данных из-за необходимости вычислять расстоя-
ния между всеми парами наблюдений в наборе данных. Это делает
248 Сложные техники заполнения пропусков в данных
данный метод неприменимым при наличии очень большого количест-
ва наблюдений;
метод MICE может оказаться очень медленным при наличии большого
количества переменных с пропущенными значениями, поскольку он
требует итеративного моделирования для каждого признака.
Решение:
при работе с большими данными воспользуйтесь более простыми ме-
тодами подстановки пропусков для большинства столбцов, оставив
сложные алгоритмы для ключевых переменных, требующих особого
подхода;
рассмотрите вариант использования фреймворков распределенной
обработки данных, таких как Dask или Apache Spark, для реализации
параллельных вычислений.
4.4.6. Сложности с нахождением шаблонов
в пропущенных значениях
Не все пропущенные значения в данных имеют случайный характер. При
наличии четких шаблонов появления пропусков использование простых ме-
тодов подстановки может привести к погрешности модели.
Что может пойти не так:
игнорирование существующих шаблонов в пропущенных значениях
может привести к недооценке структуры исходных данных. К примеру,
если доход не указан только для людей с большим достатком, замена
пропусков средними значениями приведет к явному искажению дан-
ных;
если пропуски тесно связаны с целевой переменной, недооценка шаб-
лонов их появления может привести к ухудшению итоговой модели.
Решение:
перед выполнением процедуры замены пропусков тщательно проана-
лизируйте природу их появления (типы MAR, MNAR, MCAR);
для пропусков с типами MAR и MNAR рассмотрите вариант использо-
вания метода множественного восстановления пропущенных данных
или собственных алгоритмов на основе знаний о предметной области.
Заключение
Качество анализа данных и моделей машинного обучения во многом зависит
от одного из самых важных шагов при обработке исходных данных, коим
является замена пропущенных значений. В наборах данных, с которыми мы
сталкиваемся в реальности, пропуски могут появляться по самым разным
Заключение 249
причинам, начиная от человеческого фактора и заканчивая сбоем датчиков
и сенсоров. Методы, используемые при подстановке пропущенных значений,
критическим образом влияют на качество итоговой модели и ее обобщаю-
щую способность на данных, не участвовавших в процессе обучения.
В этой главе мы рассмотрели как самые простые способы подстановки
с использованием среднего значения или медианы, так и более сложные,
такие как метод k-ближайших соседей и метод множественной подстанов-
ки с помощью цепных уравнений (MICE). Также мы подробно рассмотрели
способ, подразумевающий использование полноценных моделей машинного
обучения для предсказания пропущенных значений в наборе данных.
Кроме того, мы увидели примеры использования фреймворков для рас-
пределенных вычислений, таких как Dask и Apache Spark, позволяющих вы-
полнить горизонтальное масштабирование процедуры подстановки пропу-
щенных значений с задействованием множества ядер процессора и машин
в кластере.
В заключение мы рассмотрели возможные проблемы, которые могут вас
подстерегать в процессе заполнения пропусков в данных, и предложили ва-
рианты их решения.
В следующей главе книги мы подробно поговорим о масштабировании
признаков и их преобразовании.
Глава 5
Преобразование
и масштабирование
признаков
Преобразование признаков и их масштабирование – шаги, не уступающие
по важности процедуре подстановки пропущенных значений в исходных
данных. Эти действия помогают привести значения переменных в приемле-
мый вид для эффективного использования большинства алгоритмов. Кроме
того, они напрямую влияют на то, как мы будем интерпретировать резуль-
таты, полученные из моделей машинного обучения. Для большинства при-
меняемых алгоритмов масштаб и распределение входных данных оказывают
существенное влияние на качество их предсказаний. Без должного масшта-
бирования признаков какие-то переменные будут доминировать в процессе
обучения лишь по причине того, что для значений в них характерны большие
диапазоны.
В этой главе мы обсудим разные способы преобразования признаков,
включая масштабирование, нормализацию и стандартизацию, и поговорим
об их важности в контексте применения моделей машинного обучения.
5.1. Масштабирование и нормализация:
оптимальное применение
Масштабирование (scaling) и нормализация (normalization) представляют
собой фундаментальные техники, использующиеся в процессе подготовки
данных, которые позволяют привести признаки к сопоставимому масштабу,
что ведет к их адекватному восприятию моделями машинного обучения. Эти
техники играют ключевую роль в оптимизации качества моделей и предот-
вращении погрешностей в их результатах.
Масштабирование и нормализация: оптимальное применение 251
Масштабирование используется для приведения значений признаков
к одному фиксированному диапазону, обычно от 0 до 1. Эта процедура чрез-
вычайно важна для алгоритмов, чувствительных к величинам входных зна-
чений, таких как метод k-ближайших соседей или метод опорных векторов.
Выполняя масштабирование, мы гарантируем, что все признаки будут вно-
сить одинаковый вклад в процесс принятия решений.
Что касается нормализации, то она подразумевает преобразование ис-
ходных значений таким образом, чтобы их среднее значение стало равным
нулю, а стандартное отклонение – единице. Эта техника бывает особенно
полезна при использовании алгоритмов, предполагающих нормальное рас-
пределение данных, таких как линейная регрессия и анализ главных ком-
понент. Нормализация признаков позволяет стабилизировать сходимость
весов в нейронных сетях и улучшить качество моделей, полагающихся на
статистические свойства данных.
Эффективная реализация этих техник требует глубокого понимания на-
бора данных и выбранного алгоритма машинного обучения. В этой главе мы
узнаем, где и когда стоит использовать масштабирование и нормализацию
признаков, а также реализуем эти методы на практике с помощью библио-
теки Scikit-learn.
5.1.1. Почему так важны масштабирование
и нормализация
Многие алгоритмы машинного обучения чрезвычайно чувствительны к мас-
штабу переменных, поступающих на вход. Особенно это касается методов,
в той или иной степени полагающихся на расстояния между наблюдениями.
Представьте набор данных с двумя переменными, соответствующими го-
довому доходу клиента и его возрасту. В первом столбце диапазон значений
составляет от 10 000 до 100 000, а во втором – от 20 до 80. В результате ал-
горитм машинного обучения может решить, что первый признак обладает
большим влиянием на целевую переменную по сравнению со вторым, что
приведет к ошибкам в прогнозах модели.
Но масштабирование признаков оказывает влияние не только на алгорит-
мы, основывающиеся на расстояниях между наблюдениями. Оптимизаци-
онные алгоритмы, такие как градиентный спуск (gradient descent), лежащие
в основе обучения моделей линейной регрессии и нейронных сетей, также
сходятся более быстро и эффективно в присутствии аккуратно масштабиро-
ванных переменных.
В отсутствие масштабирования признаки с большими диапазонами могут
оказывать доминирующее влияние на оптимизационную процедуру, что
может замедлять ее сходимость и приводить к поиску неоптимальных реше-
ний. Причина в том, что алгоритму может потребоваться больше времени на
поиск оптимальных весов для переменных с большими диапазонами, даже
если они не так важны для предсказаний.
252 Преобразование и масштабирование признаков
Проблема масштабирования становится еще более отчетливой при на-
личии большого количества признаков в наборе данных. В таких случаях
накопительный эффект от разницы в диапазонах значений в разных пере-
менных может серьезно сказаться на качестве модели и повысить ее склон-
ность к переобучению.
Также стоит отметить, что некоторые алгоритмы, такие как деревья реше-
ний или случайный лес, являются не столь чувствительными к масштабиро-
ванию. Но даже для них процедура масштабирования признаков может ока-
заться полезной с точки зрения интерпретируемости результатов и анализа
значимости переменных.
5.1.2. Масштабирование и нормализация:
в чем разница?
Несмотря на то что понятия масштабирования и нормализации очень часто
путают и смешивают, операции, лежащие в их основе, служат разным целям.
Масштабирование
Как мы уже говорили, в процессе масштабирования все значения признака
приводятся к одному диапазону – чаще всего от 0 до 1. Давайте перечислим
цели, которых мы пытаемся достичь при использовании масштабирования.
1. Пропорциональный вклад переменных. Масштабируя признаки
к одному диапазону, мы тем самым гарантируем, что все они будут
вносить одинаковый вклад в результаты модели. В противном случае
признаки с большим диапазоном значений будут превалировать в про-
цессе обучения модели.
2. Совместимость алгоритмов. Масштабирование особенно полезно
выполнять при использовании алгоритмов, чувствительных к диапа-
зонам значений, которые мы уже перечисляли выше.
3. Скорость сходимости алгоритмов. В случае использования алгорит-
мов на основе градиента, таких как нейронные сети, масштабирование
признаков способно существенно ускорить процесс их сходимости во
время обучения модели.
4. Интерпретируемость модели. Масштабированные признаки гораздо
легче сравнивать и интерпретировать, поскольку их значения распо-
лагаются в одном диапазоне. Это может быть особенно полезно при
анализе значимости признаков или визуализации данных.
5. Стабильность числовых расчетов. Некоторые алгоритмы склонны
к нестабильности числовых вычислений или переполнению допусти-
мых диапазонов при работе с переменными разных масштабов. Мас-
штабирование позволяет нивелировать подобные проблемы путем
приведения всех переменных к одному диапазону.
Масштабирование и нормализация: оптимальное применение 253
Нормализация
Нормализация в контексте предварительной обработки признаков является
мощным инструментом, позволяющим преобразовать значения таким обра-
зом, чтобы их среднее равнялось нулю, а стандартное отклонение – единице.
Этот процесс, также известный как стандартизация или z-нормализация,
особенно важен при использовании алгоритмов, предполагающих нормаль-
ное распределение данных.
Основной целью нормализации является приведение всех переменных
к общему масштабу без искажения разниц в диапазонах значений. Это осо-
бенно применимо к таким методам, как линейная регрессия, логистическая
регрессия и анализ главных компонент, которые в большой степени полага-
ются на статистические свойства данных.
Одно из преимуществ нормализации заключается в способности ускорить
сходимость подбора весов при обучении нейронных сетей. Также нормали-
зация помогает повысить точность прогнозов алгоритмов, опирающихся на
расстояния между наблюдениями, таких как метод k-ближайших соседей.
Кроме того, она полезна в случаях, когда переменные в наборе данных вы-
ражены в разных единицах измерения.
В то же время нормализация может быть не лучшим выбором при наличии
в наборе данных сильных выбросов. В таких случаях больше может подой-
ти так называемое робастное масштабирование. Как всегда, выбор техники
всегда зависит от конкретной ситуации, набора данных и используемых ал-
горитмов.
5.1.3. Минимаксное масштабирование
(нормализация)
Минимаксное масштабирование (min-max scaling), также известное как нор-
мализация, является одной из основных техник масштабирования призна-
ков с приведением значений к фиксированному диапазону, обычно от 0 до 1.
Этот метод хорошо подходит для алгоритмов, чувствительных к масштабу
и распределению входных значений.
Кроме того, минимаксное масштабирование сохраняет нулевые значения
и исходное распределение данных, что может быть полезно при работе с раз-
реженными матрицами или в ситуациях, когда важна относительная разница
между значениями. Такими требованиями и характеристиками обладают,
например, рекомендательные системы или задачи, связанные с обработкой
изображений.
В то же время главным недостатком такого вида масштабирования явля-
ется его чувствительность к выбросам. Экстремальные значения в наборе
данных могут существенно сжимать полезный диапазон для основной части
значений, что снижает эффективность этого приема. В таких случаях лучше
прибегнуть к помощи робастного масштабирования или винсоризации.
254 Преобразование и масштабирование признаков
Формула для применения минимаксного масштабирования:
где X – исходный признак, X
min
– минимальное значение признака, X
max
– мак-
симальное значение признака.
Пример использования такого типа масштабирования:
import pandas as pd
import numpy as np
from sklearn.preprocessing import MinMaxScaler
import matplotlib.pyplot as plt
# Простой набор данных
np.random.seed(42)
data={
'Age': np.random.randint(18, 80, 100),
'Income': np.random.randint(20000, 150000, 100),
'Years_Experience': np.random.randint(0, 40, 100)
}
# Создаем датафрейм
df = pd.DataFrame(data)
# Первые несколько строк и статистика
print("Исходные данные:")
print(df.head())
print("\nСтатистика исходных данных:")
print(df.describe())
# Инициализируем минимаксное масштабирование
scaler = MinMaxScaler()
# Применяем масштабирование к датафрейму
df_scaled = pd.DataFrame(scaler.fit_transform(df), columns=df.columns)
# Первые несколько строк и статистика
print("\nМасштабированные данные:")
print(df_scaled.head())
print("\nСтатистика масштабированных данных:")
print(df_scaled.describe())
# Визуализируем распределение данных до и после масштабирования
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5))
# До масштабирования
df.boxplot(ax=ax1)
ax1.set_title('До минимаксного масштабирования')
ax1.set_ylim([0, 160000])
Масштабирование и нормализация: оптимальное применение 255
# После масштабирования
df_scaled.boxplot(ax=ax2)
ax2.set_title('После минимаксного масштабирования')
ax2.set_ylim([0, 1])
plt.tight_layout()
plt.show()
Вывод:
Исходные данные:
Age Income Years_Experience
0 56 57065
31
1 69 112093
3
2 46 119299
29
3 32 149293
36
4 60 52606
22
...
Статистика исходных данных:
Age
Income Years_Experience
count 100.000000
100.000000
100.000000
mean
50.270000 87499.460000
20.320000
std
19.176403 40488.269673
12.310692
min
19.000000 20206.000000
0.000000
25%
34.750000 53020.750000
9.500000
50%
51.500000 89077.500000
22.500000
75%
68.000000 120596.250000
31.000000
max
79.000000 149312.000000
39.000000
Масштабированные данные:
Age Income Years_Experience
0 0.616667 0.285494
0.794872
1 0.833333 0 .711718
0.076923
2 0.450000 0.767532
0.743590
3 0.216667 0.999853
0.923077
4 0.683333 0.250957
0.564103
...
Статистика масштабированных данных:
Age
Income Years_Experience
count 100.000000 100.000000
100.000000
mean
0.521167 0.521226
0.521026
std
0.319607 0.313605
0.315659
min
0.000000 0 .000000
0.000000
25%
0.262500 0.254169
0.243590
50%
0.541667 0.533449
0.576923
75%
0.816667 0.777580
0.794872
max
1.000000 1.000000
1.000000
256 Преобразование и масштабирование признаков
Рис. 5.1 Распределение данных до и после минимаксного масштабирования
Здесь мы создали простой набор данных с тремя числовыми переменны-
ми, обладающими разными диапазонами. После этого создали объект класса
MinMaxScaler и применили его ко всему датафрейму сразу, что более эффек-
тивно в сравнении с применением масштабирования к разным столбцам по
отдельности. В завершение мы вывели первые несколько строк исходного
и преобразованного наборов данных, а также отобразили на графике, по-
казанном на рис. 5 .1, распределение исходных и измененных переменных
при помощи диаграммы размаха. Как видите, на левом графике переменные
Age и Years_Experience практически незаметны по причине того, что значения
в них располагаются близко к нулю. В то же время на правом графике, после
выполнения масштабирования, все три переменные отчетливо видны и об-
ладают сопоставимым распределением.
5.1.4. Стандартизация (z-нормализация)
Стандартизация (standardization), также известная как z-нормализация (z-
score normalization), часто применяется при подготовке данных для моделей
машинного обучения, особенно при использовании алгоритмов наподобие
линейной или логистической регрессии, а также анализа главных компонент,
для которых статистические свойства данных играют важную роль.
Процесс стандартизации подразумевает приведение данных к нулевому
среднему и единичному стандартному отклонению, что может быть осо-
бенно ценно при работе с переменными в разных единицах измерения, по-
скольку позволяет привести их к сопоставимым диапазонам без искажения
распределения значений.
В то же время необходимо помнить, что стандартизация может быть
полезна далеко не везде. К примеру, при работе с нейронными сетями,
использующими в качестве функции активации сигмоиду, минимаксное
масштабирование с приведением к диапазону от 0 до 1 может быть более
уместным.
Масштабирование и нормализация: оптимальное применение 257
Формула для применения стандартизации:
где X – исходный признак, μ – среднее значение признака, а σ – его стандарт-
ное отклонение.
Пример использования стандартизации:
import pandas as pd
import numpy as np
from sklearn.preprocessing import StandardScaler
import matplotlib.pyplot as plt
# Простой набор данных
np.random.seed(42)
data={
'Age': np.random.randint(18, 80, 100),
'Income': np.random.randint(20000, 150000, 100),
'Years_Experience': np.random.randint(0, 40, 100)
}
# Создаем датафрейм
df = pd.DataFrame(data)
# Первые несколько строк и статистика
print("Исходные данные:")
print(df.head())
print("\nСтатистика исходных данных:")
print(df.describe())
# Инициализируем стандартизацию
scaler = StandardScaler()
# Применяем стандартизацию к датафрейму
df_standardized = pd.DataFrame(scaler.fit_transform(df), columns=df.columns)
# Первые несколько строк и статистика
print("\nСтандартизированные данные:")
print(df_standardized.head())
print("\nСтатистика стандартизированных данных:")
print(df_standardized.describe())
# Визуализируем распределение данных до и после стандартизации
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5))
# До стандартизации
df.boxplot(ax=ax1)
ax1.set_title('До стандартизации')
# После стандартизации
df_standardized.boxplot(ax=ax2)
258 Преобразование и масштабирование признаков
ax2.set_title('После стандартизации')
plt.tight_layout()
plt.show()
Вывод:
Исходные данные:
Age Income Years_Experience
0 56 57065
31
1 69 112093
3
2 46 119299
29
3 32 149293
36
4 60 52606
22
...
Статистика исходных данных:
Age
Income Years_Experience
count 100.000000
100.000000
100.000000
mean
50.270000 87499.460000
20.320000
std
19.176403 40488.269673
12.310692
min
19.000000 20206.000000
0.000000
25%
34.750000 53020.750000
9.500000
50%
51.500000 89077.500000
22.500000
75%
68.000000 120596.250000
31.000000
max
79.000000 149312.000000
39.000000
Стандартизированные данные:
Age Income Years_Experience
0 0.300310 -0 .755473
0.871909
1 0.981642 0.610484
- 1 .413995
2 -0 .223791 0.789358
0.708630
3 -0 .957533 1.533897
1.280106
4 0.509951 -0 .866158
0.137154
...
Статистика стандартизированных данных:
Age
Income Years_Experience
count 1.000000e+02 1.000000e+02
100.000000
mean -1 .511291e-16 -1 .776357e-16
0.000000
std 1.005038e+00 1.005038e+00
1.005038
min -1 .638865e+00 -1 .670421e+00
- 1 .658913
25% -8 .134052e-01 -8 .558629e-01
- 0 .883339
50% 6.446446e-02 3.917159e-02
0.177974
75% 9.292316e-01 8.215596e-01
0.871909
max
1.505743e+00 1.534369e+00
1.525024
Здесь мы вместо класса MinMaxScaler воспользовались классом Standard-
Scaler. Все остальное, включая диапазоны и распределения преобразованных
переменных, видно на рис. 5 .2 .
Масштабирование и нормализация: оптимальное применение 259
Рис. 5 .2 Распределение данных до и после стандартизации
5.1.5. Когда использовать минимаксное
масштабирование, а когда стандартизацию
Выбор между минимаксным способом масштабирования и стандартизацией
зависит от множества факторов, включая используемый алгоритм машинно-
го обучения и характеристики набора данных. Давайте узнаем, когда какому
методу стоит отдавать предпочтение.
Предпочтительные условия для использования минимаксного масштаби-
рования:
наличие граничных значений, когда вам необходимо ограничить ваш
диапазон значений снизу и сверху, например значениями 0 и 1. Это
бывает полезно при использовании алгоритмов, требующих, чтобы
входные переменные находились в определенном диапазоне, что от-
носится, к примеру, к нейронным сетям с сигмоидой, используемой
в качестве функции активации;
применение моделей, полагающихся на количественные характерис-
тики значений, таких как метод k-ближайших соседей или нейронные
сети. При использовании таких алгоритмов приведение переменных
к одному диапазону позволит избежать доминирования одних при-
знаков над другими;
отсутствие нормального распределения исходных данных. В отличие
от стандартизации, минимаксное масштабирование не строит пред-
положений относительно распределения значений, вследствие чего
подходит для разных типов данных;
обработка звука и изображений. Этот вид масштабирования особенно
хорошо подходит для работы с интенсивностью пикселей на изобра-
жении и амплитудами звуковых сигналов. В этих областях приведение
переменных к фиксированному диапазону (например, от 0 до 1 для
нормализованных значений пикселей) зачастую необходимо для эф-
фективной обработки данных и их интерпретации;
260 Преобразование и масштабирование признаков
сохранение нулевых значений. При использовании минимаксного мас-
штабирования в разреженных наборах данных сохраняются нулевые
значения, что может быть очень полезно в таких областях, как реко-
мендательные системы или анализ текста, где ноль часто соответствует
отсутствию признака;
сохранение зависимостей между исходными значениями, что может
быть полезно в сценариях, в которых относительная разница между
значениями важнее их абсолютных величин. При этом стоит помнить
о чувствительности этого вида масштабирования к выбросам и в слу-
чае их наличия рассмотреть возможность использования альтернатив-
ных методов, таких как робастное масштабирование.
Напротив, стандартизацию стоит применять при наличии следующих ус-
ловий:
используемый вами алгоритм полагается или лучше работает в условиях
нормально распределенных данных. Это, в частности, касается линей-
ных моделей, метода опорных векторов и анализа главных компонент;
ваши переменные характеризуются кардинально разными масштаба-
ми или единицами измерения;
вы хотите сохранить информацию о выбросах. В отличие от мини-
максного масштабирования, стандартизация не сжимает диапазоны
значений, что позволяет сохранить информацию о выбросах в преоб-
разованных данных;
вы работаете с переменными, масштаб которых несет в себе важную
информацию. Стандартизация позволяет сохранить форму исходного
распределения и относительную разницу между наблюдениями;
в вашей модели используются метрики на основе расстояния. Многие
алгоритмы, включая кластеризацию методом k-средних и классифи-
кацию методом k-ближайших соседей, полагаются в своих вычисле-
ниях на расстояния между наблюдениями. Стандартизация позволяет
гарантировать, что все переменные будут вносить одинаковый вклад
в эти расстояния;
вы работаете с алгоритмами на основе градиентного спуска. Стандар-
тизация обеспечивает более быструю сходимость таких алгоритмов за
счет создания более сферических распределений данных.
На практике часто бывает полезно проверить оба варианта масштабиро-
вания данных и сравнить их влияние на качество модели.
5.1.6. Робастное масштабирование, устойчивое
к выбросам
Хотя минимаксное масштабирование и стандартизация могут быть с поль-
зой применены к большей части моделей машинного обучения, они также
могут быть слишком чувствительны к выбросам. Если в вашем наборе при-
Масштабирование и нормализация: оптимальное применение 261
сутствуют существенные аномалии, вашим выбором может стать робастное
масштабирование (robust scaling). Этот метод масштабирует значения на
основе межквартильного размаха (interquartile range – IQR), что делает его
менее чувствительным к выбросам.
При вычислении робастного масштаба из значения вычитается медиана по
столбцу и результат делится на межквартильный размах. Эффект от исполь-
зования этого метода основывается на том, что медиана и межквартильный
размах менее подвержены влиянию экстремальных значений в сравнении
со средним значением и стандартным отклонением, которые используются
при расчете стандартизации.
При работе с реальными наборами данных, зачастую содержащими шум
и аномалии, робастное масштабирование может оказаться чрезвычайно полез-
ным. Особенно часто его используют в сфере финансов, где выбросы способны
существенно исказить распределение данных, или при анализе показателей
датчиков, где к выбросам могут приводить ошибки в измерениях. Используя
робастное масштабирование, вы можете быть уверены, что качество модели не
будет чрезмерно подвержено влиянию экстремальных значений, что приведет
к получению более надежных результатов при прогнозировании.
В то же время стоит отметить, что робастное масштабирование может быть
полезным далеко не всегда. К примеру, если выбросы в вашем наборе данных
обладают конкретным смыслом и вы хотели бы сохранить их влияние на
модель, или если ваши данные соответствуют нормальному распределению
без сильных выбросов, другие способы масштабирования могут оказаться
более эффективными.
Пример использования робастного масштабирования:
import pandas as pd
import numpy as np
from sklearn.preprocessing import RobustScaler
import matplotlib.pyplot as plt
# Простой набор данных с выбросами
np.random.seed(42)
data={
'Age': np.concatenate([np.random.normal(40, 10, 50), [200]]), # наличие выброса
в переменной с возрастом
'Income': np.concatenate([np.random.normal(60000, 15000, 50), [500000]]) # наличие
выброса в переменной с доходами
}
# Создаем датафрейм
df = pd.DataFrame(data)
# Статистика исходных данных
print("Статистика исходных данных:")
print(df.describe())
# Инициализируем робастное масштабирование
scaler = RobustScaler()
# Применяем робастное масштабирование к датафрейму
262 Преобразование и масштабирование признаков
df_robust_scaled = pd.DataFrame(scaler.fit_transform(df), columns=df.columns)
# Статистика масштабированных данных
print("\nСтатистика масштабированных данных:")
print(df_robust_scaled.describe())
# Визуализируем распределение данных до и после робастного масштабирования
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5))
# До робастного масштабирования
df.boxplot(ax=ax1)
ax1.set_title('До робастного масштабирования')
# После робастного масштабирования
df_robust_scaled.boxplot(ax=ax2)
ax2.set_title('После робастного масштабирования')
plt.tight_layout()
plt.show()
# Сравниваем влияние выбросов для разных типов масштабирования
from sklearn.preprocessing import StandardScaler, MinMaxScaler
# Применяем разное масштабирование
standard_scaler = StandardScaler()
minmax_scaler = MinMaxScaler()
df_standard = pd.DataFrame(standard_scaler.fit_transform(df), columns=df.columns)
df_minmax = pd.DataFrame(minmax_scaler.fit_transform(df), columns=df.columns)
# Сравнение
fig, axes = plt.subplots(2, 2, figsize=(15, 10))
fig.suptitle('Сравнение влияния выбросов для разных типов масштабирования')
df.boxplot(ax=axes[0, 0])
axes[0, 0].set_title('Исходные данные')
df_standard.boxplot(ax=axes[0, 1])
axes[0, 1].set_title('Стандартное масштабирование')
df_minmax.boxplot(ax=axes[1, 0])
axes[1, 0].set_title('Минимаксное масштабирование')
df_robust_scaled.boxplot(ax=axes[1, 1])
axes[1, 1].set_title('Робастное масштабирование')
plt.tight_layout()
plt.show()
Вывод:
Статистика исходных данных:
Age
Income
count 51.000000
51.000000
mean
40.926726 68888.934370
std
24.528313 62928.830126
min
20.403299 20703.823439
25%
31.860659 52643.015389
50%
37.658630 61305.706024
75%
43.596582 70681.515834
max
200.000000 500000.000000
Масштабирование и нормализация: оптимальное применение 263
Статистика масштабированных данных:
Age
Income
count 51.000000 51.000000
mean
0.278469 0.420391
std
2.090020 3.488584
min
- 1 .470300 -2 .250846
25% -0 .494036 -0 .480233
50%
0.000000 0 .000000
75%
0.505964 0.519767
max
13.832859 24.319887
Рис. 5.3 Распределение данных до и после робастного масштабирования
Рис. 5.4 Сравнение влияния выбросов для разных типов масштабирования
264 Преобразование и масштабирование признаков
Здесь мы воспользовались классом RobustScaler для выполнения робастно-
го масштабирования, после чего сравнили результаты применения всех трех
методов масштабирования данных, что показано на рис. 5 .4 .
5.1.7. Винсоризация
Винсоризация (winsorizing) представляет собой робастную технику обработ-
ки выбросов в данных. Эта техника предполагает ограничение переменной
сверху и снизу определенными порогами, к которым в итоге приравнивают-
ся выходящие за эти границы значения.
Обычно в качестве пороговых значений выбираются определенные про-
центили – допустим, 5-й и 95-й, в зависимости от требований анализа. В ре-
зультате значения, укладывающиеся в этот диапазон, сохраняются в исход-
ном виде, а выходящие за пределы приравниваются к граничным величинам.
Винсоризация позволяет сохранить все наблюдения в наборе данных
и снижает влияние выбросов.
На практике реализовать эту процедуру можно при помощи метода clip(),
как показано ниже:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
np.random.seed(42)
# Создаем простой набор данных с выбросами
data={
'Age': np.concatenate([
np.random.normal(30, 5, 1000), # Нормальное распределение
np.random.exponential(10, 200) + 50 # Данные с перекосом в правую сторону
])
}
df = pd.DataFrame(data)
# Функция определения выбросов на основе межквартильного размаха
def detect_outliers_iqr(df, column):
Q1 = df[column].quantile(0.25)
Q3 = df[column].quantile(0.75)
IQR=Q3-Q1
lower_bound = Q1 - 1 .5 * IQR
upper_bound = Q3 + 1.5 * IQR
df[f'{column}_Outlier_IQR'] = ((df[column] < lower_bound) | (df[column] > upper_
bound)).astype(str)
return df
# Определяем выбросы
df = detect_outliers_iqr(df, 'Age')
Масштабирование и нормализация: оптимальное применение 265
# Винсоризация
lower_bound, upper_bound = df['Age'].quantile(0.05), df['Age'].quantile(0.95)
df['Age_Winsorized'] = df['Age'].clip(lower_bound, upper_bound)
# Визуализация
plt.figure(figsize=(15, 10))
# Исходное распределение
plt.subplot(2, 2, 1)
sns.histplot(data=df, x='Age', kde=True, color='blue')
plt.title('Исходное распределение')
# Распределение после винсоризации
plt.subplot(2, 2, 2)
sns.histplot(data=df, x='Age_Winsorized', kde=True, color='red')
plt.title('Распределение после винсоризации')
# Сравнение с помощью диаграмм размаха
plt.subplot(2, 2, 3)
sns.boxplot(data=df[['Age', 'Age_Winsorized']])
plt.title('Сравнение с помощью диаграмм размаха')
# Диаграмма рассеяния
plt.subplot(2, 2, 4)
plt.scatter(df['Age'], df['Age_Winsorized'], alpha=0.5)
plt.plot([df['Age'].min(), df['Age'].max()], [df['Age'].min(), df['Age'].max()], 'r--')
plt.xlabel('Исходные данные')
plt.ylabel('Винсоризированные данные')
plt.title('Исходные и винсоризированные данные')
plt.tight_layout()
plt.show()
# Статистика
print("Общая статистика:")
print(df[['Age', 'Age_Winsorized']].describe())
# Рассчитываем и выводим перекосы
print("\nПерекос:")
print(f"Исходные данные: {df['Age'].skew():.2f}")
print(f"Винсоризированные данные: {df['Age_Winsorized'].skew():.2f}")
# Доля наблюдений, подвергнутых винсоризации
affected_percentage = (df['Age'] != df['Age_Winsorized']).mean() * 100
print(f"\nДоля наблюдений, подвергнутых винсоризации: {affected_percentage:.2f}%")
Вывод:
Общая статистика:
Age Age_Winsorized
count 1200.000000
1200.000000
mean
35.194593
34.736904
std
13.028042
11.128199
min
13.793663
22.678673
25%
27.383625
27.383625
266 Преобразование и масштабирование признаков
50%
31.244011
31.244011
75%
36.529474
36.529474
max
124.417229
62.272135
Перекос:
Исходные данные: 1.96
Винсоризированные данные: 1.35
Доля наблюдений, подвергнутых винсоризации: 10.00%
Рис. 5.5 Исходные и винсоризированные данные
Кратко подводя итоги этого раздела, можно сказать, что минимаксное
масштабирование обычно применяется с алгоритмами, требующими поступ-
ления на вход переменных с фиксированным диапазоном значений, напри-
мер с нейронными сетями. Также этот способ масштабирования позволяет
сохранить нулевые значения, что бывает удобно в рекомендательных систе-
мах и при анализе текста. Кроме того, при минимаксном масштабировании
сохраняется форма распределения исходных данных, что бывает полезно,
когда относительная разница между значениями имеет значение.
Что касается стандартизации, или z-нормализации, ее лучше применять
при работе с алгоритмами, предполагающими или лучше работающими
с нормально распределенными данными. Также она позволяет сгладить раз-
ницу, обусловленную разными масштабами или единицами измерения в пе-
ременных. Кроме того, при использовании стандартизации сохраняется ин-
формация о выбросах в данных, поскольку сжатия диапазонов не происходит.
Логарифм, квадратный корень и другие нелинейные преобразования признаков 267
Для наборов данных с явными выбросами можно воспользоваться робаст-
ным масштабированием, использующим при расчетах медиану и межквар-
тильный размах вместо среднего и стандартного отклонения.
При выборе масштабирования необходимо учитывать особенности дан-
ных и требования, характерные для используемых алгоритмов. Экспери-
менты с разными типами масштабирования могут позволить существенно
улучшить качество итоговой модели.
5.2. Логарифм, квадратный корень
и другие нелинейные преобразования
признаков
Хотя масштабирование и стандартизация признаков являются очень важ-
ными шагами предварительной обработки данных, добавление в модель
нелинейных преобразований зачастую может оказывать еще большее по-
ложительное влияние на ее качество. Особенно эффективными такие преоб-
разования бывают при работе с переменными, обладающими сложным рас-
пределением, или при наличии сложных зависимостей между предикторами.
Нелинейные преобразования, такие как логарифмирование, извлечение
корня и т. д ., обладают следующими преимуществами.
1. Позволяют нивелировать перекосы в распределениях, что особенно
полезно при работе с распределениями с длинными «хвостами», харак-
терными для финансовых метрик, демографических показателей и т. д.
Убирая смещения, мы адаптируем данные для работы с алгоритмами,
предполагающими нормальное распределение, такими как линейная
регрессия, логистическая регрессия и пр. Также мы повышаем интер-
претируемость признаков, нивелируем влияние на модель выбросов,
не исключая их из анализа, и облегчаем визуализацию.
2. Помогают эффективно стабилизировать дисперсию переменных с раз-
ными масштабами, обеспечивая равный вклад предикторов в модель.
Также наличие нелинейных преобразований позволяет повысить при-
менимость статистических тестов, предполагающих постоянную дис-
персию, таких как линейная регрессия и ANOVA. Кроме того, таким
образом мы можем улучшить качество алгоритмов машинного обуче-
ния, чувствительных к масштабу и распределению предикторов, и об-
легчить процедуру оценки гиперпараметров модели и доверительных
интервалов.
3. Помогают улучшить интерпретируемость предикторов и обнаружить
скрытые зависимости. Нелинейные преобразования позволяют зна-
чительно упростить некоторые сложные зависимости между пере-
менными, что облегчает аналитикам отслеживание динамики данных.
268 Преобразование и масштабирование признаков
В экономике логарифмическое преобразование часто применяется
к переменным, отражающим доход или ВВП. Это позволяет специа-
листам интерпретировать коэффициенты таких переменных с точки
зрения процентных, а не абсолютных, изменений, что облегчает про-
цедуру обмена информацией.
4. Позволяют повысить обобщающую способность модели. Это может
быть особенно важно при использовании алгоритмов машинного
обучения, целью которых является предсказание целевой переменной
на новых данных, не использовавшихся в процессе обучения. Повы-
шение обобщающей способности модели достигается за счет нивели-
рования влияния выбросов, нормализации распределений, масштаби-
рования признаков и снижения общей сложности модели.
Рассмотрим примеры применения нелинейных преобразований к пере-
менным.
5.2.1. Логарифмическое преобразование
Логарифмическое преобразование (logarithmic transformation) представляет
собой мощную технику, применяющуюся к данным, имеющим явные пере-
косы в распределении. Вследствие сжатия диапазона больших величин и рас-
ширения диапазона маленьких величин этот вид преобразования позволяет
уменьшить перекосы в распределении и стабилизировать дисперсию.
Уникальные свойства логарифмической функции делают это преобразо-
вание особенно эффективным при работе с шаблонами с экспоненциальным
ростом и мультипликативными зависимостями между переменными. К при-
меру, в экономических данных логарифмическое преобразование позволяет
привести тренды с экспоненциальным ростом к линейному виду, что облег-
чает их анализ и моделирование.
Логарифмическое преобразование стоит использовать, когда:
данные сильно скошены в правую сторону (положительная асиммет-
рия);
в данных присутствуют выбросы (в результате они будут приближены
к основному диапазону значений);
зависимость между предиктором и целевой переменной является
мультипликативной, а не аддитивной;
необходимо обработать переменную, диапазон значений которой рас-
пространяется на несколько порядков;
относительная разница между значениями более важна, чем относи-
тельная. К примеру, при анализе рынка ценных бумаг часто большую
роль играют процентные изменения, а не абсолютные.
К ограничениям логарифмического преобразования можно отнести то, что
оно не может быть применено в исходном виде к переменным с нулевыми
и отрицательными значениями, а также то, что иногда оно приводит к из-
лишней коррекции, смещая распределение влево. Таким образом, вы должны
Логарифм, квадратный корень и другие нелинейные преобразования признаков 269
применять логарифмическое преобразование осмотрительно и хорошо по-
нимать свои данные.
Пример применения логарифмического преобразования:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# Простой набор данных с перекосом в правую сторону
data = {'HousePrices': [50000, 120000, 250000, 500000, 1200000, 2500000]}
df = pd.DataFrame(data)
# Применяем логарифмическое преобразование
df['LogHousePrices'] = np.log(df['HousePrices'])
# Визуализируем данные
fig, axs = plt.subplots(1, 2, figsize=(15, 8))
fig.suptitle('Стоимость домов: исходные данные и преобразованные')
axs[0].hist(df['HousePrices'], bins=20)
axs[0].set_title('Исходные')
axs[1].hist(df['LogHousePrices'], bins=20)
axs[1].set_title('Логарифм')
plt.tight_layout()
plt.show()
# Просматриваем преобразованные данные
print(df, end='\n\n')
# Рассчитываем перекос распределений для столбцов
for column in df.columns:
print(f"Перекос в столбце {column}: {df[column].skew()}")
Вывод:
HousePrices LogHousePrices
0
50000
10.819778
1
120000
11.695247
2
250000
12.429216
3
500000
13.122363
4
1200000
13.997832
5
2500000
14.731801
Перекос в столбце HousePrices: 1.5832524727295971
Перекос в столбце LogHousePrices: -0 .01792295461569738
Здесь мы сгенерировали простой набор данных с распределением, сильно
смещенным вправо за счет присутствия очень большого значения. Далее мы
с помощью функции np.log() преобразовали исходные значения в натураль-
ные логарифмы, что позволило избавиться от перекосов в распределении.
В заключение мы вывели на гистограмме распределения исходных и пре-
образованных данных и для каждой колонки отобразили показатель пере-
коса. Для исходных значений он составил 1.58, что говорит о существенной
погрешности, а для преобразованных – –0 .01, что очень близко к нулю.
270 Преобразование и масштабирование признаков
Рис. 5.6 Стоимость домов: исходные данные
и преобразованные при помощи натурального логарифма
5.2.2. Преобразование квадратного корня
Преобразование квадратного корня (square root transformation), график ко-
торого показан на рис. 5 .7, – это еще один вид преобразования значений,
позволяющий избавиться от перекосов в распределении и стабилизировать
дисперсию. Хотя в эффективности этот метод уступает логарифмическому
преобразованию, он достаточно хорошо справляется с нормализацией рас-
пределений. Преобразование квадратного корня бывает особенно полезно
при наличии умеренных перекосов в данных благодаря сбалансированному
подходу к нормализации.
Преимуществами этого вида преобразования перед логарифмическим яв-
ляется то, что при его использовании лучше сохраняется исходный масштаб
значений, а также его способность обрабатывать нулевые значения.
Применять преобразование квадратного корня стоит в ситуациях, когда:
исходные данные умеренно смещены вправо – не настолько, чтобы
применять к ним логарифмическое преобразование;
вам необходимо добиться более сглаженного преобразования в срав-
нении с логарифмированием;
исходные данные являются счетными, или дискретными, и примерно
следуют распределению Пуассона.
Стоит помнить, что преобразование квадратного корня уступает в мощно-
сти искажения данных логарифмическому преобразованию, что сокращает
область его использования. Всегда визуализируйте свои данные перед вы-
бором метода преобразования.
Логарифм, квадратный корень и другие нелинейные преобразования признаков 271
Рис. 5.7 График функции квадратного корня
Пример применения преобразования квадратного корня:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# Простой набор данных с перекосом в правую сторону
data = {'HousePrices': [50000, 120000, 250000, 500000, 1200000, 2500000]}
df = pd.DataFrame(data)
# Применяем преобразование квадратного корня
df['SqrtHousePrices'] = np.sqrt(df['HousePrices'])
# Визуализируем данные
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
ax1.hist(df['HousePrices'], bins=20)
ax1.set_title('Исходные')
ax1.set_xlabel('Стоимость')
ax1.set_ylabel('Частота')
ax2.hist(df['SqrtHousePrices'], bins=20)
ax2.set_title('Квадратный корень')
ax2.set_xlabel('Корень стоимости')
ax2.set_ylabel('Частота')
plt.tight_layout()
plt.show()
# Выводим статистику
print("Статистика исходных данных:")
print(df['HousePrices'].describe())
272 Преобразование и масштабирование признаков
print(f"Перекос: {df['HousePrices'].skew()}")
print("\nСтатистика преобразованных данных:")
print(df['SqrtHousePrices'].describe())
print(f"Перекос: {df['SqrtHousePrices'].skew()}")
# Просматриваем преобразованные данные
print("\nПреобразованные данные:")
print(df)
Вывод:
Статистика исходных данных:
count 6.000000e+00
mean
7.700000e+05
std
9.446693e+05
min
5.000000e+04
25%
1.525000e+05
50%
3.750000e+05
75%
1.025000e+06
max
2.500000e+06
Name: HousePrices, dtype: float64
Перекос: 1.5832524727295971
Статистика преобразованных данных:
count
6.000000
mean
742.284614
std
512.656085
min
223.606798
25%
384.807621
50%
603.553391
75%
998.360532
max
1581.138830
Name: SqrtHousePrices, dtype: float64
Перекос: 0.9317992459358383
Преобразованные данные:
HousePrices SqrtHousePrices
0
50000
223.606798
1
120000
346.410162
2
250000
500.000000
3
500000
707.106781
4
1200000
1095.445115
5
2500000
1581.138830
Логарифм, квадратный корень и другие нелинейные преобразования признаков 273
Рис. 5.8 Стоимость домов: исходные данные
и преобразованные при помощи квадратного корня
Для преобразования данных мы воспользовались функцией np.sqrt(). Д а-
лее рассмотрим применение к исходным значениям кубического корня.
5.2.3. Преобразование кубического корня
Преобразование кубического корня (cube root transformation), график которого
показан на рис. 5 .9, представляет собой достаточно гибкую технику, которая
может быть применена к наборам данных с умеренными перекосами, со-
держащими как положительные, так и отрицательные значения. Этот тип
преобразования обладает определенными преимуществами над рассмотрен-
ными выше методами, в частности благодаря способности обрабатывать
более широкий спектр типов исходных данных.
Рис. 5.9 График функции кубического корня
274 Преобразование и масштабирование признаков
Преобразование кубического корня стоит применять, когда:
в данных присутствуют как положительные, так и отрицательные зна-
чения, что не позволяет воспользоваться логарифмическим преобра-
зованием и преобразованием квадратного корня;
вам необходимо лишь слегка сгладить распределение значений в срав-
нении с логарифмическим преобразованием;
вам важно сохранить направление перекоса (положительный или от-
рицательный) в исходных данных для лучшей интерпретации;
переменные обладают естественной кубической зависимостью, напри-
мер это касается величин, связанных с объемом, в физических науках.
Пример применения преобразования кубического корня:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# Простой набор данных с перекосом в правую сторону
data = {'HousePrices': [50000, 120000, 250000, 500000, 1200000, 2500000]}
df = pd.DataFrame(data)
# Применяем преобразование кубического корня
df['CubeRootHousePrices'] = np.cbrt(df['HousePrices'])
# Визуализируем данные
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
ax1.hist(df['HousePrices'], bins=20)
ax1.set_title('Исходные')
ax1.set_xlabel('Стоимость')
ax1.set_ylabel('Частота')
ax2.hist(df['CubeRootHousePrices'], bins=20)
ax2.set_title('Кубический корень')
ax2.set_xlabel('Кубический корень стоимости')
ax2.set_ylabel('Частота')
plt.tight_layout()
plt.show()
# Выводим статистику
print("Статистика исходных данных:")
print(df['HousePrices'].describe())
print(f"Перекос: {df['HousePrices'].skew()}")
print("\nСтатистика преобразованных данных:")
print(df['CubeRootHousePrices'].describe())
print(f"Перекос: {df['CubeRootHousePrices'].skew()}")
# Просматриваем преобразованные данные
print("\nПреобразованные данные:")
print(df)
Вывод:
Статистика исходных данных:
count 6.000000e+00
Логарифм, квадратный корень и другие нелинейные преобразования признаков 275
mean
7.700000e+05
std
9.446693e+05
min
5.000000e+04
25%
1.525000e+05
50%
3.750000e+05
75%
1.025000e+06
max
2.500000e+06
Name: HousePrices, dtype: float64
Перекос: 1.5832524727295971
Статистика преобразованных данных:
count
6.000000
mean
78.419567
std
37.075644
min
36.840315
25%
52.742194
50%
71.183053
75%
99.541906
max
135.720881
Name: CubeRootHousePrices, dtype: float64
Перекос: 0.6448974339970162
Преобразованные данные:
HousePrices CubeRootHousePrices
0
50000
36.840315
1
120000
49.324241
2
250000
62.996052
3
500000
79.370053
4
1200000
106.265857
5
2500000
135.720881
Рис. 5.10 Стоимость домов: исходные данные
и преобразованные при помощи кубического корня
276 Преобразование и масштабирование признаков
Здесь мы для выполнения преобразования кубического корня воспользо-
вались функцией np.cbrt().
5.2.4. Преобразования Бокса−Кокса
и Йео−Джонсона
Преобразования Бокса−Кокса (Box-Cox transformation) и Йео−Джонсона (Yeo-
Johnson transformation) представляют собой более сложные методы транс-
формации данных с динамическим определением степени преобразования.
В основе этих методов лежит степенное преобразование, которое можно
настроить для борьбы с перекосами распределения или для стабилизации
дисперсии.
Преобразование Бокса−Кокса, представленное в 1964 году статистиками
Джорджем Боксом (George Box) и Дэвидом Коксом (David Cox), используется
только с положительными значениями и подразумевает применение степен-
ного преобразования к каждому наблюдению, при этом параметр степени
(лямбда) оптимизируется таким образом, чтобы распределение преобразо-
ванных данных было как можно ближе к нормальному. Этот метод активно
используется в таких сферах, как экономика, биология и инженерия, бла-
годаря своей способности нормализовывать данные и улучшать качество
статистических моделей.
Формула преобразования Бокса−Кокса выглядит следующим образом:
С другой стороны, преобразование Йео−Джонсона, предложенное в 2000 го-
ду Ин-Квон Йео (In-Kwon Yeo) и Ричардом Джонсоном (Richard Johnson),
стало продолжением метода Бокса−Кокса, допускающим присутствие в на-
боре данных отрицательных значений. Это свойство позволило данному
методу завоевать популярность в финансовой сфере, где рассчитываются
прибыли и убытки, а также в области научных исследований, где показатели
часто могут принимать как положительные, так и отрицательные значения.
В преобразовании Йео−Джонсона применяется тот же степенной подход, но
с участием дополнительных параметров для контроля знака.
Оба метода активно применяются в машинном обучении и статистиче-
ском моделировании в связи с тем, что способны значительно улучшить
качество работы алгоритмов, предполагающих наличие нормально распре-
деленных данных. Автоматический поиск оптимального параметра преоб-
разования позволяет снять нагрузку с аналитика по его подбору методом
проб и ошибок.
Логарифм, квадратный корень и другие нелинейные преобразования признаков 277
Формула преобразования Йео−Джонсона выглядит так:
Преобразования Бокса−Кокса и Йео−Джонсона стоит использовать, когда:
вы имеете дело с сильно скошенными данными, которые необходимо
нормализовать для использования в статистическом анализе или ме-
тодах машинного обучения;
зависимости между переменными имеют нелинейный характер, от
которого необходимо избавиться;
вам нужно привести данные к более или менее нормальному распре-
делению без больших временных затрат;
набору данных свойственна гетероскедастичность (неоднородность
дисперсии);
в данных присутствуют как положительные, так и отрицательные зна-
чения (применительно к методу Йео−Джонсона);
вы хотите повысить качество регрессионной модели за счет удовлет-
ворения требований, касающихся нормальности распределения и го-
москедастичности.
Пример применения преобразования Бокса−Кокса:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.preprocessing import PowerTransformer
# Простой набор данных (только положительные значения для метода Бокса-Кокса)
data = {'Income': [30000, 50000, 100000, 200000, 500000, 1000000, 2000000]}
df = pd.DataFrame(data)
# Применяем преобразование Бокса-Кокса с помощью класса PowerTransformer
boxcox_transformer = PowerTransformer(method='box-cox')
df['BoxCoxIncome'] = boxcox_transformer.fit_transform(df[['Income']])
# Визуализируем исходные и преобразованные данные
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
ax1.hist(df['Income'], bins=20)
ax1.set_title('Исходное распределение дохода')
ax1.set_xlabel('Доход')
ax1.set_ylabel('Частота')
ax2.hist(df['BoxCoxIncome'], bins=20)
ax2.set_title('Распределение с методом Бокса-Кокса')
ax2.set_xlabel('Преобразованный доход')
ax2.set_ylabel('Частота')
plt.tight_layout()
278 Преобразование и масштабирование признаков
plt.show()
# Статистика
print("Статистика исходных данных:")
print(df['Income'].describe())
print(f"Перекос: {df['Income'].skew()}")
print("\nСтатистика преобразованных данных:")
print(df['BoxCoxIncome'].describe())
print(f"Перекос: {df['BoxCoxIncome'].skew()}")
# Просматриваем преобразованные данные
print("\nПреобразованные данные:")
print(df)
# Выводим оптимальное значение лямбды
print(f"\nОптимальное значение лямбды: {boxcox_transformer.lambdas_[0]}")
Вывод:
Статистика исходных данных:
count
7.000000e+00
mean
5.542857e+05
std
7.248875e+05
min
3.000000e+04
25%
7.500000e+04
50%
2.000000e+05
75%
7.500000e+05
max
2.000000e+06
Name: Income, dtype: float64
Перекос: 1.6536560235830982
Статистика преобразованных данных:
count
7.000000e+00
mean
2.537653e-16
std
1.080123e+00
min
- 1.432277e+00
25%
- 7.952794e-01
50%
- 5.135827e-02
75%
8.016251e-01
max
1.470944e+00
Name: BoxCoxIncome, dtype: float64
Перекос: 0.03930228236089779
Преобразованные данные:
Income BoxCoxIncome
0 30000
- 1 .432277
1 50000
- 1 .048540
2 100000
- 0 .542019
3 200000
- 0 .051358
4 500000
0.573762
5 1000000
1.029488
6 2000000
1.470944
Оптимальное значение лямбды: -0 .04589374550502441
Логарифм, квадратный корень и другие нелинейные преобразования признаков 279
Рис. 5.11 Распределение исходных данных
и преобразованных при помощи метода Бокса−Кокса
Здесь нам потребовалось дополнительно импортировать класс PowerTrans-
former из модуля sklearn.preprocessing, чтобы воспользоваться преобразо-
ванием Бокса−Кокса. Далее мы создали экземпляр класса с помощью ин-
струкции boxcox_transformer = PowerTransformer(method='box-cox'), после чего
выполнили преобразование, вызвав метод fit_transform(). Также мы вос-
пользовались атрибутом lambdas_ для вывода на экран оптимального значе-
ния лямбды.
Пример применения преобразования Йео−Джонсона:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.preprocessing import PowerTransformer
from scipy import stats
# Простой набор данных (включающий отрицательные значения)
data = {'Profit': [-5000, -2000, 0, 3000, 15000, 50000, 100000]}
df = pd.DataFrame(data)
# Применяем преобразование Йео-Джонсона с помощью класса PowerTransformer
yeojohnson_transformer = PowerTransformer(method='yeo-johnson')
df['YeoJohnsonProfit'] = yeojohnson_transformer.fit_transform(df[['Profit']])
# Визуализируем исходные и преобразованные данные
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
ax1.hist(df['Profit'], bins=20)
ax1.set_title('Исходное распределение дохода')
ax1.set_xlabel('Доход')
ax1.set_ylabel('Частота')
ax2.hist(df['YeoJohnsonProfit'], bins=20)
280 Преобразование и масштабирование признаков
ax2.set_title('Распределение с методом Йео-Джонсона')
ax2.set_xlabel('Преобразованный доход')
ax2.set_ylabel('Частота')
plt.tight_layout()
plt.show()
# Статистика
print("Статистика исходных данных:")
print(df['Profit'].describe())
print(f"Перекос: {df['Profit'].skew()}")
print("\nСтатистика преобразованных данных:")
print(df['YeoJohnsonProfit'].describe())
print(f"Перекос: {df['YeoJohnsonProfit'].skew()}")
# Просматриваем преобразованные данные
print("\nПреобразованные данные:")
print(df)
# Выводим оптимальное значение лямбды
print(f"\nОптимальное значение лямбды: {yeojohnson_transformer.lambdas_[0]}")
Вывод:
Статистика исходных данных:
count
7.000000
mean
23000.000000
std
38858.718455
min
- 5000.000000
25%
- 1000.000000
50%
3000.000000
75%
32500.000000
max
100000.000000
Name: Profit, dtype: float64
Перекос: 1.662057654773647
Статистика преобразованных данных:
count
7.000000e+00
mean
6.344132e-17
std
1.080123e+00
min
- 1.642769e+00
25%
- 4.768418e-01
50%
- 1.318063e-01
75%
5.040181e-01
max
1.720223e+00
Name: YeoJohnsonProfit, dtype: float64
Перекос: 0.16354488955295873
Преобразованные данные:
Profit YeoJohnsonProfit
0 -5000
- 1 .642769
1 -2000
- 0 .723508
2
0
- 0 .230176
3 3000
- 0 .131806
Логарифм, квадратный корень и другие нелинейные преобразования признаков 281
4 15000
0.157422
5 50000
0.850614
6 100000
1.720223
Оптимальное значение лямбды: 0.8516309021338928
Рис. 5.12 Распределение исходных данных
и преобразованных при помощи метода Йео−Джонсона
Здесь мы воспользовались тем же классом PowerTransformer с параметром
method='yeo-johnson' для применения преобразования Йео−Джонсона.
Кратко подводя итоги этого раздела, можно выделить следующие условия
для использования описанных выше преобразований признаков:
логарифмическое преобразование стоит применять при наличии
больших перекосов в распределении исходных данных и необходимо-
сти снизить влияние на модель больших значений. Это преобразование
позволяет сжать масштаб в районе больших значений переменной, что
делает его особенно полезным при работе с распределениями, скошен-
ными в правую сторону. Часто применяется в области финансов и при
анализе рынка ценных бумаг;
преобразование квадратного корня влияет на распределение дан-
ных не столь агрессивно, что позволяет применять его при наличии
умеренных перекосов в данных. Бывает полезно при работе со счет-
ными данными или при необходимости в какой-то мере сохранить
исходные масштабы данных. Часто применяется в экологических ис-
следованиях для получения информации о численности видов;
преобразование кубического корня может применяться при на-
личии в данных как положительных, так и отрицательных значений.
Бывает особенно полезно в сценариях, где важна симметрия данных,
например в области физических или химических исследований. Пре-
282 Преобразование и масштабирование признаков
образование кубического корня обладает уникальным свойством со-
хранения исходного знака величины;
преобразования Бокса−Кокса и Йео−Джонсона представляют со-
бой гибкие степенные методы, автоматически адаптирующиеся к ис-
ходным значениям, что делает их особенно полезными при работе со
сложными наборами данных. В этих преобразованиях используется
параметр (лямбда), позволяющий найти оптимальную степень пре-
образования. Но если преобразование Бокса−Кокса может быть при-
менено только к положительным значениям, то преобразование Йео−
Джонсона допускает присутствие в данных отрицательных значений,
что делает его применение более универсальным.
Выбирать наиболее подходящее преобразование стоит исходя из конкрет-
ной ситуации. К примеру, при работе с временными рядами вы могли бы
применить к данным логарифмическое преобразование для стабилизации
дисперсии. Если же вам необходимо обработать данные, содержащие отрица-
тельные значения, такие как температурные изменения, вы можете попро-
бовать применить преобразование кубического корня или преобразование
Йео−Джонсона.
5.3. Практические упражнения
Теперь приступим к выполнению заданий, с помощью которых вы сможете
закрепить полученные знания на практике.
Упражнение 1. Минимаксное масштабирование
Есть набор данных о клиентах со столбцами Age и Income:
import pandas as pd
from sklearn.preprocessing import MinMaxScaler
# Простой набор данных
data = {'Age': [25, 40, 35, 50, 60],
'Income': [40000, 50000, 60000, 80000, 100000]}
df = pd.DataFrame(data)
Ваша задача состоит в том, чтобы применить минимаксное масштабиро-
вание к обоим столбцам, приведя их к диапазону значений от 0 до 1.
Решение
# Создаем экземпляр класса
scaler = MinMaxScaler()
# Применяем масштабирование к набору данных
df_scaled = pd.DataFrame(scaler.fit_transform(df), columns=df.columns)
Практические упражнения 283
# Выводим результаты
print(df_scaled)
Вывод:
Age Income
0 0.000000 0 .000000
1 0.428571 0.166667
2 0.285714 0.333333
3 0.714286 0.666667
4 1.000000 1.000000
Упражнение 2. Стандартизация (z-нормализация)
Имеем тот же набор исходных данных:
import pandas as pd
from sklearn.preprocessing import StandardScaler
# Простой набор данных
data = {'Age': [25, 40, 35, 50, 60],
'Income': [40000, 50000, 60000, 80000, 100000]}
df = pd.DataFrame(data)
Ваша задача состоит в том, чтобы применить стандартизацию к обоим
столбцам.
Решение
# Создаем экземпляр класса
scaler = StandardScaler()
# Применяем стандартизацию к набору данных
df_standardized = pd.DataFrame(scaler.fit_transform(df), columns=df.columns)
# Выводим результаты
print(df_standardized)
Вывод:
Age Income
0 -1 .406930 -1 .207020
1 -0 .165521 -0 .742781
2 -0 .579324 -0 .278543
3 0.662085 0.649934
4 1.489691 1.578410
Упражнение 3. Логарифмическое преобразование
У нас есть набор данных с рыночными стоимостями домов, сильно переко-
шенный в правую сторону:
import numpy as np
import pandas as pd
284 Преобразование и масштабирование признаков
# Простой набор данных с сильным перекосом вправо
data = {'HousePrices': [50000, 120000, 250000, 500000, 1200000, 2500000]}
df = pd.DataFrame(data)
Ваша задача состоит в том, чтобы применить логарифмическое преоб-
разование к столбцу.
Решение
# Применяем логарифмическое преобразование
df['LogHousePrices'] = np.log(df['HousePrices'])
# Выводим результаты
print(df)
Вывод:
HousePrices LogHousePrices
0
50000
10.819778
1
120000
11.695247
2
250000
12.429216
3
500000
13.122363
4
1200000
13.997832
5
2500000
14.731801
Упражнение 4. Преобразование квадратного корня
Набор данных возьмем из предыдущего упражнения. Теперь ваша задача со-
стоит в том, чтобы применить преобразование квадратного корня к столбцу.
Решение
# Применяем преобразование квадратного корня
df['SqrtHousePrices'] = np.sqrt(df['HousePrices'])
# Выводим результаты
print(df)
Вывод:
HousePrices SqrtHousePrices
0
50000
223.606798
1
120000
346.410162
2
250000
500.000000
3
500000
707.106781
4
1200000
1095.445115
5
2500000
1581.138830
Упражнение 5. Преобразование кубического корня
У нас есть набор данных со столбцом PropertyValues, содержащим как поло-
жительные, так и отрицательные значения:
import numpy as np
import pandas as pd
Практические упражнения 285
# Простой набор данных с положительными и отрицательными значениями
data = {'PropertyValues': [-8000, -5000, 0, 5000, 10000, 20000]}
df = pd.DataFrame(data)
Ваша задача состоит в том, чтобы применить преобразование кубического
корня к столбцу.
Решение
# Применяем преобразование кубического корня
df['CubeRootPropertyValues'] = np.cbrt(df['PropertyValues'])
# Выводим результаты
print(df)
Вывод:
PropertyValues CubeRootPropertyValues
0
-8000
- 20.000000
1
- 5000
- 1 7.099759
2
0
0.000000
3
5000
17.099759
4
10000
21.544347
5
20000
27.144176
Упражнение 6. Преобразование Бокса−Кокса
У нас есть набор данных со столбцом Income и небольшим перекосом в правую
сторону:
from sklearn.preprocessing import PowerTransformer
import pandas as pd
# Простой набор данных (только положительные значения)
data = {'Income': [30000, 50000, 100000, 200000, 500000]}
df = pd.DataFrame(data)
Ваша задача состоит в том, чтобы применить преобразование Бокса−Кокса
к столбцу.
Решение
# Применяем преобразование Бокса–Кокса
boxcox_transformer = PowerTransformer(method='box-cox')
df['BoxCoxIncome'] = boxcox_transformer.fit_transform(df[['Income']])
# Выводим результаты
print(df)
Вывод:
Income BoxCoxIncome
0 30000
- 1 .368839
1 50000
- 0 .754499
286 Преобразование и масштабирование признаков
2 100000
0.000031
3 200000
0.672558
4 500000
1.450750
Упражнение 7. Преобразование Йео−Джонсона
У нас есть набор данных со столбцом Profit, содержащим как положительные,
так и отрицательные значения:
from sklearn.preprocessing import PowerTransformer
import pandas as pd
# Простой набор данных с положительными и отрицательными значениями
data = {'Profit': [-5000, -2000, 0, 3000, 15000]}
df = pd.DataFrame(data)
Ваша задача состоит в том, чтобы применить преобразование Йео−Джонсона
к столбцу.
Решение
# Применяем преобразование Йео–Джонсона
yeojohnson_transformer = PowerTransformer(method='yeo-johnson')
df['YeoJohnsonProfit'] = yeojohnson_transformer.fit_transform(df[['Profit']])
# Выводим результаты
print(df)
Вывод:
Profit YeoJohnsonProfit
0 -5000
- 1 .475842
1 -2000
- 0 .518763
2
0
0.056200
3 3000
0.390016
4 15000
1.548388
5.4. Возможные проблемы
Преобразование и масштабирование признаков – важная часть предвари-
тельной подготовки данных для дальнейшего анализа. Но и здесь вы можете
столкнуться с проблемами, о которых мы спешим заранее вас предупредить.
5.4.1. Неправильный выбор метода преобразования
Одной из самых распространенных ошибок здесь является неправильный
выбор способа трансформации данных. Не все признаки стоит масштабиро-
вать одинаково, а применение неподходящего преобразования может приве-
сти к нарушению зависимостей между предикторами и целевой переменной.
Возможные проблемы 287
Что может пойти не так:
применение логарифмического преобразования к данным, содержа-
щим нулевые или отрицательные значения, может приводить к ошиб-
кам или неожиданным результатам, поскольку логарифм для отрица-
тельных чисел не определен;
применение преобразования квадратного корня к данным с отрица-
тельными значениями может выдавать ошибки NaN (не число);
использование минимаксного масштабирования с данными, содержа-
щими сильные выбросы, может приводить к сильному сжатию основ-
ного диапазона значений, что сделает модель излишне чувствительной
к выбросам.
Решение:
всегда тщательно изучайте свои данные перед выбором метода транс-
формации. Если в данных присутствуют нулевые или отрицательные
значения, стоит отдавать предпочтение преобразованию кубического
корня или преобразованию Йео−Джонсона;
для переменных с экстремальными выбросами можно рассмотреть
вариант с применением робастного масштабирования или других ме-
тодов трансформации, устойчивых к выбросам, таких как логарифми-
ческое преобразование или преобразование кубического корня.
5.4.2. Неправильное масштабирование
тестовых данных
При работе с алгоритмами машинного обучения неправильное преобразова-
ние или масштабирование данных, особенно после разбиения исходного на-
бора на обучающую и тестовую выборки, может приводить к переобучению
модели или некорректным предсказаниям.
Что может пойти не так:
если масштабирование применяется к обеим выборкам одновремен-
но (до разбиения), в тестовые данные может утечь информация из
обучающей выборки, что приведет к излишне оптимистичной оценке
качества итоговой модели;
применение преобразований к обеим выборкам полностью независи-
мо может приводить к появлению неконсистентных масштабов и рас-
хождению между выборками.
Решение:
всегда применяйте масштабирование и преобразования после разде-
ления исходных данных на обучающую и тестовую выборки;
обучайте модели преобразования на обучающей выборке, после чего
применяйте их к тестовой выборке. Это гарантирует вам, что тесто-
вые данные не будут влиять на процесс обучения.
288 Преобразование и масштабирование признаков
5.4.3. Излишнее преобразование признаков
Хотя преобразование признаков призвано повышать качество итоговой мо-
дели, чрезмерное преобразование может пойти модели во вред, особенно
это касается нелинейных трансформаций вроде логарифмического преоб-
разования или преобразования Бокса−Кокса. Избыточное преобразование
признаков может приводить к снижению интерпретируемости модели и на-
рушению естественных зависимостей в данных.
Что может пойти не так:
применение множественных преобразований признаков в попытке
нормализовать данные может приводить к ухудшению интерпретиру-
емости связей между предикторами;
избыточно агрессивные преобразования признаков (например, приме-
нение логарифмического преобразования к уже нормально распреде-
ленным данным) могут приводить к излишнему сглаживанию распре-
делений данных, что сделает их менее информативными для модели.
Решение:
используйте преобразование признаков только тогда, когда это не-
обходимо. Если данные уже распределены нормально, в дальнейших
преобразованиях может уже не быть необходимости;
всегда визуализируйте данные перед и после применения преобразо-
ваний, чтобы убедиться, что они пошли на пользу и только улучшили
распределение.
5.4.4. Неправильная интерпретация результатов
логарифмического преобразования
Как мы уже говорили, в процессе применения логарифмического преоб-
разования диапазоны в области больших значений сжимаются, что может
приводить к ошибкам при интерпретации результатов.
Что может пойти не так:
после применения логарифмического преобразования масштаб пере-
менной меняется. Вследствие этого попытки интерпретировать ре-
зультаты без обратного преобразования могут приводить к неправиль-
ным выводам о влиянии предиктора на модель.
Решение:
при применении логарифмического преобразования всегда помни-
те о необходимости обратного преобразования (экспоненциального)
для приведения признака к исходному масштабу. Это особенно важно,
когда речь идет о презентации результатов работы модели людям без
технического образования.
Возможные проблемы 289
5.4.5. Игнорирование природы нелинейных
зависимостей
Не все зависимости между предикторами и целевой переменной линейны.
При использовании только линейных преобразований вроде масштабиро-
вания или нормализации можно упустить важные нелинейные зависимости
в данных.
Что может пойти не так:
предположение о линейной зависимости между переменными и при-
менение соответствующего преобразования может не позволить обна-
ружить более сложные шаблоны в данных;
если в действительности связь между переменными нелинейна, при-
менение только простых линейных преобразований может ослабить
качество предсказаний модели.
Решение:
всегда тщательно исследуйте данные на предмет наличия нелинейных
зависимостей;
визуализируйте связи между предикторами и целевой переменной,
чтобы лучше понимать существующие шаблоны.
5.4.6. Неправильное обращение с выбросами
Преобразования вроде минимаксного масштабирования и стандартизации
чувствительны к выбросам в данных. Если в вашем наборе данных содер-
жатся большие выбросы, применение таких видов преобразования может
привести к искажению действительных масштабов значений.
Что может пойти не так:
наличие выбросов при масштабировании данных может приводить
к сжатию основного диапазона значений, что не позволит модели по-
казывать хорошее качество, особенно если она полагается на расстоя-
ния между наблюдениями, как, к примеру, метод k-ближайших соседей.
Решение:
перед выполнением трансформаций определите наличие в ваших при-
знаках выбросов и избавьтесь от них, применив некий порог, либо
используйте робастное масштабирование на основе межквартильных
размахов, более устойчивое к выбросам;
воспользуйтесь логарифмическим преобразованием или преобразо-
ванием квадратного корня для минимизации влияния выбросов и со-
хранения общей структуры данных.
290 Преобразование и масштабирование признаков
Заключение
В этой главе мы поговорили о том, почему так важно преобразовывать и мас-
штабировать исходные переменные при подготовке данных для методов
машинного обучения. Правильно масштабированные и преобразованные
признаки позволят модели обнаружить все присутствующие в данных за-
висимости, что положительно скажется на ее качестве.
В начале главы мы рассмотрели простые способы преобразования число-
вых данных, такие как минимаксное масштабирование и стандартизация.
Минимаксное масштабирование позволяет привести все значения перемен-
ной к фиксированному диапазону, обычно от 0 до 1. В то же время стандар-
тизация, или z-нормализация, приводит переменную к нулевому среднему
и единичному стандартному отклонению, что лучше подходит для моделей,
предполагающих нормальное распределение данных.
Далее мы подробно поговорили о нелинейных типах преобразований,
таких как логарифмическое преобразование, преобразование квадратного
и кубического корня, а также о преобразованиях Бокса−Кокса и Йео−Джон-
сона. Эти типы трансформаций позволяют избавиться от перекосов в рас-
пределениях, стабилизировать дисперсию, сделать зависимости между пере-
менными более линейными и улучшить качество моделей.
Следующую главу мы целиком и полностью посвятим приемам кодирова-
ния категориальных переменных.
Глава 6
Кодирование
категориальных
переменных
При работе с моделями машинного обучения одной из главных сложностей
является правильное кодирование категориальных переменных. В отличие
от числовых переменных, категориальные часто требуют особого подхода
к кодированию, чтобы алгоритмы машинного обучения работали эффек-
тивно. Кодирование таких переменных должно помогать модели выявлять
зависимости между категориями и эффективно использовать эту инфор-
мацию в процессе предсказания. В этой главе мы рассмотрим множество
техник кодирования категориальных переменных и начнем с углубленного
изучения всех нюансов самого распространенного типа, а именно кодиро-
вания с одним активным состоянием. В следующих разделах мы рассмотрим
и другие важные техники кодирования.
6.1. Кодирование с одним активным
состоянием: углубленное изучение
Кодирование с одним активным состоянием (one-hot encoding) представляет
собой фундаментальную технику приведения значений категориальных пе-
ременных из набора данных в формат, позволяющий максимально повысить
эффективность алгоритмов машинного обучения. В результате применения
этого метода для каждого уникального значения переменной создается от-
дельный двоичный столбец, в котором устанавливается единица, если для
конкретного наблюдения значение переменной соответствует данной ка-
тегории, и ноль – в противном случае. И хотя этот способ кодирования до-
статочно прост в реализации, ему присущи несколько нюансов, требующих
особого рассмотрения.
292 Кодирование категориальных переменных
Одним из главных преимуществ кодирования с одним активным состоя-
нием является его способность сохранять отсутствие порядковой природы
исходной переменной. В отличие от числовых методов кодирования, которые
могут непроизвольно вносить некоторый порядок следования категорий, ко-
дирование с одним активным состоянием рассматривает каждую категорию
независимо от остальных. Это бывает особенно удобно для таких категорий,
как цвет, не подразумевающих определенного порядка.
В то же время метод кодирования с одним активным состоянием может
доставлять неудобства при работе с большими наборами данных или пере-
менными с большим количеством уникальных значений. В таких условиях
вы можете не только существенно увеличить количество переменных в на-
боре, но и получить сильно разреженную матрицу, что может негативно
сказаться на качестве и интерпретируемости будущей модели.
Кроме того, кодирование с одним активным состоянием может быть связа-
но со сложностями при появлении новых категорий во время развертывания
модели. Если в модели появится категория, не участвовавшая в процессе
обучения, для нее не будет создана отдельная колонка, что потенциально
может приводить к ошибкам классификации. Таким образом, появляется
необходимость в стратегии обработки неизвестных категорий, например
в виде создания одной общей категории во время кодирования, которая
будет включать в себя все необработанные категории.
В данном разделе мы подробно обсудим все эти особенности и дадим со-
веты по эффективной реализации метода кодирования с одним активным
состоянием. Мы рассмотрим разные стратегии, позволяющие избежать так
называемого проклятия размерности, поработаем с неизвестными катего-
риями и обсудим способы оптимизации вычислений.
Суть метода кодирования с одним активным состоянием
Реализация метода кодирования с одним активным состоянием примени-
тельно к столбцу Color, содержащему три категориальных значения Red, Blue
и Green, предполагает создание трех новых столбцов с именами Color_Red,
Color_Blue и Color_Green, в которых будут стоять единицы там, где в исходном
наблюдении присутствовал соответствующий цвет, и нули в остальных слу-
чаях. Рассмотрим пример с тремя категориальными столбцами:
import pandas as pd
import numpy as np
from sklearn.preprocessing import OneHotEncoder
# Простой набор данных с тремя категориальными столбцами
data={
'Color': ['Red', 'Blue', 'Green', 'Blue', 'Red', 'Yellow'],
'Size': ['Small', 'Medium', 'Large', 'Medium', 'Small', 'Large'],
'Brand': ['A', 'B', 'C', 'A', 'B', 'C']
}
df = pd.DataFrame(data)
print("Исходный набор данных:")
Кодирование с одним активным состоянием: углубленное изучение 293
print(df)
print("\n")
# Способ 1: с использованием функции get_dummies()
df_one _hot_pd = pd.get_dummies(df, columns=['Color', 'Size', 'Brand'],
prefix=['Color', 'Size', 'Brand'])
print("Кодирование с помощью библиотеки pandas:")
print(df_one _hot_pd)
print("\n")
# Способ 2: с помощью класса OneHotEncoder из библиотеки sklearn
encoder = OneHotEncoder(sparse_output=False, handle_unknown='ignore')
encoded_features = encoder.fit_transform(df)
# Создаем датафрейм с закодированными именами столбцов
feature_names = encoder.get_feature_names _out(['Color', 'Size', 'Brand'])
df_one _hot_sk = pd.DataFrame(encoded_features, columns=feature_names)
print("Кодирование с помощью библиотеки sklearn:")
print(df_one _hot_sk)
print("\n")
# Работа с неизвестными категориями
new_data = pd.DataFrame({'Color': ['Purple'], 'Size': ['Extra Large'], 'Brand': ['D']})
encoded_new_data = encoder.transform(new_data)
df_new_encoded = pd.DataFrame(encoded_new_data, columns=feature_names)
print("Обработка неизвестных категорий:")
print(df_new_encoded)
Вывод:
Исходный набор данных:
Color Size Brand
0
Red Small
A
1 Blue Medium
B
2 Green Large
C
3 Blue Medium
A
4
Red Small
B
5 Yellow Large
C
Кодирование с помощью библиотеки pandas:
Color_Blue Color_Green Color_Red ... Brand_A Brand_B Brand_C
0
False
False
True ...
True
False False
1
True
False
False ...
False
True
False
2
False
True
False ...
False False
True
3
True
False
False ...
True
False False
4
False
False
True ...
False
True
False
5
False
False
False ...
False False
True
[6 rows x 10 columns]
Кодирование с помощью библиотеки sklearn:
Color_Blue Color_Green Color_Red ... Brand_A Brand_B Brand_C
0
0.0
0.0
1.0
...
1.0
0.0
0.0
1
1.0
0.0
0.0
...
0.0
1.0
0.0
294 Кодирование категориальных переменных
2
0.0
1.0
0.0
...
0.0
0.0
1.0
3
1.0
0.0
0.0
...
1.0
0.0
0.0
4
0.0
0.0
1.0
...
0.0
1.0
0.0
5
0.0
0.0
0.0
...
0.0
0.0
1.0
[6 rows x 10 columns]
Обработка неизвестных категорий:
Color_Blue Color_Green Color_Red ... Brand_A Brand_B Brand_C
0
0.0
0.0
0.0
...
0.0
0.0
0.0
[1 rows x 10 columns]
Что здесь происходит? Помимо библиотек pandas и numpy, мы также им-
портируем класс OneHotEncoder из модуля sklearn.preprocessing. Далее создаем
простой набор данных с тремя категориальными столбцами. В первом спосо-
бе мы воспользовались функцией pd.get_dummies() для преобразования кате-
гориальных переменных в индикаторы, или фиктивные переменные. С по-
мощью параметра prefix можно задать префиксы для имен новых столбцов.
Во втором способе мы сделали то же самое с помощью класса OneHotEncoder,
передав ему при инициализации аргументы sparse_output=False для полу-
чения плотной матрицы и handle_unknown='ignore' для игнорирования новых
категорий во время преобразования. Само преобразование мы выполнили
с помощью метода fit_transform(), после чего посредством метода get_fea-
ture_names_out() извлекли имена новых столбцов и создали датафрейм. В за-
ключение мы показали, как класс OneHotEncoder обрабатывает новые катего-
рии. Для этого мы создали новый датафрейм с наблюдением, содержащим
категории, не использовавшиеся при обучении, и применили к нему метод
transform(). В результате для этих категорий мы получили наши новые столб-
цы, заполненные нулями, что позволит избежать появления ошибок во время
предсказаний.
6.1.1. Совет 1: избегайте ловушки, связанной
с фиктивными переменными
При использовании кодирования с одним активным состоянием аналитики
часто попадаются на крючок, связанный с лишним фиктивным столбцом.
Эта ловушка обусловлена добавлением в набор данных индикаторов по всем
без исключения категориям, что ведет к появлению мультиколлинеарности,
т. е. ситуации, когда два или более признаков в наборе данных сильно корре-
лируют друг с другом. Фактически же при наличии n уникальных категорий
в столбце вам достаточно добавить в набор новые столбцы в количестве n – 1 ,
поскольку значения последнего столбца однозначно выводятся из значений
остальных.
К примеру, в случае с категориальным столбцом Color с уникальными зна-
чениями Red, Blue и Green вам будет достаточно добавить в набор индикаторы
Кодирование с одним активным состоянием: углубленное изучение 295
Is_Red и Is_Blue, чтобы восстановить полную информацию. Зеленый цвет
однозначно выводится при наличии нулей в столбцах Is_Red и Is_Blue.
Такая избыточность может приводить к серьезным проблемам при ис-
пользовании статистических моделей и алгоритмов машинного обучения:
мультиколлинеарность в линейных моделях: может сделать модель
неустойчивой и затруднить ее интерпретируемость вследствие нали-
чия лишних коэффициентов;
переобучение: дополнительные столбцы не несут новой информации,
но при этом усложняют модель, что может вести к ее переобучению;
вычислительные проблемы: добавление необязательных столбцов
влияет на размерность модели и может негативно сказаться на време-
ни ее обучения и объеме используемых ресурсов.
Решение: удаление лишних столбцов
Во избежание перечисленных выше проблем принято избавляться от одного
избыточного столбца при кодировании каждой категориальной переменной.
Это позволит модели избавиться от пагубной мультиколлинеарности, сохра-
нив при этом всю исходную информацию.
Большинство современных библиотек, включая pandas и Scikit-learn, пред-
лагают встроенные опции для удаления первого или любого указанного
столбца при кодировании с одним активным состоянием. Пример исполь-
зования такой опции в обеих библиотеках показан ниже:
import pandas as pd
from sklearn.preprocessing import OneHotEncoder
# Простой набор данных
data={
'Color': ['Red', 'Blue', 'Green', 'Blue', 'Red', 'Yellow'],
'Size': ['Small', 'Medium', 'Large', 'Medium', 'Small', 'Large']
}
df = pd.DataFrame(data)
print("Исходный набор данных:")
print(df)
print("\n")
# Способ 1: с использованием функции get_dummies()
df_one _hot_pd = pd.get_dummies(df, columns=['Color'], drop_first=True, prefix='Color')
print("Кодирование с помощью библиотеки pandas с drop_first=True:")
print(df_one _hot_pd)
print("\n")
# Способ 2: с помощью класса OneHotEncoder из библиотеки sklearn
encoder = OneHotEncoder(drop='first', sparse_output=False)
encoded_features = encoder.fit_transform(df[['Color']])
# Создаем датафрейм с закодированными именами столбцов
feature_names = encoder.get_feature_names _out(['Color'])
296 Кодирование категориальных переменных
df_one _hot_sk = pd.DataFrame(encoded_features, columns=feature_names)
# Объединение с исходным столбцом Size
df_one _hot_sk = pd.concat([df['Size'], df_one _hot_sk], axis=1)
print("Кодирование с помощью библиотеки sklearn и drop='first':")
print(df_one _hot_sk)
Вывод:
Исходный набор данных:
Color Size
0
Red Small
1 Blue Medium
2 Green Large
3 Blue Medium
4
Red Small
5 Yellow Large
Кодирование с помощью библиотеки pandas с drop_first=True:
Size Color_Green Color_Red Color_Yellow
0 Small
False
True
False
1 Medium
False
False
False
2 Large
True
False
False
3 Medium
False
False
False
4 Small
False
True
False
5 Large
False
False
True
Кодирование с помощью библиотеки sklearn и drop='first':
Size Color_Green Color_Red Color_Yellow
0 Small
0.0
1.0
0.0
1 Medium
0.0
0.0
0.0
2 Large
1.0
0.0
0.0
3 Medium
0.0
0.0
0.0
4 Small
0.0
1.0
0.0
5 Large
0.0
0.0
1.0
Здесь мы создали датафрейм со столбцами Color и Size и применили ко-
дирование только к первому столбцу. Для удаления первого столбца с зако-
дированными значениями при использовании функции pd.get_dummies() мы
передали ей аргумент drop_first=True, а при использовании класса OneHotEn-
coder добавили параметр инициализации drop='first'. Заметьте, что во вто-
ром способе для получения итогового результата нам пришлось объединить
новый датафрейм с исходным столбцом Size.
6.1.2. Совет 2: правильно кодируйте значения
в столбцах с высокой кардинальностью
При работе с категориальными столбцами, насчитывающими большое коли-
чество уникальных значений (их еще называют столбцами с высокой карди-
нальностью (cardinality)), кодирование с одним активным состоянием может
Кодирование с одним активным состоянием: углубленное изучение 297
приводить к образованию большого количества дополнительных столбцов
в наборе данных и, как следствие, к увеличению времени обучения модели
и нежелательному росту ее сложности. К примеру, если в вашем наборе есть
столбец City с сотнями уникальных городов, после применения кодирования
с одним активным состоянием в нем добавится столько же фиктивных би-
нарных столбцов, что может стать причиной следующих проблем:
увеличение размерности: использование этого типа кодирования
очень быстро может привести к возникновению проклятия размер-
ности;
увеличение времени обучения: чем больше столбцов в модели, тем
дольше она будет обучаться;
переобучение: при наличии чрезмерно большого количества столб-
цов модель может начать обучаться на шуме, а не на существующих
шаблонах в данных;
повышенный расход ресурсов: работа с объемными разреженными
матрицами может требовать больших ресурсов в отношении памяти.
Для решения этих проблем мы можем пойти разными путями.
Решение 1: группировка признаков
При работе с категориальными столбцами с высокой кардинальностью вы
можете разбить множество уникальных категорий на небольшое количество
групп. В нашем примере с городами вы могли бы сгруппировать их по регио-
нам или по численности населения. У этого подхода есть сразу несколько
преимуществ:
позволяет сократить размерность с сохранением важной информации;
дает возможность применить знания о предметной области для объ-
единения категорий в группы;
делает модель более устойчивой к редким или новым категориям.
Рассмотрим пример с группировкой городов по численности населения:
import pandas as pd
import numpy as np
# Простой набор данных с категориальной переменной с высокой кардинальностью
data={
'City': ['New York', 'Los Angeles', 'Chicago', 'Houston', 'Phoenix', 'Philadelphia',
'San Antonio', 'San Diego', 'Dallas', 'San Jose', 'Austin', 'Jacksonville'],
'Population': [8336817, 3898747, 2746388, 2304580, 1608139, 1603797, 1434625,
1386932, 1304379, 1013240, 961855, 911507]
}
df = pd.DataFrame(data)
# Определяем функцию для разделения городов на категории по численности населения
def group_cities(population):
if population > 3000000:
return 'Mega City'
298 Кодирование категориальных переменных
elif population > 1200000:
return 'Medium City'
else:
return 'Small City'
# Применяем группирующую функцию
df['City_Group'] = df['Population'].apply(group_cities)
# Применяем кодирование с одним активным состоянием к сгруппированному столбцу
df_encoded = pd.get_dummies(df, columns=['City_Group'], prefix='CityGroup')
print(df_encoded)
Вывод:
City Population CityGroup_Medium City CityGroup_Mega City CityGroup_Small City
0
New York
8336817
False
True
False
1 Los Angeles
3898747
False
True
False
2
Chicago
2746388
True
False
False
3
Houston
2304580
True
False
False
4
Phoenix
1608139
True
False
False
5 Philadelphia
1603797
True
False
False
6 San Antonio
1434625
True
False
False
7
San Diego
1386932
True
False
False
8
Dallas
1304379
True
False
False
9
San Jose
1013240
False
False
True
10
Austin
961855
False
False
True
11 Jacksonville
911507
False
False
True
В нашем наборе данных 12 городов с разной численностью населения. Мы
написали дополнительную функцию, возвращающую название категории
исходя из населения города, после чего создали новый столбец City_Group,
применив наше преобразование к столбцу Population посредством метода ap-
ply(). В результате мы применили функцию pd.get_dummies() не к исходному
столбцу с городами, а к новому столбцу с категориями городов по числен-
ности населения, кардинальность которого значительно ниже со всеми вы-
текающими плюсами.
Этот подход позволил нам сократить количество дополнительных столб-
цов в наборе данных с 12 до четырех без потери значимой информации. Вы
можете использовать любые критерии для группировки категорий в зависи-
мости от вашей предметной области.
Решение 2: кодирование на основе частоты
Еще одним методом для работы с категориальными столбцами с высокой
кардинальностью является кодирование на основе частоты (frequency encod-
ing), при котором каждое значение заменяется на количество его появлений
в столбце. Этот метод обладает следующими достоинствами:
снижение размерности: вместо потенциально огромного количества
новых столбцов при использовании кодирования с одним активным
состоянием мы получили всего один дополнительный столбец;
Кодирование с одним активным состоянием: углубленное изучение 299
сохранение важной информации: частота встречаемости категорий
часто несет полезную информацию для модели;
обработка новых категорий: для новых категорий в тестовых данных
вы можете задать частоту по умолчанию (к примеру, ноль или среднюю
частоту).
В то же время необходимо помнить, что использование этого подхода
предполагает, что частота встречаемости категории напрямую связана с ее
значимостью в предсказании целевой переменной, что не всегда соответ-
ствует действительности.
Пример применения кодирования на основе частоты:
import pandas as pd
# Простой набор данных с категориальной переменной с высокой кардинальностью
data={
'City': ['New York', 'Los Angeles', 'Chicago', 'New York', 'Houston', 'Los Angeles',
'Chicago', 'Phoenix', 'Philadelphia', 'San Antonio', 'San Diego', 'Dallas']
}
df = pd.DataFrame(data)
# Рассчитываем частоту встречаемости каждой категории
frequency = df['City'].value_counts(normalize=True)
# Выполняем кодирование на основе частоты
df['City_Frequency'] = df['City'].map(frequency)
# Выводим результаты кодирования
print(df)
Вывод:
City City_Frequency
0
New York
0.166667
1 Los Angeles
0.083333
2
Chicago
0.166667
3
New York
0.166667
4
Houston
0.083333
5 Los Angeles
0.083333
6
Chicago
0.166667
7
Phoenix
0.083333
8 Philadelphia
0.083333
9 San Antonio
0.083333
10 San Diego
0.083333
11
Dallas
0.083333
Здесь мы при помощи метода value_counts() рассчитали относитель-
ную частоту встречаемости каждого города в наборе данных. Параметр
normalize=True отвечает за получение долей вместо абсолютного количества.
Далее мы при помощи инструкции df['City'].map(frequency) создали новый
столбец с относительной частотой встречаемости городов. Чем чаще город
встречается в исходном столбце, тем больше будет значение для него в ко-
лонке City_Frequency. В результате мы на основе категориального столбца
300 Кодирование категориальных переменных
City с высоким показателем кардинальности получили один дополнитель-
ный столбец с новой важной информацией, который можем использовать
в алгоритмах машинного обучения.
Решение 3: кодирование на основе целевой переменной
Кодирование на основе целевой переменной (target encoding), также называе-
мое кодированием по среднему (mean encoding) или целевероятностным коди-
рованием (likelihood encoding), представляет собой технику, использующуюся
для замены названия категории на среднее значение целевой переменной
по этой категории. Этот метод может быть особенно полезен при наличии
тесной взаимосвязи между категориальным предиктором и целевой пере-
менной. Работает эта техника в два этапа.
1. Для каждой уникальной категории в столбце рассчитывается среднее
значение целевой переменной.
2. Названия категорий заменяются на эти средние значения.
К примеру, если вы предсказываете рыночную стоимость домов, то можете
заменить категориальные значения в столбце Neighborhood, отвечающем за
соседство дома с важными объектами городской инфраструктуры, на сред-
нюю стоимость домов с такими же объектами неподалеку.
Основными достоинствами этого метода являются следующие:
способность обнаруживать сложные зависимости между категориями
и целевой переменной;
возможность эффективно обрабатывать категориальные переменные
с высокой кардинальностью;
потенциально высокое качество моделей, особенно основанных на де-
ревьях.
К рискам кодирования на основе целевой переменной относятся:
переобучение: ошибки в реализации могут привести к утечке инфор-
мации;
излишняя чувствительность к выбросам в целевой переменной;
возможная погрешность модели в случае, если закодированные значе-
ния должным образом не регуляризованы.
Для снижения этих рисков применяются следующие техники:
k-блочная кросс-валидация: кодирование данных с использованием
предсказаний на отложенной выборке;
сглаживание: добавление регуляризации для баланса средних по кате-
гориям с общим средним;
кодирование с исключением: вычисление среднего для категории по
всем наблюдениям, за исключением текущего.
Пример применения кодирования на основе целевой переменной:
import pandas as pd
import numpy as np
Кодирование с одним активным состоянием: углубленное изучение 301
from sklearn.model_selection import KFold
# Простой набор данных
data={
'Neighborhood': ['A', 'B', 'C', 'A', 'B', 'C', 'A', 'B', 'C', 'A'],
'Price': [100, 150, 200, 120, 160, 220, 110, 140, 190, 130]
}
df = pd.DataFrame(data)
# Функция для применения кодирования на основе целевой переменной
def target_encode(df, target_col, encode_col, n_splits=5):
# Создаем новый столбец для закодированных значений
df[f'{encode_col}_encoded'] = np.nan
# Подготавливаем кросс-валидацию KFold
kf = KFold(n_splits=n_splits, shuffle=True, random_state=42)
# Выполняем кодирование на основе целевой переменной
for train_idx, val_idx in kf.split(df):
# Рассчитываем среднее по целевой переменной для каждой категории в обучающей
выборке
target_means = df.iloc[train_idx].groupby(encode_col)[target_col].mean()
# Кодируем на валидационном блоке
df.loc[val_idx, f'{encode_col}_encoded'] = df.loc[val_idx, encode_col].map(target_
means)
# Обрабатываем пропущенные значения (для категорий, не участвовавших в обучении)
overall_mean = df[target_col].mean()
df[f'{encode_col}_encoded'] = df[f'{encode_col}_encoded'].fillna(overall_mean)
return df
# Применяем кодирование на основе целевой переменной
encoded_df = target_encode(df, 'Price', 'Neighborhood')
print(encoded_df)
Вывод:
Neighborhood Price Neighborhood_encoded
0
A 100
120.0
1
B 150
150.0
2
C 200
205.0
3
A 120
115.0
4
B 160
145.0
5
C 220
195.0
6
A 110
115.0
7
B 140
155.0
8
C 190
210.0
9
A 130
110.0
Здесь мы, помимо традиционных библиотек, импортировали класс KFold
из модуля sklearn.model_selection. Затем создали простой набор данных с ка-
тегориальной переменной Neighborhood и целевой переменной Price. После
этого объявили функцию target_encode, принимающую в качестве парамет-
ров датафрейм, целевую переменную, переменную для кодирования и ко-
личество блоков кросс-валидации. В этой функции происходит следующее:
302 Кодирование категориальных переменных
создается столбец в датафрейме для хранения закодированных зна-
чений;
создается экземпляр класса KFold для выполнения кросс-валидации
с исключением риска утечки информации;
в цикле по выборкам рассчитываются средние значения целевой пере-
менной для каждой категории с использованием обучающих данных,
после чего сопоставляются с соответствующими категориями в вали-
дационном блоке;
пустые значения (для категорий, не участвовавших в обучении) заме-
няются на среднее значение целевой переменной.
В заключение функция target_encode применяется к исходному датафрей-
му, и на экран выводится датафрейм с закодированными значениями.
В этой реализации кодирования на основе целевой переменной приме-
няется кросс-валидация, что позволяет снизить риск переобучения. Значе-
ния для кодирования рассчитываются с использованием данных из других
блоков, а это обеспечивает гарантию того, что при кодировании не будут
использоваться целевые значения для этого же наблюдения.
Стоит отметить, что технику кодирования на основе целевой переменной
необходимо применять очень осмотрительно, особенно при работе с неболь-
шими наборами данных или при наличии риска утечки информации. Всегда
оценивайте эффективность этого метода исходя из конкретной задачи.
Решение 4: техники снижения размерности
После применения кодирования с одним активным состоянием вы мо-
жете воспользоваться одной из техник снижения размерности, например
анализом главных компонент (PCA) или методом нелинейного снижения
размерности и визуализации многомерных переменных (t-SNE), с целью
уменьшения количества признаков с сохранением большей части ценной
информации. Эти техники бывают особенно полезны при работе с данными
большой размерности, которые мы получаем в результате применения ко-
дирования с одним активным состоянием к категориальным переменным
с большим количеством уникальных значений.
Анализ главных компонент (Principal Component Analysis – PCA) представ-
ляет собой линейную технику снижения размерности, заключающуюся в вы-
делении так называемых главных компонент (principal component) в данных,
соответствующих направлениям, характеризующимся наибольшей диспер-
сией. Выбрав подмножество таких компонент, вы сможете существенно сни-
зить количество признаков в наборе данных, сохранив при этом большую
часть дисперсии в данных. Это может помочь избежать проклятия размер-
ности и повысить качество будущей модели.
Метод нелинейного снижения размерности и визуализации многомерных
переменных (t-Distributed Stochastic Neighbor Embedding – t-SNE) является
нелинейной техникой снижения размерности, которая бывает особенно по-
лезна для визуализации многомерных данных в двух или трех измерениях.
Кодирование с одним активным состоянием: углубленное изучение 303
В процессе применения этой техники сохраняется исходная структура дан-
ных, и в то же время она позволяет выделить в данных кластеры или шабло-
ны, которые могут быть незаметны в многомерном пространстве.
Советы при применении техник снижения размерности после кодирова-
ния с одним активным состоянием:
убедитесь, что вы должным образом масштабировали данные перед
применением техник PCA или t-SNE, поскольку они очень чувствитель-
ны к масштабу входных признаков;
для применения метода PCA определитесь с накопительной долей дис-
персии, которая должна объясняться главными компонентами. Обычно
в наборе данных оставляют достаточно компонент, чтобы они объяс-
няли порядка 85–95 % дисперсии;
помните, что метод t-SNE главным образом нацелен на визуализацию
и описание данных, а не на создание признаков для будущей модели;
не забывайте, что при всей своей эффективности методы снижения
размерности могут приводить к ухудшению интерпретируемости при-
знаков по сравнению с исходным набором переменных, полученным
в результате кодирования с одним активным состоянием.
Комбинируя кодирование с одним активным состоянием с методами сни-
жения размерности, вы часто будете находить нужный компромисс между
объемом ценной информации и количеством итоговых признаков для мо-
дели.
Пример применения анализа главных компонент после кодирования с од-
ним активным состоянием:
import pandas as pd
import numpy as np
from sklearn.preprocessing import OneHotEncoder
from sklearn.decomposition import PCA
from sklearn.compose import ColumnTransformer
# Простой набор данных
data={
'Color': ['Red', 'Blue', 'Green', 'Blue', 'Red', 'Green', 'Blue', 'Red'],
'Size': ['Small', 'Medium', 'Large', 'Medium', 'Small', 'Large', 'Small', 'Medium'],
'Price': [10, 15, 20, 14, 11, 22, 13, 16]
}
df = pd.DataFrame(data)
# Шаг 1: кодирование с одним активным состоянием
ct = ColumnTransformer([
('encoder', OneHotEncoder(drop='first', sparse_output=False), ['Color', 'Size'])
], remainder='passthrough')
X = ct.fit_transform(df)
# Шаг 2: применение PCA
pca = PCA(n_components=0.95) # Сохраняем 95% дисперсии
X_pca = pca.fit_transform(X)
304 Кодирование категориальных переменных
# Вывод результатов
print("Исходная размерность:", X.shape)
print("Размерность после применения PCA:", X_pca.shape)
print("Доля объясненной дисперсии:", pca.explained_variance_ratio_)
Вывод:
Исходная размерность: (8, 5)
Размерность после применения PCA: (8, 1)
Доля объясненной дисперсии: [0.96624324]
Здесь мы, помимо прочих библиотек, загрузили класс PCA из модуля
sklearn.decomposition. Далее мы создали простой набор данных с двумя ка-
тегориальными переменными (Color и Size) и одной числовой (Price). После
этого мы применили кодирование с одним активным состоянием к категори-
альным переменным. Для этого мы при инициализации класса OneHotEncoder
передали ему параметры drop='first' с целью удаления одного избыточ-
ного столбца и sparse_output=False для возврата плотной матрицы. Стол-
бец Price мы оставили как есть, передав классу ColumnTransformer параметр
remainder='passthrough'. Затем мы инициализировали класс PCA, передав ему
параметр n_components=0.95 для сохранения необходимого количества ком-
понент, которые смогут объяснить 95 % дисперсии в данных. В заключение
вывели для сравнения форму исходных и преобразованных данных, а также
долю объясненной дисперсии.
Важные аспекты этого процесса:
мы сначала увеличили размерность набора данных, применив коди-
рование с одним активным состоянием к категориальным перемен-
ным, а затем уменьшили ее, воспользовавшись анализом главных ком-
понент, что могло позволить нам обнаружить сложные зависимости
между категориями;
значение параметра n_components, переданного при инициализации
класса PCA, было установлено в 0.95, тем самым мы выразили желание
сохранить столько главных компонент, сколько будет необходимо для
объяснения как минимум 95 % дисперсии в данных. Это типичное зна-
чение порога, но вы можете менять его при необходимости;
результирующие признаки (главные компоненты) представляют со-
бой линейные комбинации исходных переменных, полученных после
применения кодирования с одним активным состоянием, что может
означать их более слабую интерпретируемость, но большую информа-
тивность с точки зрения моделей машинного обучения;
этот метод может быть особенно эффективен при наличии в наборе
данных большого количества категориальных переменных или кате-
горий, поскольку он способен существенно снижать размерность, со-
храняя при этом большую часть исходной информации о данных.
Помните о необходимости масштабировать числовые признаки перед при-
менением анализа главных компонент, если они обладают разными масшта-
бами. В нашем примере у нас была только одна числовая переменная Price,
Кодирование с одним активным состоянием: углубленное изучение 305
так что масштабирование нам не понадобилось. Но в реальных примерах со
множеством числовых переменных вы всегда должны включать в конвейер
шаг с масштабированием данных.
Выбор между этими техниками можно сделать на основе набора данных,
природы категориальных переменных и выбранного алгоритма машинного
обучения. Зачастую комбинация этих техник способна дать наилучший ре-
зультат.
6.1.3. Совет 3: используйте разреженные матрицы
для повышения эффективности
При работе с большими наборами данных или категориальными перемен-
ными, насчитывающими большое количество уникальных значений, при-
менение кодирования с одним активным состоянием может приводить к об-
разованию сильно разреженных матриц, т. е. матриц, содержащих большое
количество нулевых значений. И хотя с точки зрения представленных дан-
ных в таких матрицах все может быть в порядке, в плане работы с памятью
и использования вычислительных ресурсов они могут доставлять опреде-
ленные проблемы.
Снижение производительности связано с тем, что традиционные плот-
ные, или заполненные, матрицы хранят все значения, включая нулевые.
Это может приводить к быстрому расходованию памяти, когда переменных
в наборе данных чрезвычайно много. К тому же и вычисления над такими
матрицами бывает производить довольно трудозатратно.
Решение: использование разреженных матриц
Для решения этой проблемы вы можете при использовании класса OneHotEn-
coder заявить о желании работать с объектом в виде разреженной матрицы
(sparse matrix). Разреженные матрицы представляют собой особую структу-
ру данных, оптимизированную для хранения объемных массивов данных,
содержащих множество нулевых значений. Технически это достигается за
счет хранения только ненулевых элементов матрицы с занимаемыми ими
позициями.
Преимущества использования таких структур следующие:
существенная экономия памяти: хранение только ненулевых значений
позволяет серьезно снизить требования к ресурсам, особенно при ра-
боте с большими массивами;
повышение скорости вычислений: многие операции линейной алгеб-
ры с разреженными матрицами выполняются более эффективно, по-
скольку задействуют только ненулевые значения;
масштабируемость: разреженные матрицы позволяют работать с до-
статочно большими массивами данных, практически неподъемными
для обычных плотных матриц.
306 Кодирование категориальных переменных
Ниже приведен пример работы с разреженными матрицами:
import pandas as pd
import numpy as np
from sklearn.preprocessing import OneHotEncoder
# Простой набор данных
data={
'Color': ['Red', 'Blue', 'Green', 'Blue', 'Red', 'Yellow', 'Green', 'Blue'],
'Size': ['Small', 'Medium', 'Large', 'Medium', 'Small', 'Large', 'Medium', 'Small']
}
df = pd.DataFrame(data)
# Инициализируем класс OneHotEncoder с выводом в виде разреженной матрицы
encoder = OneHotEncoder(sparse_output=True, drop='first')
# Применяем кодирование с одним активным состоянием и преобразовываем вывод в разреженную
матрицу
sparse_matrix = encoder.fit_transform(df)
# Выводим разреженную матрицу
print("Разреженная матрица:")
print(sparse_matrix)
# Извлекаем имена признаков
feature_names = encoder.get_feature_names _out(['Color', 'Size'])
print("\nИмена признаков:")
print(feature_names)
# Преобразуем разреженную матрицу в плотный массив
dense_array = sparse_matrix.toarray()
print("\nПлотный массив:")
print(dense_array)
# Создаем датафрейм на основе плотного массива
encoded_df = pd.DataFrame(dense_array, columns=feature_names)
print("\nЗакодированный датафрейм:")
print(encoded_df)
# Использование памяти
print("\nИспользование памяти:")
print(f"Разреженная матрица: {sparse_matrix.data.nbytes + sparse_matrix.indptr.nbytes +
sparse_matrix.indices.nbytes} bytes")
print(f"Плотный массив: {dense_array.nbytes} bytes")
# Выполняем операции над разреженной матрицей
print("\nСумма по всем признакам:")
print(np.asarray(sparse_matrix.sum(axis=0)).flatten())
# Обратное преобразование
original_data = encoder.inverse_transform(sparse_matrix)
print("\nДанные после обратного преобразования:")
print(pd.DataFrame(original_data, columns=['Color', 'Size']))
Кодирование с одним активным состоянием: углубленное изучение 307
Вывод:
Разреженная матрица:
<Compressed Sparse Row sparse matrix of dtype 'float64'
with 11 stored elements and shape (8, 5)>
Coords
Values
(0, 1)
1.0
(0, 4)
1.0
(1, 3)
1.0
(2, 0)
1.0
(3, 3)
1.0
(4, 1)
1.0
(4, 4)
1.0
(5, 2)
1.0
(6, 0)
1.0
(6, 3)
1.0
(7, 4)
1.0
Имена признаков:
['Color_Green' 'Color_Red' 'Color_Yellow' 'Size_Medium' 'Size_Small']
Плотный массив:
[[0. 1. 0. 0. 1.]
[0.0.0.1.0.]
[1.0.0.0.0.]
[0.0.0.1.0.]
[0.1.0.0.1.]
[0.0.1.0.0.]
[1.0.0.1.0.]
[0. 0. 0. 0. 1.]]
Закодированный датафрейм:
Color_Green Color_Red Color_Yellow Size_Medium Size_Small
0
0.0
1.0
0.0
0.0
1.0
1
0.0
0.0
0.0
1.0
0.0
2
1.0
0.0
0.0
0.0
0.0
3
0.0
0.0
0.0
1.0
0.0
4
0.0
1.0
0.0
0.0
1.0
5
0.0
0.0
1.0
0.0
0.0
6
1.0
0.0
0.0
1.0
0.0
7
0.0
0.0
0.0
0.0
1.0
Использование памяти:
Разреженная матрица: 168 bytes
Плотный массив: 320 bytes
Сумма по всем признакам:
[2.2.1.3.3.]
Данные после обратного преобразования:
Color Size
0
Red Small
1 Blue Medium
2 Green Large
308 Кодирование категориальных переменных
3 Blue Medium
4
Red Small
5 Yellow Large
6 Green Medium
7 Blue Small
Здесь мы создали простой набор данных с двумя категориальными пере-
менными (Color и Size). При инициализации класса OneHotEncoder мы при
помощи параметра sparse_output=True указали, что результат хотели бы по-
лучить в виде разреженной матрицы. Далее вывели полученную матрицу на
экран. Как видите, в ней оказалось всего 11 ненулевых значений, притом что
форма матрицы соответствует (8, 5). Далее мы при помощи метода get_fea-
ture_names_out() извлекли имена всех столбцов, а также преобразовали нашу
разреженную матрицу в плотный массив при помощи метода toarray(). Этот
шаг иногда требуется для совместимости с некоторыми алгоритмами ма-
шинного обучения. После этого мы создали датафрейм на основе плотного
массива с использованием полученных имен столбцов.
Затем мы сравнили занимаемую память разреженной матрицей и плот-
ным массивом. Превосходство разреженной матрицы оказалось двукратным.
Далее мы выполнили операцию суммирования по столбцам над разреженной
матрицей, после чего с помощью метода inverse_transform() преобразовали
закодированные данные обратно в формат категорий. Это бывает полезно
в целях интерпретации результатов или выполнения проверки операции
кодирования.
6.1.4. Выводы и рекомендации
При работе с категориальными переменными стоит помнить о следующем:
кодирование с одним активным состоянием является одной из ключе-
вых техник для работы с категориальными переменными при исполь-
зовании алгоритмов машинного обучения. В то же время необходимо
тщательно продумывать процесс преобразования таких переменных,
чтобы модель не утратила целостность и вычислительную эффектив-
ность;
при использовании кодирования с одним активным состоянием всегда
стоит удалять избыточные столбцы из модели. Это позволит избежать
мультиколлинеарности и облегчит интерпретирование коэффициен-
тов модели;
категориальные переменные с высокой кардинальностью представ-
ляют серьезную проблему для метода кодирования с одним активным
состоянием в связи с риском появления большого количества дополни-
тельных столбцов в наборе данных. Одной из альтернатив в этом случае
может стать кодирование на основе частоты. Этот метод позволяет не
только снизить количество признаков, но и извлечь зачастую полезную
для модели информацию о распределении категорий в переменной;
Более сложные примеры применения кодирования на основе целевой переменной 309
еще одной альтернативой при работе с признаками с высокой кар-
динальностью является метод группировки с объединением в общую
группу категорий с частотой ниже определенного порога, который
определяется исходя из конкретной задачи;
при кодировании с одним активным состоянием подумайте об опти-
мизации за счет использования разреженных матриц. Хранение только
ненулевых значений с их координатами позволяет значительно сокра-
тить расход памяти и повысить вычислительную эффективность при
использовании подобных структур;
выбор способа преобразования столбцов с категориальными данными
должен зависеть от конкретного набора данных и его характеристик.
К примеру, вы можете применить кодирование с одним активным со-
стоянием к столбцам с низкой кардинальностью, а для столбцов с вы-
сокой кардинальностью оставить метод преобразования на основе
частоты;
при выборе метода преобразования категориальных переменных всег-
да принимайте в расчет интерпретируемость будущей модели. Если
кодирование с одним активным состоянием сохраняет информацию
о важности категорий, более сложные техники могут скрывать прямые
зависимости между исходными категориями и результатами модели.
Всегда выбирайте наиболее оптимальный с точки зрения бизнес-за-
дачи компромисс между качеством модели и интерпретируемостью
ее выводов.
6.2. Более сложные примеры
применения кодирования на основе
целевой переменной, частоты
и порядкового кодирования
Как вы уже знаете, кодирование с одним активным состоянием для кате-
гориальных переменных далеко не всегда может быть оптимальным вы-
бором, особенно когда мы говорим о работе с объемными наборами данных
и переменных с высокой кардинальностью. В таких сценариях гораздо лучше
могут подходить техники кодирования на основе целевой переменной или
частоты, а также порядковое кодирование. Суть первых двух техник мы уже
описывали выше, а порядковое кодирование применяется в случаях, когда
между категориями есть естественный порядок и зависимость. В отличие от
способа кодирования с одним активным состоянием, не делающего разли-
чий между категориями, порядковое кодирование присваивает категориям
числовые значения, отражающие некий их порядок. Этот метод хорошо под-
310 Кодирование категориальных переменных
ходит для категориальных переменных, содержащих уровни образования,
рейтинги товаров и т. д ., где порядок имеет значение.
6.2.1. Кодирование на основе целевой переменной
с регуляризацией и без
Давайте рассмотрим два похожих примера применения кодирования на ос-
нове целевой переменной. В первом примере мы будем кодировать катего-
риальную переменную средними значениями по соответствующим катего-
риям:
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
from sklearn.linear_model import LinearRegression
np.random.seed(42)
# Простой набор данных
data={
'Neighborhood': ['A', 'B', 'A', 'C', 'B', 'A', 'C', 'B', 'D', 'D'] * 10,
'SalePrice': np.random.randint(200000, 600000, 100)
}
df = pd.DataFrame(data)
# Разбиваем исходные данные на обучающую и тестовую выборки
train, test = train_test_split(df, test_size=0.2, random_state=42)
# Функция для выполнения порядкового кодирования
def target_encode(train, test, column, target):
# Рассчитываем общее среднее
global_mean = train[target].mean()
# Рассчитываем среднее значение целевой переменной для каждой категории
category_means = train.groupby(column)[target].agg(['mean'])
# Применяем кодирование к обучающей выборке
train_encoded = train[column].map(category_means['mean'])
# Применяем кодирование к тестовой выборке
test_encoded = test[column].map(category_means['mean'])
# Обрабатываем неизвестные категории в тестовой выборке
test_encoded = test_encoded.fillna(global_mean)
return train_encoded, test_encoded
# Применяем порядковое кодирование
train['NeighborhoodEncoded'], test['NeighborhoodEncoded'] = target_encode(train, test,
'Neighborhood', 'SalePrice')
Более сложные примеры применения кодирования на основе целевой переменной 311
# Выводим датафреймы с кодированием
print("Обучающие данные:")
print(train.head())
print("\nТестовые данные:")
print(test.head())
# Демонстрируем влияние кодирования на простую модель
# Модель с исходными категориальными данными (кодирование с одним активным состоянием)
model_orig = LinearRegression()
model_orig.fit(pd.get_dummies(train['Neighborhood']), train['SalePrice'])
pred_orig = model_orig.predict(pd.get_dummies(test['Neighborhood']))
mse_orig = mean_squared_error(test['SalePrice'], pred_orig)
# Модель с закодированными категориальными данными (порядковое кодирование)
model_encoded = LinearRegression()
model_encoded.fit(train[['NeighborhoodEncoded']], train['SalePrice'])
pred_encoded = model_encoded.predict(test[['NeighborhoodEncoded']])
mse_encoded = mean_squared_error(test['SalePrice'], pred_encoded)
print(f"\nMSE на исходных данных (кодирование с одним активным состоянием): {mse_orig}")
print(f"MSE на закодированных данных (порядковое кодирование): {mse_encoded}")
Вывод:
Обучающие данные:
Neighborhood SalePrice NeighborhoodEncoded
55
A
341699
422362.476190
88
D
299299
368878.000000
26
C
303355
409195.333333
42
A
284654
422362.476190
69
D
248555
368878.000000
Тестовые данные:
Neighborhood SalePrice NeighborhoodEncoded
83
C
471836
409195.333333
53
C
456840
409195.333333
70
A
456508
422362.476190
45
A
519030
422362.476190
44
B
267435
393660.730769
MSE на исходных данных (кодирование с одним активным состоянием): 10240419865.499079
MSE на закодированных данных (порядковое кодирование): 10240419865.499079
Здесь мы сгенерировали набор данных из ста наблюдений, содержащий
одну категориальную переменную Neighborhood и одну целевую переменную
SalePrice. После этого мы разбили наш набор на обучающую и тестовую вы-
борки и вызвали написанную нами функцию target_encode(), добавляющую
в наши выборки столбец NeighborhoodEncoded с закодированными значения-
ми на основе средней стоимости продажи домов по конкретной категории.
Далее мы создали две модели линейной регрессии: первую обучили на ка-
тегориальной переменной, закодированной с одним активным состоянием,
а вторую – на той же переменной с кодированием на основе целевой пере-
312 Кодирование категориальных переменных
менной. В заключение мы вывели значения MSE для обеих моделей, которые
оказались одинаковыми.
Ключевые риски кодирования на основе целевой переменной связаны
с потенциальной утечкой информации и переобучением.
1. Утечка информации: вы уже знаете, что под этим термином мы под-
разумеваем просачивание информации о данных из тестовой выборки
в обучающую выборку. Утечка информации может приводить к из-
лишне оптимистичной оценке качества итоговой модели и ослаб-
лению ее обобщающей способности. Во избежание появления этого
эффекта рекомендуется выполнять кодирование на основе целевой
переменной в рамках процедуры кросс-валидации, чтобы информа-
ция для кодирования поступала только из обучающих выборок на каж-
дой итерации.
2. Переобучение: поскольку этот вид кодирования напрямую связан с це-
левой переменной, существует большой риск переобучения модели,
особенно для категорий с небольшим количеством наблюдений. В ре-
зультате модель может начать обучаться на шумах, а не на истинных
шаблонах, присутствующих в данных. Для исключения такой ситуации
можно воспользоваться следующими техниками:
• сглаживание (smoothing): применение регуляризации путем до-
бавления сглаживающего фактора в вычисление значения во вре-
мя кодирования. Это позволит находить баланс между глобальным
средним и средним значением по категории, что поможет снизить
влияние на модель выбросов и редко встречающихся категорий;
• кросс-валидация: использование k-блочной кросс-валидации в про-
цессе кодирования на основе целевой переменной позволит обес-
печить модели высокую надежность и обобщающую способность;
• добавление шума: внесение небольших колебаний случайного ха-
рактера в закодированные значения, чтобы модель не переобуча-
лась;
• применение кросс-валидации по отдельным объектам: вы можете
для каждого наблюдения рассчитывать среднее значение целевой
переменной по всем наблюдениям, кроме текущего, что сократит
риск переобучения на конкретных значениях.
Теперь рассмотрим пример, похожий на предыдущий, но в который до-
бавлена возможность вносить регуляризацию в расчет средних значений по
категориям путем сглаживания:
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
from sklearn.linear_model import LinearRegression
np.random.seed(42)
Более сложные примеры применения кодирования на основе целевой переменной 313
# Простой набор данных
data={
'Neighborhood': ['A', 'B', 'A', 'C', 'B', 'A', 'C', 'B', 'D', 'D'] * 10,
'SalePrice': np.random.randint(200000, 600000, 100)
}
df = pd.DataFrame(data)
# Разбиваем исходные данные на обучающую и тестовую выборки
train, test = train_test_split(df, test_size=0.2, random_state=42)
# Функция для выполнения порядкового кодирования со сглаживанием
def target_encode_smooth(train, test, column, target, alpha=5):
# Рассчитываем общее среднее
global_mean = train[target].mean()
# Рассчитываем среднее значение целевой переменной для каждой категории
category_means = train.groupby(column)[target].agg(['mean', 'count'])
# Применяем сглаживание
smoothed_means = (category_means['mean'] * category_means['count'] + global_mean *
alpha) / (category_means['count'] + alpha)
# Применяем кодирование к обучающей выборке
train_encoded = train[column].map(smoothed_means)
# Применяем кодирование к тестовой выборке
test_encoded = test[column].map(smoothed_means)
# Обрабатываем неизвестные категории в тестовой выборке
test_encoded.fillna(global_mean, inplace=True)
return train_encoded, test_encoded
# Применяем порядковое кодирование со сглаживанием
train['NeighborhoodEncoded'], test['NeighborhoodEncoded'] = target_encode_smooth(train,
test, 'Neighborhood', 'SalePrice', alpha=5)
# Выводим датафреймы с кодированием
print("Обучающие данные:")
print(train[['Neighborhood', 'NeighborhoodEncoded', 'SalePrice']].head())
print("\nТестовые данные:")
print(test[['Neighborhood', 'NeighborhoodEncoded', 'SalePrice']].head())
# Демонстрируем влияние кодирования на простую модель
# Модель с исходными категориальными данными (кодирование с одним активным состоянием)
model_orig = LinearRegression()
model_orig.fit(pd.get_dummies(train['Neighborhood']), train['SalePrice'])
pred_orig = model_orig.predict(pd.get_dummies(test['Neighborhood']))
mse_orig = mean_squared_error(test['SalePrice'], pred_orig)
# Модель с закодированными категориальными данными (порядковое кодирование)
model_encoded = LinearRegression()
model_encoded.fit(train[['NeighborhoodEncoded']], train['SalePrice'])
pred_encoded = model_encoded.predict(test[['NeighborhoodEncoded']])
314 Кодирование категориальных переменных
mse_encoded = mean_squared_error(test['SalePrice'], pred_encoded)
print(f"\nMSE на исходных данных (кодирование с одним активным состоянием): {mse_orig}")
print(f"MSE на закодированных данных (порядковое кодирование): {mse_encoded}")
# Визуализация распределения закодированных значений
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 6))
train.groupby('Neighborhood')['NeighborhoodEncoded'].mean().plot(kind='bar')
plt.title('Средние закодированные значения по Neighborhood')
plt.xlabel('Neighborhood')
plt.ylabel('Закодированное значение')
plt.show()
Вывод:
Обучающие данные:
Neighborhood NeighborhoodEncoded SalePrice
55
A
417779.608173
341699
88
D
375324.426630
299299
26
C
406529.390625
303355
42
A
417779.608173
284654
69
D
375324.426630
248555
Тестовые данные:
Neighborhood NeighborhoodEncoded SalePrice
83
C
406529.390625
471836
53
C
406529.390625
456840
70
A
417779.608173
456508
45
A
417779.608173
519030
44
B
394446.348790
267435
MSE на исходных данных (кодирование с одним активным состоянием): 10240419865.499079
MSE на закодированных данных (порядковое кодирование): 10235013416.040737
Здесь мы в функцию target_encode(), которая теперь называется target_en-
code_smooth(), добавили параметр регуляризации alpha, по умолчанию равный
пяти, который следующим образом влияет на расчет среднего по категории:
smoothed_means = (category_means['mean'] * category_means['count'] + global_mean * alpha) /
(category_means['count'] + alpha)
Как видите, в расчете теперь участвует не только среднее значение по
категории, но также количество наблюдений в конкретной категории, гло-
бальное среднее и добавленный коэффициент сглаживания. Применение
регуляризации позволило снизить значение метрики MSE.
Также мы добавили столбчатую диаграмму (см рис. 6 .1) со средними зако-
дированными значениями для каждой категории, по которым можно судить
о зависимостях между категориями и целевой переменной.
Более сложные примеры применения кодирования на основе целевой переменной 315
Рис. 6.1 Средние закодированные значения по столбцу Neighborhood
6.2.2. Пример использования кодирования
на основе частоты
Как мы уже говорили, кодирование на основе частоты представляет собой
очень мощную технику работы с категориальными переменными, которая
может оказаться особенно полезной при наличии монотонной зависимости
между количеством наблюдений для конкретной категории и значением це-
левой переменной.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
np.random.seed(42)
# Простой набор данных
data={
'City': np.random.choice(['New York', 'Los Angeles', 'Chicago', 'Houston',
'Phoenix'], 1000),
'Customer_Churn': np.random.choice([0, 1], 1000)
}
df = pd.DataFrame(data)
# Разбиваем исходные данные на обучающую и тестовую выборки
train, test = train_test_split(df, test_size=0.2, random_state=42)
316 Кодирование категориальных переменных
# Применяем кодирование на основе частоты к обучающей выборке
train['City_Frequency'] = train.groupby('City')['City'].transform('count')
# Нормализуем частоту
train['City_Frequency_Normalized'] = train['City_Frequency'] / len(train)
# Применяем кодирование на основе частоты к тестовой выборке
city_freq = train.groupby('City')['City_Frequency'].first()
test['City_Frequency'] = test['City'].map(city_freq).fillna(0)
test['City_Frequency_Normalized'] = test['City_Frequency'] / len(train)
# Выводим закодированные датафреймы
print("Обучающие данные:")
print(train.head())
print("\nТестовые данные:")
print(test.head())
# Визуализируем распределение частоты
plt.figure(figsize=(10, 6))
train['City'].value_counts().plot(kind='bar')
plt.title('Частота встречаемости городов в обучающих данных')
plt.xlabel('Город')
plt.ylabel('Частота')
plt.show()
# Обучаем простую модель логистической регрессии
model = LogisticRegression()
model.fit(train[['City_Frequency_Normalized']], train['Customer_Churn'])
# Делаем предсказания
train_pred = model.predict(train[['City_Frequency_Normalized']])
test_pred = model.predict(test[['City_Frequency_Normalized']])
# Оцениваем качество модели
print(f"\nТочность предсказаний на обучающей выборке: {accuracy_score(train['Customer_
Churn'], train_pred):.4f}")
print(f"Точность предсказаний на тестовой выборке: {accuracy_score(test['Customer_Churn'],
test_pred):.4f}")
# Сравниваем с кодированием с одним активным состоянием
train_onehot = pd.get_dummies(train['City'], prefix='City')
test_onehot = pd.get_dummies(test['City'], prefix='City')
# Убедимся, что в тестовом наборе содержатся все столбцы из обучающей выборки
for col in train_onehot.columns:
if col not in test_onehot.columns:
test_onehot[col] = 0
test_onehot = test_onehot[train_onehot.columns]
# Обучаем и оцениваем качество модели с кодированием с одним активным состоянием
model_onehot = LogisticRegression()
model_onehot.fit(train_onehot, train['Customer_Churn'])
train_pred_onehot = model_onehot.predict(train_onehot)
test_pred_onehot = model_onehot.predict(test_onehot)
Более сложные примеры применения кодирования на основе целевой переменной 317
print(f"\nТочность предсказаний на обучающей выборке (кодирование с одним активным
состоянием): {accuracy_score(train['Customer_Churn'], train_pred_onehot):.4f}")
print(f"Точность предсказаний на тестовой выборке (кодирование с одним активным
состоянием): {accuracy_score(test['Customer_Churn'], test_pred_onehot):.4f}")
Вывод:
Обучающие данные:
City Customer_Churn City_Frequency City_Frequency_Normalized
29
Houston
1
169
0.21125
535
Phoenix
0
170
0.21250
695 Los Angeles
0
143
0.17875
557
Phoenix
0
170
0.21250
836 Los Angeles
0
143
0.17875
Тестовые данные:
City Customer_Churn City_Frequency City_Frequency_Normalized
521
New York
0
166
0.20750
737
Houston
1
169
0.21125
740 Los Angeles
1
143
0.17875
660
Phoenix
0
170
0.21250
411 Los Angeles
1
143
0.17875
Точность предсказаний на обучающей выборке: 0.5038
Точность предсказаний на тестовой выборке: 0.5500
Точность предсказаний на обучающей выборке (кодирование с одним активным состоянием):
0.5387
Точность предсказаний на тестовой выборке (кодирование с одним активным состоянием):
0.4900
Рис. 6.2 Частота встречаемости городов в обучающих данных
318 Кодирование категориальных переменных
Что мы здесь делаем? Для начала мы сгенерировали набор данных из
1000 наблюдений, состоящий из одной категориальной переменной City
и целевой переменной Customer_Churn. Далее разбили его на обучающую и те-
стовую выборки. Кодирование на основе частоты мы реализовали с помощью
функций groupby() и transform(), после чего привели значения к процентам,
поделив их на общее количество наблюдений в обучающей выборке. Затем
применили кодирование к тестовой выборке и вывели на графике распре-
деление частоты встречаемости городов в обучающей выборке. После этого
создали и обучили простые модели логистической регрессии по данным,
закодированным на основе частоты, и по данным, закодированным с одним
активным состоянием, и вывели результаты их предсказаний. Попутно при
выполнении кодирования с одним активным состоянием мы убедились, что
в тестовом наборе содержатся все столбцы из обучающей выборки, а недо-
стающие столбцы добавили и заполнили нулями.
6.2.3. Порядковое кодирование
Порядковое кодирование (ordinal encoding) удобно применять в случаях, когда
категории в переменной характеризуются естественным упорядочиванием.
Этот метод является противоположностью методу кодирования с одним ак-
тивным состоянием, который воспринимает все категории независимо друг
от друга.
Порядковое кодирование особенно подходит для переменных, распола-
гающих строгой иерархической структурой. Примеры таких переменных:
уровень образования: значения в этой переменной можно закодировать
следующим образом: средняя школа (1), степень бакалавра (2), степень
магистра (3), докторская степень (4);
удовлетворенность покупателей: здесь значения могут быть такими:
не удовлетворен (1), частично удовлетворен (2), нейтрально (3), удов-
летворен (4), очень удовлетворен (5);
рейтинг товаров: в данном случае можно использовать обычную пя-
тибалльную систему от 1 до 5.
Этот вид кодирования уместно применять в следующих случаях:
когда категориальная переменная наделена естественным упорядо-
чиванием (мало, больше, достаточно, очень много), но этот порядок
должен распространяться на все категории в переменной;
когда модель при прогнозировании должна принимать во внимание
ранги или порядок категорий. Это особенно важно для алгоритмов,
способных использовать числовые зависимости между закодирован-
ными значениями;
при анализе временных рядов, когда категории непосредственно свя-
заны со временем (например, стадии выполнения проекта: заплани-
рован, в разработке, в тестировании, развертывание и т. д.);
Более сложные примеры применения кодирования на основе целевой переменной 319
при работе с переменными, расстояние между значениями которых
приблизительно одинаковое или может считаться таковым.
Важно отметить, что порядковое кодирование действительно предполага-
ет равноудаленность категорий друг от друга, что в реальности редко бывает
достижимо. К примеру, разница в академических достижениях между вы-
пускником школы и бакалавром и между магистром и доктором наук вполне
очевидна. Это накладывает определенные требования к внимательности,
с которой стоит подходить к этому виду кодирования.
Пример применения порядкового кодирования:
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.preprocessing import OrdinalEncoder
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
# Простой набор данных
data={
'EducationLevel': ['High School', 'Bachelor', 'Master', 'PhD', 'Bachelor',
'High School', 'Master', 'PhD', 'Bachelor', 'Master'],
'Salary': [30000, 50000, 70000, 90000, 55000, 35000, 75000, 95000, 52000, 72000]
}
df = pd.DataFrame(data)
# Определяем словарь сопоставления для порядкового кодирования
education_order = {'High School': 1, 'Bachelor': 2, 'Master': 3, 'PhD': 4}
# Применяем порядковое кодирование вручную
df['EducationLevelEncoded'] = df['EducationLevel'].map(education_order)
# Применяем порядковое кодирование с помощью класса OrdinalEncoder из библиотеки Scikit-
learn
ordinal_encoder = OrdinalEncoder(categories=[['High School', 'Bachelor', 'Master', 'PhD']])
df['EducationLevelEncodedSK'] = ordinal_encoder.fit_transform(df[['EducationLevel']])
# Выводим преобразованный датафрейм
print("Закодированный датафрейм:")
print(df)
# Визуализируем кодирование
plt.figure(figsize=(10, 6))
plt.scatter(df['EducationLevelEncoded'], df['Salary'], alpha=0.6)
plt.xlabel('Уровень образования (закодированный)')
plt.ylabel('Зарплата')
plt.title('Зарплата в сравнении с уровнем образования (порядковое кодирование)')
plt.show()
# Подготавливаем данные для моделирования
X = df[['EducationLevelEncoded']]
# Двоичная классификация: 1, если зарплата > средней, иначе 0
320 Кодирование категориальных переменных
y = (df['Salary'] > df['Salary'].median()).astype(int)
# Разделяем данные на обучающую и тестовую выборки
X_train,X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# Обучаем простое дерево решений
clf = DecisionTreeClassifier(random_state=42)
clf.fit(X_train, y_train)
# Делаем предсказания
y_pred = clf.predict(X_test)
# Оцениваем качество модели
accuracy = accuracy_score(y_test, y_pred)
print(f"\nТочность модели: {accuracy:.2f}")
# Обрабатываем неизвестные категории
new_data = pd.DataFrame({'EducationLevel': ['Associate', 'Bachelor', 'PhD']})
new_data['EducationLevelEncoded'] = new_data['EducationLevel'].map(education_order).
fillna(0)
print("\nОбработка неизвестных категорий:")
print(new_data)
Вывод:
Закодированный датафрейм:
EducationLevel Salary EducationLevelEncoded EducationLevelEncodedSK
0 High School 30000
1
0.0
1
Bachelor 50000
2
1.0
2
Master 70000
3
2.0
3
PhD 90000
4
3.0
4
Bachelor 55000
2
1.0
5 High School 35000
1
0.0
6
Master 75000
3
2.0
7
PhD 95000
4
3.0
8
Bachelor 52000
2
1.0
9
Master 72000
3
2.0
Точность модели: 1.00
Обработка неизвестных категорий:
EducationLevel EducationLevelEncoded
0
Associate
0.0
1
Bachelor
2.0
2
PhD
4.0
Более сложные примеры применения кодирования на основе целевой переменной 321
Рис. 6.3 Зарплата в сравнении с уровнем образования
(порядковое кодирование)
Здесь мы сгенерировали простой набор данных с одной категориальной
переменной EducationLevel и одной числовой – Salary. Затем мы определи-
ли словарь с именем education_order для ручного порядкового кодирования
и с помощью метода map() выполнили это преобразование. После этого мы
показали, как порядковое кодирование можно осуществить с помощью клас-
са OrdinalEncoder из библиотеки Scikit-learn. Этот способ может быть удобен
при необходимости выполнить кодирование сразу в нескольких столбцах
или использовать конвейеры обработки данных. Далее мы вывели на диа-
грамме рассеяния зависимости между зарплатой и закодированным уровнем
образования. Это помогает понять, как порядковое кодирование сохраняет
следование категорий. После этого мы создали модель дерева решений, с по-
мощью которой предсказали признак того, является ли зарплата человека
выше медианной в зависимости от уровня образования, и оценили качество
модели. В заключение мы показали, как можно обрабатывать неизвестные
категории. Мы создали датафрейм с новой категорией уровня образования
Associate, применили к нему наш словарь сопоставления и с помощью вызова
метода fillna() заполнили нулями пропущенные значения.
Предпосылки для использования порядкового кодирования категориаль-
ных переменных:
порядковое кодирование стоит применять только при наличии явной
упорядоченности категорий. В противном случае модель будет делать
предположения относительно зависимостей между категориями, ко-
торых на самом деле нет;
322 Кодирование категориальных переменных
для моделей вроде деревьев решений и градиентного бустинга порядок
следования категорий может служить источником ценной информа-
ции. Эти алгоритмы способны анализировать числовые зависимости
между закодированными значениями при принятии решений о раз-
делении узлов дерева. В то же время для линейных моделей порядко-
вое кодирование может вносить нежелательную информацию о связях
между категориями;
выбор значений для кодирования может оказывать влияние на качест-
во модели. Хотя общепринятым подходом считается кодирование ка-
тегорий целочисленными значениями 1, 2, 3 и т. д. , бывают случаи,
когда альтернативный выбор значений может лучше отражать зави-
симости между категориями. К примеру, выбор последовательности 1,
2, 4, 8 вместо 1, 2, 3, 4 для кодирования уровней образования способен
подчеркнуть увеличивающуюся сложность или временные затраты на
получение все более высоких ученых степеней;
у вас должна быть разработана стратегия кодирования новых катего-
рий, которые могут появиться в тестовой выборке. Для таких катего-
рий вы можете использовать нулевое значение, среднее значение по
всем существующим категориям или выделить неизвестные значения
в новую категорию.
6.2.4. Выводы и рекомендации
Кратко подводя итоги этого раздела, можно сделать следующие заключения
относительно методов кодирования категориальных переменных:
кодирование на основе целевой переменной используется для того,
чтобы подчеркнуть зависимость между категориальным предиктором
и целевой переменной, что может положительно сказаться на качестве
итоговой модели. При реализации этого метода стоит учитывать сле-
дующие моменты:
• во избежание переобучения модели рекомендуется воспользоваться
кросс-валидацией или кодированием на отложенном блоке;
• для лучшей обработки редко встречающихся категорий можно при-
менить регуляризацию в виде различных техник сглаживания;
• необходимо внимательно следить за тем, чтобы во время обучения
не произошла утечка информации, особенно при работе с времен-
ными рядами;
кодирование на основе частоты является эффективным решением
для обработки категориальных переменных с высокой кардинально-
стью и обладает следующими преимуществами:
• снижает размерность модели в сравнении с кодированием с одним
активным состоянием;
• позволяет делать выводы на основе частоты встречаемости кате-
горий;
Практические упражнения 323
• одинаково хорошо работает как в моделях на основе деревьев, так
и в линейных моделях;
порядковое кодирование идеально подходит для переменных с явно
прослеживающейся последовательностью категорий. К этому виду ко-
дирования применимы следующие утверждения:
• сохраняется относительное ранжирование категорий;
• особенно эффективен применительно к моделям на основе деревьев;
• требует понимания предметной области для определения правиль-
ного порядка категорий.
Выбор метода кодирования может ощутимо влиять на качество итоговой
модели и ее интерпретируемость. При принятии решений необходимо учи-
тывать следующие аспекты:
природа категориальной переменной (упорядоченная или нет);
кардинальность переменной;
выбранный алгоритм машинного обучения;
размер набора данных;
потребность в интерпретируемости выводов модели.
6.3. Практические упражнения
Теперь, по традиции, приступим к выполнению заданий, с помощью которых
вы сможете закрепить полученные знания на практике.
Упражнение 1. Кодирование на основе целевой переменной
Есть набор данных о клиентах с категориальной переменной Neighborhood
и целевой переменной SalePrice:
import pandas as pd
# Простой набор данных
data = {'Neighborhood': ['A', 'B', 'A', 'C', 'B'],
'SalePrice': [300000, 450000, 350000, 500000, 470000]}
df = pd.DataFrame(data)
Ваша задача состоит в том, чтобы применить кодирование на основе целе-
вой переменной к столбцу Neighborhood путем вычисления среднего значения
целевой переменной для каждой категории соседства.
Решение
# Рассчитываем средние значения SalePrice для каждой категории соседства
neighborhood_mean = df.groupby('Neighborhood')['SalePrice'].mean()
# Применяем кодирование на основе целевой переменной
df['NeighborhoodEncoded'] = df['Neighborhood'].map(neighborhood_mean)
324 Кодирование категориальных переменных
# Выводим результаты
print(df[['Neighborhood', 'SalePrice', 'NeighborhoodEncoded']])
Вывод:
Neighborhood SalePrice NeighborhoodEncoded
0
A
300000
325000.0
1
B
450000
460000.0
2
A
350000
325000.0
3
C
500000
500000.0
4
B
470000
460000.0
Упражнение 2. Кодирование на основе целевой переменной
со сглаживанием
Есть набор данных из первого упражнения. Ваша задача состоит в том, чтобы
применить кодирование на основе целевой переменной к столбцу Neighbor-
hood со сглаживанием во избежание переобучения модели. Воспользуйтесь
параметром сглаживания alpha, по умолчанию равным пяти.
Решение
# Параметр сглаживания
alpha = 5
# Глобальное среднее для SalePrice
global_mean = df['SalePrice'].mean()
# Рассчитываем сглаженные средние значения SalePrice для каждой категории соседства
df['NeighborhoodEncoded'] = df['Neighborhood'].map(
lambda x: (neighborhood_mean[x] * df['Neighborhood'].value_
counts()[x] +
global_mean * alpha) / (df['Neighborhood'].value_
counts()[x] + alpha))
# Выводим результаты
print(df[['Neighborhood', 'NeighborhoodEncoded']])
Вывод:
Neighborhood NeighborhoodEncoded
0
A
388571.428571
1
B
427142.857143
2
A
388571.428571
3
C
428333.333333
4
B
427142.857143
Упражнение 3. Кодирование на основе частоты
Есть набор данных, содержащий одну категориальную переменную City:
import pandas as pd
# Простой набор данных
Практические упражнения 325
data = {'City': ['New York', 'Los Angeles', 'Chicago', 'New York', 'Houston', 'Los Angeles']}
df = pd.DataFrame(data)
Ваша задача состоит в том, чтобы применить кодирование на основе час-
тоты к столбцу City, заменив значения в нем на частоту встречаемости ка-
тегорий в наборе данных.
Решение
# Выполняем кодирование на основе частоты
df['City_Frequency'] = df.groupby('City')['City'].transform('count')
# Выводим результаты
print(df)
Вывод:
City City_Frequency
0
New York
2
1 Los Angeles
2
2
Chicago
1
3
New York
2
4
Houston
1
5 Los Angeles
2
Упражнение 4. Порядковое кодирование
Есть набор данных, содержащий одну категориальную переменную Educa-
tionLevel с четырьмя следующими категориями: High School, Bachelor, Master
и PhD:
import pandas as pd
# Простой набор данных
data = {'EducationLevel': ['High School', 'Bachelor', 'Master', 'PhD', 'Bachelor']}
df = pd.DataFrame(data)
Ваша задача состоит в том, чтобы применить порядковое кодирование
к столбцу EducationLevel с помощью словаря соответствий.
Решение
# Определяем словарь соответствий
education_order = {'High School': 1, 'Bachelor': 2, 'Master': 3, 'PhD': 4}
# Применяем порядковое кодирование
df['EducationLevelEncoded'] = df['EducationLevel'].map(education_order)
# Выводим результаты
print(df)
Вывод:
EducationLevel EducationLevelEncoded
0 High School
1
326 Кодирование категориальных переменных
1
Bachelor
2
2
Master
3
3
PhD
4
4
Bachelor
2
Упражнение 5. Обработка столбца с высокой
кардинальностью с помощью кодирования
на основе частоты
Есть набор данных, содержащий одну категориальную переменную Product-
Category с предположительно большим количеством уникальных категорий
(для примера мы рассмотрим маленький набор данных):
import pandas as pd
# Простой набор данных с высокой кардинальностью
data = {'ProductCategory': ['Electronics', 'Furniture', 'Electronics', 'Clothing',
'Furniture', 'Clothing', 'Electronics']}
df = pd.DataFrame(data)
Ваша задача состоит в том, чтобы применить кодирование на основе час-
тоты к столбцу ProductCategory с целью упрощения набора данных.
Решение
# Выполняем кодирование на основе частоты
df['ProductCategory_Frequency'] = df.groupby('ProductCategory')['ProductCategory'].
transform('count')
# Выводим результаты
print(df)
Вывод:
ProductCategory ProductCategory_Frequency
0
Electronics
3
1
Furniture
2
2
Electronics
3
3
Clothing
2
4
Furniture
2
5
Clothing
2
6
Electronics
3
6.4. Возможные проблемы
В этом разделе мы еще раз проговорим подводные камни, с которыми вы
можете столкнуться при кодировании категориальных переменных.
Возможные проблемы 327
6.4.1. Переобучение при использовании
кодирования на основе целевой переменной
Кодирование на основе целевой переменной представляет собой очень мощ-
ную технику, но при этом она таит существенный риск переобучения модели,
связанный с непосредственным вовлечением целевой переменной в процесс
кодирования.
Что может пойти не так:
переобучение может возникать в случаях, когда модель становится
чрезмерно зависимой от конкретных значений целевой переменной
в обучающей выборке. В результате мы получим низкое качество мо-
дели на тестовых данных;
без принятия необходимых мер кодирование на основе целевой пере-
менной может приводить к утечке информации, когда данные из тес-
товой выборки непреднамеренно влияют на процесс обучения.
Решение:
всегда применяйте кодирование на основе целевой переменной со-
вместно с кросс-валидацией, чтобы гарантировать отсутствие доступа
модели к значениям целевой переменной из тестовой выборки в про-
цессе обучения;
при работе с редко встречающимися категориями избежать переобу-
чения может помочь техника сглаживания или добавление небольшого
шума к закодированным значениям.
6.4.2. Неправильное использование порядкового
кодирования
Порядковое кодирование может применяться только в случаях, когда катего-
рии в переменной связаны друг с другом естественным образом. В против-
ном случае использование этого способа кодирования может вводить модель
в заблуждение и снижать ее качество.
Что может пойти не так:
применение порядкового кодирования к переменной с несвязанными
категориями может приводить к ослаблению качества предсказаний
модели.
Решение:
всегда применяйте порядковое кодирование только к переменным
с четкой логической последовательностью категорий;
для переменных без явного порядка категорий воспользуйтесь коди-
рованием с одним активным состоянием или кодированием на основе
целевой переменной.
328 Кодирование категориальных переменных
6.4.3. Использование кодирования
с одним активным состоянием для столбцов
с высокой кардинальностью
Одним из главных красных флагов на пути применения кодирования с одним
активным состоянием является высокая кардинальность переменной, т. е .
большое количество уникальных значений. В этом случае вы можете получить
сильно раздутый набор данных и, как следствие, замедление процесса обуче-
ния модели, сложность которой может оказаться неоправданно высокой.
Что может пойти не так:
применение кодирования с одним активным состоянием к столбцам
с высокой кардинальностью может приводить к чрезмерному расходу
памяти и вычислительных ресурсов;
избыточная размерность модели может негативно сказываться на ее
способности к обобщению и приводить к переобучению.
Решение:
при работе со столбцами с высокой кардинальностью воспользуйтесь
кодированием на основе частоты или целевой переменной. Эти мето-
ды не приводят к увеличению размерности модели, сохраняя при этом
большую часть ценной информации;
если без кодирования с одним активным состоянием не обойтись,
рассмотрите вариант предварительного объединения редко встреча-
ющихся категорий в отдельную группу для снижения количества ито-
говых столбцов.
6.4.4. Пренебрежение разреженностью матрицы
при кодировании с одним активным состоянием
При работе с большими наборами данных применение кодирования с одним
активным состоянием зачастую ведет к образованию разреженных матриц,
в которых большая часть значений заполнена нулями. Хранение и обработка
таких матриц без использования особого подхода могут оказаться очень за-
тратными в плане ресурсов.
Что может пойти не так:
работа с разреженными матрицами как с плотными может приводить
к повышенному расходованию ресурсов;
без специальной оптимизации операции над разреженными матрица-
ми могут выполняться неэффективно.
Решение:
при работе с разреженными матрицами используйте специально раз-
работанные с этой целью структуры, присутствующие в библиотеках
Возможные проблемы 329
наподобие Scipy. Класс OneHotEncoder поддерживает специальный пара-
метр для возвращения результата в виде разреженной матрицы;
убедитесь, что ваш конвейер данных оптимизирован для работы с раз-
реженными данными, если активно используете кодирование с одним
активным состоянием.
6.4.5. Утечка информации при использовании
кодирования на основе целевой переменной
Одним из главных недостатков кодирования на основе целевой переменной
является его склонность к возникновению утечки информации. Это может
приводить к излишне оптимистичной оценке качества модели и снижению
ее обобщающей способности.
Что может пойти не так:
утечка информации может приводить к тому, что модель будет хорошо
показывать себя во время обучения и валидации, но плохо прогнози-
ровать новые данные.
Решение:
воспользуйтесь методом кросс-валидации, чтобы данные из тестовой
выборки не утекали в процессе обучения модели;
применяйте сглаживание или регуляризацию, чтобы снизить риск
переобучения.
6.4.6. Ошибочная интерпретация результатов
кодирования на основе частоты
Кодирование на основе частоты может приводить к ухудшению качества
модели, если частота встречаемости категорий в переменной в действитель-
ности никак не связана с целевой переменной.
Что может пойти не так:
модель может больше полагаться на часто встречающиеся категории
в переменной, тогда как они не добавляют ей предсказательной мощ-
ности.
Решение:
перед применением кодирования на основе частоты убедитесь в том,
что распределение категорий в столбце напрямую относится к решае-
мой задаче. В противном случае лучше будет применить кодирование
на основе целевой переменной или порядковое кодирование;
проверьте эффективность кодирования на валидационной выборке,
чтобы убедиться, что кодируемая переменная вносит значительный
вклад в качество модели.
330 Кодирование категориальных переменных
Заключение
В этой главе мы рассмотрели различные способы кодирования категори-
альных переменных. Кодировать переменные необходимо для того, чтобы
с ними могли эффективно работать алгоритмы машинного обучения. Вы-
бор метода должен зависеть главным образом от природы категориальной
переменной и ее наполнения, а также от количества уникальных категорий
и выбранного алгоритма машинного обучения.
В начале главы мы подробно поговорили про все нюансы использования
наиболее распространенного метода кодирования с одним активным состоя-
нием, после чего описали другие способы преобразования категориальных
переменных, такие как кодирование на основе целевой переменной, кодиро-
вание на основе частоты и порядковое кодирование. Попутно мы обсудили
все тонкости использования этих методов и рассмотрели возможные подвод-
ные камни на пути их использования.
В следующей главе мы подробно поговорим о создании признаков и пере-
менных взаимодействия.
Глава 7
Конструирование
признаков и переменных
взаимодействия
Создание новых переменных является неотъемлемой частью процесса по-
лучения качественных моделей машинного обучения. Эта процедура, также
именуемая конструированием признаков, предполагает создание новых пре-
дикторов на основе существующих для выявления сложных зависимостей
и шаблонов в данных, недоступных при анализе исходного набора перемен-
ных. Это помогает значительно повысить качество, надежность и интерпре-
тируемость моделей.
7.1. Создание признаков на основе
существующих переменных
7.1.1. Математические преобразования переменных
В разделе 5.2 мы уже поговорили подробно о логарифмическом преобразо-
вании, преобразованиях квадратного и кубического корня, а также о преоб-
разованиях Бокса−Кокса и Йео−Джонсона. В этой главе мы продолжим тему
трансформации переменных и начнем с экспоненциального и степенного
преобразований.
Экспоненциальное преобразование
Экспоненциальный тип преобразования переменной хорошо справляется
с задачами выявления различий между значениями и нормализации рас-
пределений, скошенных в левую сторону. В отличие от логарифмического
332 Конструирование признаков и переменных взаимодействия
преобразования, которое сжимает область больших значений переменной,
экспоненциальное преобразование, наоборот, расширяет ее, что делает его
полезным в следующих ситуациях:
когда необходимо подчеркнуть различия между большими значения-
ми переменной;
когда нужно сбалансировать распределение переменной, скошенной
влево;
когда в вашей переменной наблюдаются большие различия в области
низких значений и большая плотность в верхней части диапазона.
Часто экспоненциальное преобразование применяется в следующих об-
ластях:
финансовое моделирование: для расчета сложных процентов или тем-
пов роста;
анализ динамики численности населения: для моделирования шабло-
нов с экспоненциальным ростом;
обработка сигналов: для укрупнения сигналов на определенной час-
тоте.
При применении экспоненциального преобразования необходимо обра-
щать особое внимание на:
основание экспоненциальной функции и его влияние на масштаб пре-
образования;
вероятность появления выброса в данных, которые могут потребовать
дополнительной обработки;
необходимость выполнения обратного преобразования для интерпре-
тации результатов.
Предположим, у нас в наборе данных есть переменная, значения которой
мы хотим как-то выделить. Применим к ней экспоненциальное преобра-
зование с созданием нового признака, подчеркивающего различия между
большими значениями исходной переменной:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
# Простой набор данных
data = {'Value': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}
df = pd.DataFrame(data)
# Создадим новый признак путем применения экспоненциального преобразования к переменной
Value
df['ExpValue'] = np.exp(df['Value'])
# Выводим преобразованный набор
print("Преобразованный датафрейм:")
print(df)
Создание признаков на основе существующих переменных 333
# Рассчитываем статистику
print("\nОбщая статистика:")
print(df.describe())
# Визуализируем распределения
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
sns.scatterplot(x='Value', y='ExpValue', data=df, ax=ax1)
ax1.set_title('Исходные значения и экспоненциально преобразованные')
ax1.set_xlabel('Исходные значения')
ax1.set_ylabel('Экспоненциально преобразованные значения')
sns.lineplot(x='Value', y='Value', data=df, ax=ax2, label='Исходные')
sns.lineplot(x='Value', y='ExpValue', data=df, ax=ax2, label='Экспоненциальные')
ax2.set_title('Сравнение значений')
ax2.set_xlabel('Исходные значения')
ax2.set_ylabel('Преобразованные значения')
ax2.legend()
plt.tight_layout()
plt.show()
# Сравнение диапазонов
original_range = df['Value'].max() - df['Value'].min()
exp_range = df['ExpValue'].max() - df['ExpValue'].min()
print(f"\nДиапазон исходных значений: {original_range:.2f}")
print(f"Диапазон экспоненциально преобразованных значений: {exp_range:.2f}")
Вывод:
Преобразованный датафрейм:
Value
ExpValue
0
1
2.718282
1
2
7.389056
2
3
20.085537
3
4
54.598150
4
5 148.413159
5
6 403.428793
6
7 1096.633158
7
8 2980.957987
8
9 8103.083928
9
10 22026.465795
Общая статистика:
Value
ExpValue
count 10.00000
10.000000
mean
5.50000 3484.377385
std
3.02765 6989.621269
min
1.00000
2.718282
25%
3.25000
28.713690
50%
5.50000 275.920976
75%
7.75000 2509.876780
max
10.00000 22026.465795
Диапазон исходных значений: 9.00
Диапазон экспоненциально преобразованных значений: 22023.75
334 Конструирование признаков и переменных взаимодействия
Рис. 7.1 Сравнение исходных значений и экспоненциально преобразованных
Здесь мы применили к исходной переменной функцию np.exp() и показали
с помощью статистики, диапазонов и графиков, как исходные значения со-
поставляются с экспоненциально преобразованными.
Степенное преобразование
К операциям степенного преобразования относится возведение значений
исходной переменной в определенную степень. Такого рода преобразования
могут пригодиться при необходимости подчеркнуть большие значения или
выявить нелинейные зависимости в данных. Наиболее популярными степен-
ными преобразованиями являются следующие:
квадратическое преобразование (square transformation), x
2
: может ока-
заться полезным, если нужно подчеркнуть различия в области больших
значений переменной и нивелировать различия в области маленьких
значений. Часто этот вид преобразования применяется в статистиче-
ском анализе и моделях машинного обучения для обнаружения квад-
ратических зависимостей;
кубическое преобразование (cube transformation), x
3
: еще больше подчер-
кивает различия больших значений переменной в сравнении с квадра-
тическим преобразованием. Применяется в случаях, когда различия
в правой части диапазона значений имеют гораздо большее значение
по сравнению с левой частью;
преобразования с большими показателями степени, x
4
,x
5
ит.д.:та-
кие преобразования могут использоваться для обнаружения сложных
Создание признаков на основе существующих переменных 335
нелинейных зависимостей в данных. Стоит проявлять осторожность
при использовании больших показателей степеней в преобразованиях,
поскольку они могут приводить к численной неустойчивости и пере-
обучению;
дробно-степенные преобразования (fractional power transformation): та-
кие виды преобразований применяются достаточно редко, но могут
быть незаменимыми в некоторых сценариях. К примеру, преобразо-
вание кубического корня можно использовать для обнаружения экс-
тремальных выбросов с сохранением характера исходного масштаба.
При использовании степенных преобразований необходимо:
хорошо понимать природу исходных данных и задачу, которую необ-
ходимо решить. Применение тех или иных степенных преобразований
зависит от решаемой задачи и характера данных;
осознавать вероятность создания выбросов в данных, особенно при
использовании больших показателей степени;
предвосхищать возможные сложности с интерпретируемостью резуль-
татов. Применение степенных преобразований может затруднить ин-
терпретацию коэффициентов итоговой модели;
осознавать необходимость масштабирования признака после приме-
нения степенного преобразования из-за сильного влияния на исход-
ный масштаб данных.
Пример применения степенных преобразований:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
# Простой набор данных
data = {'Value': np.random.uniform(1, 100, 1000)}
df = pd.DataFrame(data)
# Применяем степенные преобразования
df['Square'] = df['Value'] ** 2
df['Cube'] = df['Value'] ** 3
df['SquareRoot'] = np.sqrt(df['Value'])
# Визуализируем распределения
fig, axs = plt.subplots(2, 2, figsize=(15, 15))
sns.histplot(df['Value'], kde=True, ax=axs[0, 0])
axs[0, 0].set_title('Исходное распределение')
axs[0, 0].set(xlabel='', ylabel='')
sns.histplot(df['Square'], kde=True, ax=axs[0, 1])
axs[0, 1].set_title('Квадратическое преобразование')
axs[0, 1].set(xlabel='', ylabel='')
sns.histplot(df['Cube'], kde=True, ax=axs[1, 0])
axs[1, 0].set_title('Кубическое преобразование')
axs[1, 0].set(xlabel='', ylabel='')
sns.histplot(df['SquareRoot'], kde=True, ax=axs[1, 1])
336 Конструирование признаков и переменных взаимодействия
axs[1, 1].set_title('Преобразование квадратного корня')
axs[1, 1].set(xlabel='', ylabel='')
plt.tight_layout()
plt.show()
# Сравниваем перекосы
print("Перекосы:")
print(f"Исходные данные: {df['Value'].skew():.2f}")
print(f"Квадратическое преобразование: {df['Square'].skew():.2f}")
print(f"Кубическое преобразование: {df['Cube'].skew():.2f}")
print(f"Преобразование квадратного корня: {df['SquareRoot'].skew():.2f}")
Вывод:
Перекосы:
Исходные данные: -0 .00
Квадратическое преобразование: 0.60
Кубическое преобразование: 1.01
Преобразование квадратного корня: -0 .50
Рис. 7.2 Распределения значений после степенного преобразования
Создание признаков на основе существующих переменных 337
Здесь мы сгенерировали набор данных с одной переменной, насчитываю-
щей 1000 значений из равномерного распределения от 1 до 100, и применили
к этой переменной квадратическое преобразование, кубическое и преоб-
разование квадратного корня. Далее мы визуализировали распределения
исходных и преобразованных значений при помощи гистограмм с ядерной
оценкой плотности. Также мы вывели показания перекоса распределений
для всех четырех переменных.
На этом примере видно, как разные степенные преобразования влияют на
распределение исходной переменной. Как видите, квадратическое и куби-
ческое преобразования стремятся подчеркнуть различия в области больших
значений и могут приводить к перекосу распределений в правую сторо-
ну, тогда как преобразование квадратного корня помогает снизить перекос
в правую сторону путем сжатия диапазона больших значений.
7.1.2. Извлечение компонентов из дат
При работе с наборами данных, содержащими переменные, относящиеся
к дате и времени, вы можете значительно повысить качество итоговой мо-
дели за счет создания новых признаков на основе компонентов таких пере-
менных. К примеру, путем извлечения месяца и дня недели из данных по
продажам вы можете обнаружить месячные или недельные шаблоны. В фи-
нансовых временных рядах извлечение года и квартала помогает идентифи-
цировать долгосрочные тенденции и циклические шаблоны.
Также вы можете создавать более сложные признаки на основе временных
рядов, отвечающие на вопросы о том, какой сегодня день – выходной или
будний, является ли этот день праздничным, какое количество дней прошло
с заданного события и т. д.
Давайте рассмотрим простой пример создания признаков путем извлече-
ния компонентов из дат, которые могут помочь повысить качество модели:
# Sample data with a date column
data={
'SaleDate': ['2021-01-15', '2020-07-22', '2021-03 -01', '2019-10-10', '2022-12-31'],
'Price': [250000, 300000, 275000, 225000, 350000]
}
df = pd.DataFrame(data)
# Приводим столбец SaleDate к типу datetime
df['SaleDate'] = pd.to_datetime(df['SaleDate'])
# Извлекаем компоненты даты и времени из столбца SaleDate
df['YearSold'] = df['SaleDate'].dt.year
df['MonthSold'] = df['SaleDate'].dt.month
df['DayOfWeekSold'] = df['SaleDate'].dt.dayofweek
df['QuarterSold'] = df['SaleDate'].dt.quarter
df['IsWeekend'] = df['SaleDate'].dt.dayofweek.isin([5, 6]).astype(int)
df['DaysSince2019'] = (df['SaleDate'] - pd.Timestamp('2019-01-01')).dt.days
338 Конструирование признаков и переменных взаимодействия
# Создаем столбец с сезоном
df['Season'] = pd.cut(
df['MonthSold'],
bins=[0, 3, 6, 9, 12],
labels=['Зима', 'Весна', 'Лето', 'Осень'],
include_lowest=True
)
# Посмотрим на новые признаки
print(df)
# Анализируем зависимости между новыми признаками и стоимостью дома
import matplotlib.pyplot as plt
import seaborn as sns
plt.figure(figsize=(12, 6))
sns.scatterplot(data=df, x='DaysSince2019', y='Price', hue='Season', s=100)
plt.title('Стоимость домов с течением времени')
plt.show()
# Рассчитываем среднюю стоимость по годам и месяцам
avg_price = df.groupby(['YearSold', 'MonthSold'])['Price'].mean().unstack()
plt.figure(figsize=(12, 6))
sns.heatmap(avg_price, annot=True, fmt='.0f', cmap='YlOrRd')
plt.title('Средняя стоимость домов по годам и месяцам')
plt.show()
Вывод:
SaleDate Price YearSold MonthSold DayOfWeekSold QuarterSold IsWeekend DaysSince2019 Season
0 2021-01-15 250000
2021
1
4
1
0
745 Зима
1 2020-07-22 300000
2020
7
2
3
0
568 Лето
2 2021-03-01 275000
2021
3
0
1
0
790 Зима
3 2019-10-10 225000
2019
10
3
4
0
282 Осень
4 2022-12-31 350000
2022
12
5
4
1
1460 Осень
Здесь мы сначала создали набор данных со столбцами SaleDate и Price
и привели столбец SaleDate к типу данных datetime с помощью функции
pd.to_datetime(). Затем, воспользовавшись атрибутом доступа dt, извлекли
разные свойства столбца SaleDate и сохранили их в виде отдельных столбцов
в датафрейме. При этом поля YearSold, MonthSold, DayOfWeekSold и QuarterSold
представляют собой простые компоненты даты и времени, а поля IsWeekend
и DaysSince2019 создаются при помощи более сложных выражений. В первом
случае мы вычисляем, является ли дата выходным днем, а во втором рас-
считываем количество дней, прошедших с 1 января 2019 года. После этого
мы создали переменную Season, разбив нашу исходную переменную на се-
зоны с помощью функции pd.cut(). В заключение мы вывели два графика.
На первом (рис. 7.3) визуализировали зависимость между количеством дней,
прошедших с 1 января 2019 года, и стоимостью продаж, а на втором (рис. 7.4)
при помощи тепловой карты отобразили среднюю стоимость домов по годам
и месяцам.
Создание признаков на основе существующих переменных 339
Рис. 7.3 Стоимость домов с течением времени
Рис. 7.4 Средняя стоимость домов по годам и месяцам
340 Конструирование признаков и переменных взаимодействия
7.1.3. Комбинирование признаков
Создание новых признаков на основе уже существующих переменных по-
зволяет обнажить скрытые зависимости в наборе данных.
К примеру, в наборе данных, посвященном рыночной стоимости домов, вы
могли бы создать новый признак, отвечающий за стоимость за квадратный
метр, поделив стоимость дома на его площадь. Это может позволить норма-
лизовать стоимости домов по площади и выявить шаблоны, которые были
недоступны при просмотре этих двух переменных по отдельности.
Рассмотрим пример создания признаков на основе других переменных
и констант:
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
# Простой набор данных
data={
'HousePrice': [500000, 700000, 600000, 550000, 800000],
'HouseSize': [2000, 3000, 2500, 1800, 3500],
'Bedrooms': [3, 4, 3, 2, 5],
'YearBuilt': [1990, 2005, 2000, 1985, 2010]
}
df = pd.DataFrame(data)
# Создаем новые признаки
df['PricePerSqFt'] = df['HousePrice'] / df['HouseSize']
df['AvgRoomSize'] = df['HouseSize'] / df['Bedrooms']
df['AgeOfHouse'] = 2023 - df['YearBuilt']
df['PricePerRoom'] = df['HousePrice'] / df['Bedrooms']
# Просматриваем новые признаки
print(df[['PricePerSqFt', 'AvgRoomSize', 'AgeOfHouse', 'PricePerRoom']], end='\n\n')
# Визуализируем зависимости
plt.figure(figsize=(12, 8))
# Диаграмма рассеяния по полям HousePrice и HouseSize с цветовым разделением по полю
AgeOfHouse
plt.subplot(2, 2, 1)
sns.scatterplot(data=df, x='HouseSize', y='HousePrice', hue='AgeOfHouse',
palette='viridis', s=70)
plt.title('Стоимость домов в зависимости от их размера')
# Столбчатая диаграмма со средней стоимостью за квадратный фут по количеству спален
plt.subplot(2, 2, 2)
sns.barplot(data=df, x='Bedrooms', y='PricePerSqFt')
plt.title('Средняя стоимость за кв. фут по количеству спален')
# Тепловая карта с корреляциями
Создание признаков на основе существующих переменных 341
plt.subplot(2, 2, 3)
sns.heatmap(df.corr(), annot=True, cmap='coolwarm')
plt.title('Тепловая карта с корреляциями')
# Диаграмма рассеяния по полям PricePerRoom и AgeOfHouse
plt.subplot(2, 2, 4)
sns.scatterplot(data=df, x='AgeOfHouse', y='PricePerRoom', s=70)
plt.title('Зависимость стоимости на одну комнату от возраста дома')
plt.tight_layout()
plt.show()
# Статистика
print(df[['PricePerSqFt', 'AvgRoomSize', 'AgeOfHouse', 'PricePerRoom']].describe(),
end='\n\n')
# Корреляционный анализ
print(df.corr()['HousePrice'].sort_values(ascending=False))
Вывод:
PricePerSqFt AvgRoomSize AgeOfHouse PricePerRoom
0 250.000000 666.666667
33 166666.666667
1 233.333333 750.000000
18 175000.000000
2 240.000000 833 .333333
23 200000.000000
3 305.555556 900.000000
38 275000.000000
4 228.571429 700.000000
13 160000.000000
PricePerSqFt AvgRoomSize AgeOfHouse PricePerRoom
count
5.000000
5.000000 5.000000
5.000000
mean
251.492063 770.000000 25.000000 195333.333333
std
31.273991 96.032402 10.368221 47043.124424
min
228.571429 666.666667 13.000000 160000.000000
25%
233.333333 700.000000 18.000000 166666.666667
50%
240.000000 750.000000 23.000000 175000.000000
75%
250.000000 833 .333333 33 .000000 200000.000000
max
305.555556 900.000000 38 .000000 275000.000000
HousePrice
1.000000
HouseSize
0.963940
YearBuilt
0.911094
Bedrooms
0.892237
AvgRoomSize
- 0 .263033
PricePerRoom -0 .450888
PricePerSqFt -0 .594413
AgeOfHouse
- 0 .911094
Name: HousePrice, dtype: float64
Здесь мы создали четыре новых признака, отвечающих за стоимость дома
в расчете на квадратный фут, среднюю площадь комнат, возраст дома и стои-
мость в расчете на количество спален. Далее мы вывели четыре диаграммы
с зависимостями по созданным признакам.
342 Конструирование признаков и переменных взаимодействия
Рис. 7.5 Диаграммы с новыми признаками
7.2. Переменные взаимодействия
и значимость признаков для моделей
Как вы уже знаете, переменные взаимодействия (feature interaction) играют
очень важную роль в обнаружении скрытых сложных зависимостей между
предикторами в исходном наборе данных.
Переменные взаимодействия могут быть представлены в самых разных
формах, что позволяет извлечь информацию о разного рода зависимостях.
Ниже перечислены самые распространенные виды таких переменных:
полиномиальные признаки − позволяют обнаружить нелинейности в ис-
ходном наборе данных за счет возведения переменных в степень, что
бывает полезно при наличии экспоненциальных или квадратических
зависимостей в данных;
перекрестные признаки − получаются при помощи перемножения дру-
гих существующих переменных, что дает возможность подсветить си-
туации, когда эффект от изменения одного предиктора на целевую
переменную меняется в зависимости от значений другого предиктора;
кусочные функции (piecewise function) − делят пространство предикто-
ров на сегменты, что позволяет моделировать зависимости в рамках
отдельных сегментов. Этот подход бывает особенно полезен, когда
Переменные взаимодействия и значимость признаков для моделей 343
в исходных переменных наблюдаются какие-то пороговые значения
или когда зависимости между переменными меняют свой характер
в определенных точках.
7.2.1. Полиномиальные признаки
Мы уже создавали отдельно квадратические и кубические признаки в набо-
ре данных, а теперь посмотрим, как можно удобно делать это при помощи
специального класса PolynomialFeatures, присутствующего в модуле sklearn.
preprocessing.
Предположим, у вас есть переменная HouseSize, и вы предполагаете, что
стоимость дома может быть связана с его размером нелинейно. В этом слу-
чае вы можете с помощью одного экземпляра класса легко и просто создать
столько полиномиальных признаков, сколько необходимо, как показано
ниже:
import pandas as pd
import numpy as np
from sklearn.preprocessing import PolynomialFeatures
import matplotlib.pyplot as plt
import seaborn as sns
# Простой набор данных
np.random.seed(42)
data = {'HouseSize': np.random.randint(1000, 5000, 100)}
df = pd.DataFrame(data)
# Создаем экземпляр класса PolynomialFeatures для полинома третьей степени
poly = PolynomialFeatures(degree=3, include_bias=False)
# Генерируем полиномиальные признаки
polynomial_features = poly.fit_transform(df[['HouseSize']])
# Создаем новый датафрейм с полиномиальными признаками
df_poly = pd.DataFrame(polynomial_features, columns=['HouseSize', 'HouseSize^2',
'HouseSize^3'])
# Добавляем столбец с ценой, который будет зависеть от наших полиномиальных признаков
и содержать небольшой шум
df_poly['Price'] = (0.0005 * df_poly['HouseSize'] +
0.00005 * df_poly['HouseSize^2'] -
0.000005 * df_poly['HouseSize^3'] +
np.random.normal(0, 50000, 100))
# Выведем первые несколько строк датафрейма
print(df_poly.head(), end='\n\n')
# Визуализируем зависимости
plt.figure(figsize=(15, 10))
344 Конструирование признаков и переменных взаимодействия
# Диаграмма рассеяния по признаку HouseSize
plt.subplot(2, 2, 1)
sns.scatterplot(data=df_poly, x='HouseSize', y='Price')
plt.title('Зависимость стоимости дома от его площади')
# Диаграмма рассеяния по признаку HouseSize^2
plt.subplot(2, 2, 2)
sns.scatterplot(data=df_poly, x='HouseSize^2', y='Price')
plt.title('Зависимость стоимости дома от квадрата его площади')
# Диаграмма рассеяния по признаку HouseSize^3
plt.subplot(2, 2, 3)
sns.scatterplot(data=df_poly, x='HouseSize^3', y='Price')
plt.title('Зависимость стоимости дома от куба его площади')
# Тепловая карта с корреляциями
plt.subplot(2, 2, 4)
sns.heatmap(df_poly.corr(), annot=True, cmap='coolwarm')
plt.title('Тепловая карта с корреляциями')
plt.tight_layout()
plt.show()
# Суммарная статистика
print(df_poly.describe(), end='\n\n')
# Корреляции с целевой переменной Price
print(df_poly.corr()['Price'].sort_values(ascending=False))
Вывод:
HouseSize HouseSize^2 HouseSize^3
Price
0
4174.0 17422276.0 7.272058e+10 -371543.982416
1
4507.0 20313049.0 9.155091e+10 -396692.014668
2
1860.0 3459600.0 6.434856e+09 2919.577067
3
2294.0 5262436.0 1.207203e+10 -68677.313881
4
2130.0 4536900.0 9.663597e+09 -93449.404007
HouseSize HouseSize^2 HouseSize^3
Price
count 100.000000 1.000000e+02 1.000000e+02
100.000000
mean 3132.320000 1.105011e+07 4.217637e+10 -209180.600993
std 1118.566966 6 .921442e+06 3.529126e+10 183475.146197
min 1021.000000 1.042441e+06 1.064332e+09 -615481.498853
25% 2207.250000 4.872133e+06 1.075481e+10 -335065.908064
50% 3191.500000 1.018618e+07 3.251242e+10 -160888.281467
75% 4077.750000 1.662811e+07 6.780584e+10 -68102.337285
max
4943.000000 2.443325e+07 1.207735e+11 87777.202669
Price
1.000000
HouseSize
- 0 .909919
HouseSize^2
- 0 .949775
HouseSize^3
- 0 .963244
Name: Price, dtype: float64
Переменные взаимодействия и значимость признаков для моделей 345
Рис. 7.6 Диаграммы рассеяния по полиномиальным признакам
и тепловая карта с корреляциями
На рис. 7.6 мы видим, как целевая переменная зависит от исходного пре-
диктора и полиномиальных признаков второй и третьей степеней. Подобный
анализ может быть весьма полезен в процессе конструирования и отбора
признаков для модели, поскольку позволяет обнаружить важные корреляции
между переменными.
Увеличивая степень полинома, вы можете создавать более сложные при-
знаки. Но будьте осторожны, поскольку наличие признаков высокой степени
может приводить к переобучению модели, особенно при работе с небольши-
ми наборами данных.
7.2.2. Перекрестные признаки
Мы с вами уже создавали перекрестные признаки (cross-feature), также име-
нующиеся переменными взаимодействия, позволяющие обнаруживать ком-
бинированные эффекты зависимости на целевую переменную, с использо-
ванием числовых предикторов. В этом разделе мы рассмотрим на примерах
степень влияния подобных признаков на целевую переменную, а также по-
говорим о создании перекрестных признаков на основе категориальных
переменных.
346 Конструирование признаков и переменных взаимодействия
Предположим, в нашем наборе данных есть три числовые переменные
HouseSize (площадь дома), NumBedrooms (количество спален) и YearBuilt (год по-
стройки), и вы считаете, что их попарные произведения могут нести больше
полезной информации о целевой переменной в сравнении с каждой пере-
менной по отдельности.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# Простой набор данных
np.random.seed(42)
data={
'HouseSize': np.random.randint(1000, 5000, 100),
'NumBedrooms': np.random.randint(1, 6, 100),
'YearBuilt': np.random.randint(1950, 2023, 100)
}
df = pd.DataFrame(data)
# Создаем перекрестные признаки
df['HouseSize_BedroomInt'] = df['HouseSize'] * df['NumBedrooms']
df['HouseSize_YearInt'] = df['HouseSize'] * df['YearBuilt']
df['Bedroom_YearInt'] = df['NumBedrooms'] * df['YearBuilt']
# Генерируем целевую переменную с зависимостью от одиночных признаков и одного
перекрестного с небольшим шумом
df['Price'] = (1000 * df['HouseSize'] +
500000 * df['NumBedrooms'] +
1000 * (df['YearBuilt'] - 1950) +
0.5 * df['HouseSize_BedroomInt'] +
np.random.normal(0, 50000, 100))
# Выведем первые несколько строк датафрейма
print(df.head(), end='\n\n')
# Визуализируем зависимости
plt.figure(figsize=(15, 10))
# Диаграмма рассеяния по признаку HouseSize и с цветовым разделением по NumBedrooms
plt.subplot(2, 2, 1)
sns.scatterplot(data=df, x='HouseSize', y='Price', hue='NumBedrooms', palette='viridis')
plt.title('Зависимость стоимости от переменной HouseSize (цвет – NumBedrooms)')
# Диаграмма рассеяния по признаку HouseSize_BedroomInt
plt.subplot(2, 2, 2)
sns.scatterplot(data=df, x='HouseSize_BedroomInt', y='Price')
plt.title('Зависимость стоимости от произведения HouseSize и NumBedrooms')
Переменные взаимодействия и значимость признаков для моделей 347
# Тепловая карта с корреляциями
plt.subplot(2, 2, 3)
sns.heatmap(df.corr(), annot=True, cmap='coolwarm', fmt='.2f')
plt.title('Тепловая карта с корреляциями')
# Распределение переменной Price
plt.subplot(2, 2, 4)
sns.histplot(data=df, x='Price', kde=True)
plt.title('Распределение переменной Price')
plt.tight_layout()
plt.show()
# Суммарная статистика
print(df_poly.describe(), end='\n\n')
# Корреляции с целевой переменной Price
print(df.corr()['Price'].sort_values(ascending=False))
Вывод:
HouseSize NumBedrooms YearBuilt HouseSize_BedroomInt HouseSize_YearInt Bedroom_YearInt
Price
0
4174
2
2000
8348
8348000
4000 5.259886e+06
1
4507
2
1993
9014
8982451
3986 5.707562e+06
2
1860
4
1973
7440
3669780
7892 3.820791e+06
3
2294
2
2008
4588
4606352
4016 3.390313e+06
4
2130
2
1981
4260
4219530
3962 3.178925e+06
HouseSize HouseSize^2 HouseSize^3
Price
count 100.000000 1.000000e+02 1.000000e+02 1.000000e+02
mean 3132.320000 1.105011e+07 4.217637e+10 5.524940e+07
std 1118.566966 6 .921442e+06 3.529126e+10 3.460694e+07
min 1021.000000 1.042441e+06 1.064332e+09 5.314426e+06
25% 2207.250000 4.872133e+06 1.075481e+10 2.435446e+07
50% 3191.500000 1.018618e+07 3.251242e+10 5.089630e+07
75% 4077.750000 1.662811e+07 6.780584e+10 8.305185e+07
max
4943.000000 2.443325e+07 1.207735e+11 1.221130e+08
Price
1.000000
HouseSize_BedroomInt 0.915972
HouseSize
0.848189
HouseSize_YearInt
0.845261
Bedroom_YearInt
0.586184
NumBedrooms
0.583655
YearBuilt
- 0 .013245
Name: Price, dtype: float64
Как видите, наибольшая корреляция с целевой переменной Price наблю-
дается у перекрестного признака HouseSize_BedroomInt.
Визуализация зависимостей в совокупности со статистическим анализом
может помочь обнаружить наиболее сильные зависимости целевой перемен-
ной от отдельных признаков и переменных взаимодействия.
348 Конструирование признаков и переменных взаимодействия
Рис. 7.7 Диаграммы рассеяния по перекрестным признакам
и тепловая карта с корреляциями
Категориальные перекрестные признаки
Также вы можете строить перекрестные признаки на основе категориаль-
ных переменных, что может помочь вам обнаружить скрытые зависимости
в наборе данных. К примеру, если в вашем наборе есть переменные Region,
отвечающая за район расположения дома, и HouseType, отвечающая за тип
постройки, вы можете объединить их и обнаружить в результате уникаль-
ные характеристики для определенных комбинаций исходных признаков,
например для квартир, расположенных в северном районе, или домов на
юге региона.
Такие перекрестные признаки могут быть особенно полезны, когда влия-
ние на целевую переменную одного категориального предиктора тесно свя-
зано со значениями другого. К примеру, влияние изменения типа дома на
стоимость может быть разным в разных районах. Создавая перекрестные
признаки на основе категориальных переменных, вы можете отлавливать
подобные нюансы взаимодействий.
Кроме того, категориальные перекрестные признаки могут помочь в борь-
бе с увеличением размерности данных за счет того, что мы можем оставить
в модели только действительно значимые комбинации признаков, восполь-
зовавшись знаниями о предметной области.
Рассмотрим пример с переменными Region и HouseType, которые объединим
в перекрестном признаке Region_HouseType:
Переменные взаимодействия и значимость признаков для моделей 349
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# Простой набор данных с категориальными переменными
np.random.seed(42)
data={
'Region': np.random.choice(['Север', 'Юг', 'Восток', 'Запад'], 100),
'HouseType': np.random.choice(['Квартира', 'Дом', 'Кондоминиум'], 100),
'Price': np.random.randint(100000, 500000, 100)
}
df = pd.DataFrame(data)
# Создаем перекрестный признак путем объединения переменных Region и HouseType
df['Reg_HT'] = df['Region'] + '_' + df['HouseType']
# Применяем к перекрестному признаку кодирование с одним активным состоянием
df_encoded = pd.get_dummies(df, columns=['Reg_HT'])
# Посмотрим на исходные данные и на закодированный датафрейм
print("Исходный датафрейм:")
print(df.head())
print("\nЗакодированный датафрейм:")
print(df_encoded.head())
# Визуализируем среднюю стоимость для каждого сочетания в признаке Region_HouseType
plt.figure(figsize=(12, 6))
sns.barplot(x='Reg_HT', y='Price', data=df)
plt.xticks(rotation=45)
plt.title('Средняя стоимость по районам и типам домов')
plt.tight_layout()
plt.show()
# Анализируем зависимости между закодированными признаками и переменной Price
correlation = df_encoded.corr(numeric_only=True)['Price'].sort_values(ascending=False)
print("\nКорреляция с целевой переменной Price:")
print(correlation)
# Применяем простую линейную регрессию с перекрестным признаком в качестве предиктора
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
X = df_encoded.drop(['Price', 'Region', 'HouseType'], axis=1)
y = df_encoded['Price']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
model = LinearRegression()
model.fit(X_train, y_train)
print("\nМетрика R-квадрат модели:", model.score(X_test, y_test))
# Выводим значимость признаков
feature_importance = pd.DataFrame({'feature': X.columns, 'importance': model.coef_})
print("\nЗначимость признаков:")
350 Конструирование признаков и переменных взаимодействия
print(feature_importance.sort_values('importance', ascending=False))
Вывод:
Исходный датафрейм:
Region HouseType Price
Reg_HT
0 Восток Кондоминиум 457403 Восток_Кондоминиум
1 Запад
Дом 343307
Запад_Дом
2 Север
Дом 300235
Север_Дом
3 Восток
Дом 125939
Восток_Дом
4 Восток
Дом 252906
Восток_Дом
Закодированный датафрейм:
Region HouseType Price ... Reg_HT_Юг_Дом Reg_HT _Юг_Квартира Reg_HT_Юг_Кондоминиум
0 Восток Кондоминиум 457403 ...
False
False
False
1 Запад
Дом 343307 ...
False
False
False
2 Север
Дом 300235 ...
False
False
False
3 Восток
Дом 125939 ...
False
False
False
4 Восток
Дом 252906 ...
False
False
False
[5 rows x 15 columns]
Корреляция с целевой переменной Price:
Price
1.000000
Reg_HT _Юг_Дом
0.200562
Reg_HT _Восток_Кондоминиум 0.174705
Reg_HT _Юг_Квартира
0.102367
Reg_HT _Север_Кондоминиум
0.064809
Reg_HT _Восток_Квартира
0.039143
Reg_HT _Юг_Кондоминиум
0.037592
Reg_HT _Запад_Дом
- 0 .037864
Reg_HT _Север_Дом
- 0 .076840
Reg_HT _Восток_Дом
- 0 .085151
Reg_HT _Запад_Квартира
- 0 .089066
Reg_HT _Север_Квартира
- 0 .127187
Reg_HT _Запад_Кондоминиум
- 0 .187524
Name: Price, dtype: float64
Метрика R-квадрат модели: -0 .02026066754352107
Значимость признаков:
feature
importance
9
Reg_HT _Юг_Дом 74628.739815
2 Reg_HT_Восток_Кондоминиум 46850.656481
10
Reg_HT _Юг_Квартира 46631.977910
8 Reg_HT _Север_Кондоминиум 32675.806481
11
Reg_HT _Юг_Кондоминиум 26700.295370
1
Reg_HT _Восток_Квартира 19279.835053
3
Reg_HT _Запад_Дом 17782.739815
4
Reg_HT _Запад_Квартира -28605.176852
6
Reg_HT _Север_Дом -29599 .164947
7
Reg_HT _Север_Квартира -55422.426852
5 Reg_HT _Запад_Кондоминиум -57768.022090
0
Reg_HT_Восток_Дом -93155.260185
Переменные взаимодействия и значимость признаков для моделей 351
Рис. 7.8 Средняя стоимость по районам и типам домов
Здесь мы объединили две категориальные переменные в один перекрестный
признак, затем применили к нему кодирование с одним активным состоянием
и воспользовались этим признаком в модели линейной регрессии для пред-
сказания стоимости домов. Если бы целевая переменная у нас была заполнена
неслучайным образом, качество модели, конечно, получилось бы получше.
На этом примере мы продемонстрировали возможность использования
перекрестных признаков, полученных на основе категориальных перемен-
ных, в процессе создания моделей машинного обучения.
7.2.3. Переменные взаимодействия
и нелинейные зависимости
Переменные взаимодействия представляют собой мощный инструмент для
обнаружения сложных нелинейных зависимостей между признаками при
построении моделей машинного обучения. Особенно полезны они бывают
при работе с моделями на основе деревьев, которые могут содержать в своей
структуре эти переменные взаимодействия. Но также они могут помочь по-
высить качество линейных моделей, таких как линейная регрессия и метод
опорных векторов.
Давайте рассмотрим наш пример с определением рыночной стоимости до-
мов, в который добавим три переменные взаимодействия, сгенерируем на их
352 Конструирование признаков и переменных взаимодействия
основе целевую переменную, построим модель и выведем статистику и гра-
фики. В этом примере мы объединим все ранее использованные наработки:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score
np.random.seed(42)
# Простой набор данных
data={
'HouseSize': np.random.randint(1000, 3000, 100),
'NumBedrooms': np.random.randint(2, 6, 100),
'YearBuilt': np.random.randint(1950, 2023, 100)
}
df = pd.DataFrame(data)
# Создаем переменные взаимодействия
df['Size_Bedrooms_Int'] = df['HouseSize'] * df['NumBedrooms']
df['Size_Year_Int'] = df['HouseSize'] * df['YearBuilt']
df['Bedrooms_Year_Int'] = df['NumBedrooms'] * df['YearBuilt']
# Генерируем целевую переменную на основе всех признаков и переменных взаимодействия
с шумом
df['Price'] = (100 * df['HouseSize'] +
50000 * df['NumBedrooms'] +
1000 * (df['YearBuilt'] - 1950) +
0.1 * df['Size_Bedrooms_Int'] +
0.05 * df['Size_Year_Int'] +
10 * df['Bedrooms_Year_Int'] +
np.random.normal(0, 50000, 100) # Add some noise
)
# Разделяем данные на признаки (X) и целевую переменную (y)
X = df[['HouseSize', 'NumBedrooms', 'YearBuilt', 'Size_Bedrooms_Int', 'Size_Year_Int',
'Bedrooms_Year_Int']]
y = df['Price']
# Разделяем набор данных на обучающую и тестовую выборки
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# Обучаем модель линейной регрессии
model = LinearRegression()
model.fit(X_train, y_train)
# Делаем предсказания на тестовой выборке
y_pred = model.predict(X_test)
# Оцениваем качество модели
mse = mean_squared_error(y_test, y_pred)
Переменные взаимодействия и значимость признаков для моделей 353
r2 = r2_score(y_test, y_pred)
print("Качество модели:")
print(f"MSE: {mse:.2f}")
print(f"R-квадрат: {r2:.2f}")
# Выводим значимость признаков
feature_importance = pd.DataFrame({'Feature': X.columns, 'Importance': model.coef_})
print("\nЗначимость признаков:")
print(feature_importance.sort_values('Importance', ascending=False))
# Визуализируем зависимости
plt.figure(figsize=(15, 10))
plt.subplot(2, 2, 1)
sns.scatterplot(data=df, x='HouseSize', y='Price', hue='NumBedrooms')
plt.title('Зависимость стоимости от HouseSize (цвет – NumBedrooms)')
plt.subplot(2, 2, 2)
sns.scatterplot(data=df, x='YearBuilt', y='Price', hue='HouseSize')
plt.title('Зависимость стоимости от YearBuilt (цвет – HouseSize)')
plt.subplot(2, 2, 3)
sns.heatmap(df.corr(), annot=True, cmap='coolwarm', fmt='.2f')
plt.title('Тепловая карта с корреляциями')
plt.subplot(2, 2, 4)
sns.residplot(x=y_pred, y=y_test - y _pred, lowess=True, color="g")
plt.xlabel('Предсказанные значения')
plt.ylabel('Остатки')
plt.title('График остатков')
plt.tight_layout()
plt.show()
# Итоговый датафрейм
print("\nИтоговый датафрейм:")
print(df.head())
Вывод:
Качество модели:
MSE: 2368126700.89
R-квадрат: 0.88
Значимость признаков:
Feature
Importance
1
NumBedrooms 263554.014861
2
YearBuilt 4247.712491
0
HouseSize
2876.718209
4
Size_Year_Int
- 1 .343193
3 Size_Bedrooms_Int
- 6 .165372
5 Bedrooms_Year_Int
-84.034882
Итоговый датафрейм:
HouseSize NumBedrooms YearBuilt ... Size_Year_Int Bedrooms_Year_Int
Price
0
2126
3
2001 ...
4254126
6003 736998.380388
1
2459
5
1953 ...
4802427
9765 845521.893113
2
1860
3
1972 ...
3667920
5916 571044.137721
354 Конструирование признаков и переменных взаимодействия
3
2294
3
1964 ...
4505416
5892 635948.282157
4
2130
3
1992 ...
4242960
5976 718354.636144
[5 rows x 7 columns]
Рис. 7.9 Диаграммы рассеяния по зависимостям между признаками,
тепловая карта с корреляциями и график остатков
Здесь мы создали в наборе данных три переменные взаимодействия и сге-
нерировали целевую переменную, зависящую от всех исходных и создан-
ных предикторов и содержащую небольшой шум для имитации реальных
условий. Затем мы создали и обучили модель линейной регрессии на основе
имеющихся признаков и вывели метрики MSE и R-квадрат. В завершение
построили и отобразили графики, на которых показана зависимость между
признаками и целевой переменной, а также тепловая карта с корреляциями
и график остатков модели.
7.2.4. Комбинирование полиномиальных
и перекрестных признаков
Допустимо комбинировать полиномиальные признаки с перекрестными для
создания еще более сложных взаимодействий между переменными. К при-
меру, вы можете возвести в квадрат перекрестный признак для обнаружения
сложных нелинейных зависимостей.
Переменные взаимодействия и значимость признаков для моделей 355
В нашем примере с определением рыночной стоимости домов можно было
бы возвести в квадрат перекрестный признак, полученный на основе произ-
ведения площади дома на количество спален. С помощью такого признака
можно, допустим, обнаружить, что влияние дополнительной спальни в доме
на его стоимость возрастает более быстро для больших домов, или найти
золотую середину для отношения площади дома к количеству спален.
Но, опять же, важно заметить, что добавление таких сложных признаков
может приводить к переобучению модели, особенно при работе с неболь-
шими наборами данных. Во избежание этого рекомендуется использовать
регуляризацию и кросс-валидацию при включении подобных предикторов.
Кроме того, сложные признаки могут негативно сказываться на интерпре-
тируемости результатов модели, так что всегда нужно искать приемлемый
компромисс между сложностью модели и возможностью объяснить ее ре-
зультаты.
Пример комбинирования полиномиальных и перекрестных признаков:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score
from sklearn.preprocessing import PolynomialFeatures
np.random.seed(42)
# Простой набор данных
data={
'HouseSize': np.random.randint(1000, 3000, 100),
'NumBedrooms': np.random.randint(2, 6, 100),
'YearBuilt': np.random.randint(1950, 2023, 100)
}
df = pd.DataFrame(data)
# Создаем перекрестные признаки
df['Size_Bedrooms_Int'] = df['HouseSize'] * df['NumBedrooms']
df['Size_Year_Int'] = df['HouseSize'] * df['YearBuilt']
df['Bedrooms_Year_Int'] = df['NumBedrooms'] * df['YearBuilt']
# Создаем полиномиальные признаки на основе перекрестных
df['Size_Bedrooms_Int_Sq'] = df['Size_Bedrooms_Int'] ** 2
df['Size_Year_Int_Sq'] = df['Size_Year_Int'] ** 2
df['Bedrooms_Year_Int_Sq'] = df['Bedrooms_Year_Int'] ** 2
# Генерируем целевую переменную на основе предикторов и их взаимодействий
df['Price'] = (100 * df['HouseSize'] +
50000 * df['NumBedrooms'] +
1000 * (df['YearBuilt'] - 1950) +
0.1 * df['Size_Bedrooms_Int'] +
0.05 * df['Size_Year_Int'] +
356 Конструирование признаков и переменных взаимодействия
10 * df['Bedrooms_Year_Int'] +
0.00001 * df['Size_Bedrooms_Int_Sq'] +
0.000005 * df['Size_Year_Int_Sq'] +
0.001 * df['Bedrooms_Year_Int_Sq'] +
np.random.normal(0, 50000, 100) # Добавляем небольшой шум
)
# Разделяем данные на признаки и целевую переменную
X = df.drop('Price', axis=1)
y = df['Price']
# Разбиваем набор данных на обучающую и тестовую выборки
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# Обучаем модель линейной регрессии
model = LinearRegression()
model.fit(X_train, y_train)
# Делаем предсказания на тестовой выборке
y_pred = model.predict(X_test)
# Оцениваем качество модели
mse = mean_squared_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)
print("Качество модели:")
print(f"Метрика MSE: {mse:.2f}")
print(f"Метрика R-квадрат: {r2:.10f}")
# Выводим значимость признаков
feature_importance = pd.DataFrame({'Feature': X.columns, 'Importance': abs(model.coef_)})
print("\nЗначимость признаков:")
print(feature_importance.sort_values('Importance', ascending=False))
# Визуализируем зависимости
plt.figure(figsize=(15, 10))
plt.subplot(2, 2, 1)
sns.scatterplot(data=df, x='Size_Bedrooms_Int', y='Price', hue='NumBedrooms')
plt.title('Зависимость стоимости от Size_Bedrooms_Int (цвет – NumBedrooms)')
plt.subplot(2, 2, 2)
sns.scatterplot(data=df, x='Size_Bedrooms_Int_Sq', y='Price', hue='YearBuilt')
plt.title('Зависимость стоимости от Size_Bedrooms_Int_Sq (цвет – YearBuilt)')
plt.subplot(2, 2, 3)
sns.heatmap(df.corr(), annot=False, cmap='coolwarm')
plt.title('Тепловая карта с корреляциями')
plt.subplot(2, 2, 4)
sns.residplot(x=y_pred, y=y_test - y _pred, lowess=True, color="g")
plt.xlabel('Предсказанные значения')
plt.ylabel('Остатки')
plt.title('График остатков')
plt.tight_layout()
plt.show()
# Итоговый датафрейм
print("\nИтоговый датафрейм:")
print(df.head())
Переменные взаимодействия и значимость признаков для моделей 357
Вывод:
Качество модели:
Метрика MSE: 2645826549.23
Метрика R-квадрат: 0.9999974263
Значимость признаков:
Feature Importance
2
YearBuilt 3695.970140
0
HouseSize 2755.484186
5
Bedrooms_Year_Int 23.699200
3
Size_Bedrooms_Int 13.014725
1
NumBedrooms 10.100177
4
Size_Year_Int
1.340071
8 Bedrooms_Year_Int_Sq
0.002174
6 Size_Bedrooms_Int_Sq
0.000666
7
Size_Year_Int_Sq
0.000005
Итоговый датафрейм:
HouseSize NumBedrooms YearBuilt ... Size_Year_Int_Sq Bedrooms_Year_Int_Sq
Price
0
2126
3
2001 ... 18097588023876
36036009 9.126138e+07
1
2459
5
1953 ... 23063305090329
95355225 1.162589e+08
2
1860
3
1972 ... 13453637126400
34999056 6.787454e+07
3
2294
3
1964 ... 20298773333056
34715664 1.021650e+08
4
2130
3
1992 ... 18002709561600
35712576 9.076802e+07
[5 rows x 10 columns]
Рис. 7.10 Диаграммы рассеяния по зависимостям между сложными признаками,
тепловая карта с корреляциями и график остатков
358 Конструирование признаков и переменных взаимодействия
Здесь мы заполнили исходные данные, после чего создали переменные
взаимодействия и дополнительно возвели их в квадрат. Целевую перемен-
ную мы сгенерировали таким образом, чтобы она зависела от всех созданных
признаков. Затем создали и обучили модель линейной регрессии, сделали
предсказания на тестовой выборке и вывели значения метрик. В заверше-
ние мы вывели графики с зависимостями целевой переменной от сложных
признаков, тепловой картой с корреляциями и графиком остатков модели.
Подобные графики и статистический анализ могут помочь в выборе под-
ходящих признаков для модели.
7.3. Практические упражнения
Теперь перейдем к решению практических задач по темам, пройденным
в этой главе.
Упражнение 1. Применение логарифмического
преобразования
Есть набор данных с переменной Income с небольшим перекосом в правую
сторону:
import numpy as np
import pandas as pd
# Простой набор данных
data = {'Income': [30000, 50000, 75000, 120000, 250000]}
df = pd.DataFrame(data)
Ваша задача состоит в том, чтобы создать новый признак LogIncome, при-
менив к переменной Income логарифмическое преобразование.
Решение
# Применяем логарифмическое преобразование для создания признака LogIncome
df['LogIncome'] = np.log(df['Income'])
# Выводим исходный и новый признаки
print(df)
Вывод:
Income LogIncome
0 30000 10.308953
1 50000 10.819778
2 75000 11.225243
3 120000 11.695247
4 250000 12.429216
Практические упражнения 359
Упражнение 2. Извлечение компонентов из дат
Есть набор данных с переменной SaleDate, содержащей фактические даты
продажи домов:
import pandas as pd
# Простой набор данных с колонкой с датами
data = {'SaleDate': ['2022-01-05', '2021-06-15', '2020-09-22', '2019-11 -30']}
df = pd.DataFrame(data)
Ваша задача состоит в том, чтобы извлечь из столбца SaleDate следующие
компоненты: YearSold (год продажи), MonthSold (месяц продажи) и DayOfWeek-
Sold (день недели, в которую была осуществлена продажа).
Решение
# Приводим столбец SaleDate к типу данных datetime
df['SaleDate'] = pd.to_datetime(df['SaleDate'])
# Извлекаем нужные нам компоненты: год, месяц и день недели
df['YearSold'] = df['SaleDate'].dt.year
df['MonthSold'] = df['SaleDate'].dt.month
df['DayOfWeekSold'] = df['SaleDate'].dt.dayofweek
# Выводим итоговый датафрейм
print(df)
Вывод:
SaleDate YearSold MonthSold DayOfWeekSold
0 2022-01-05
2022
1
2
1 2021-06-15
2021
6
1
2 2020-09-22
2020
9
1
3 2019-11 -30
2019
11
5
Упражнение 3. Создание перекрестных признаков
Есть набор данных с переменными HouseSize (площадь дома в кв. футах)
и NumBedrooms (количество спален в доме):
import pandas as pd
# Простой набор данных
data = {'HouseSize': [2000, 2500, 3000, 3500, 4000],
'NumBedrooms': [3, 4, 4, 5, 6]}
df = pd.DataFrame(data)
Ваша задача состоит в том, чтобы создать перекрестный признак PricePer-
Bedroom путем деления переменной HouseSize на NumBedrooms. Таким образом
мы можем нормализовать площадь домов по количеству спален.
360 Конструирование признаков и переменных взаимодействия
Решение
# Создаем новый перекрестный признак
df['PricePerBedroom'] = df['HouseSize'] / df['NumBedrooms']
# Выводим итоговый датафрейм
print(df)
Вывод:
HouseSize NumBedrooms PricePerBedroom
0
2000
3
666.666667
1
2500
4
625.000000
2
3000
4
750.000000
3
3500
5
700.000000
4
4000
6
666.666667
Упражнение 4. Создание полиномиальных признаков
Есть набор данных с единственной переменной Age:
import pandas as pd
from sklearn.preprocessing import PolynomialFeatures
# Простой набор данных
data = {'Age': [25, 30, 35, 40, 45]}
df = pd.DataFrame(data)
Ваша задача состоит в том, чтобы создать полиномиальные признаки вто-
рой и третьей степеней для переменной Age.
Решение
# Инициализируем класс PolynomialFeatures для степеней 2 и 3
poly = PolynomialFeatures(degree=3, include_bias=False)
# Генерируем полиномиальные признаки
polynomial_features = poly.fit_transform(df[['Age']])
# Создаем датафрейм с исходным и полиномиальными признаками
df_poly = pd.DataFrame(polynomial_features, columns=['Age', 'Age^2', 'Age^3'])
# Выводим итоговый датафрейм
print(df_poly)
Вывод:
Age Age^2 Age^3
0 25.0 625.0 15625.0
1 30.0 900.0 27000.0
2 35.0 1225.0 42875.0
3 40.0 1600.0 64000.0
4 45.0 2025.0 91125.0
Практические упражнения 361
Упражнение 5. Создание переменных взаимодействия
Есть набор данных с переменными HousePrice, HouseSize и YearBuilt:
import pandas as pd
# Простой набор данных
data = {'HousePrice': [300000, 500000, 700000],
'HouseSize': [1500, 2000, 2500],
'YearBuilt': [1990, 2000, 2010]}
df = pd.DataFrame(data)
Ваша задача состоит в том, чтобы создать три следующих признака взаи-
модействия: Price_Size_Int (HousePrice * HouseSize), Price_Year_Int (HousePrice
* YearBuilt) и Size_Year_Int (HouseSize * YearBuilt).
Решение
# Создаем переменные взаимодействия
df['Price_Size_Int'] = df['HousePrice'] * df['HouseSize']
df['Price_Year_Int'] = df['HousePrice'] * df['YearBuilt']
df['Size_Year_Int'] = df['HouseSize'] * df['YearBuilt']
# Выводим итоговый датафрейм
print(df)
Вывод:
HousePrice HouseSize YearBuilt Price_Size_Int Price_Year_Int Size_Year_Int
0
300000
1500
1990
450000000
597000000
2985000
1
500000
2000
2000
1000000000
1000000000
4000000
2
700000
2500
2010
1750000000
1407000000
5025000
Упражнение 6. Комбинирование полиномиальных
и перекрестных признаков
Продолжим работать с набором данных, полученным в результате выполне-
ния упражнения 5.
Ваша задача состоит в том, чтобы создать новый признак путем возведе-
ния в квадрат переменной взаимодействия Price_Size_Int.
Решение
# Создаем полиномиальный признак путем возведения в квадрат переменной Price_Size_Int
df['Price_Size_Int_Sq'] = df['Price_Size_Int'] ** 2
# Выводим итоговый датафрейм
print(df)
Вывод:
HousePrice HouseSize YearBuilt ... Price_Year_Int Size_Year_Int Price_Size_Int_Sq
0
300000
1500
1990 ...
597000000
2985000 202500000000000000
362 Конструирование признаков и переменных взаимодействия
1
500000
2000
2000 ...
1000000000
4000000 1000000000000000000
2
700000
2500
2010 ...
1407000000
5025000 3062500000000000000
[3 rows x 7 columns]
7.4. Возможные проблемы
Создание новых признаков на основе существующих с применением раз-
личных преобразований способно существенно повысить качество итоговой
модели, но, как и следовало ожидать, также сопряжено с определенными
сложностями. При неправильном применении описанных техник могут воз-
никнуть проблемы с мультиколлинеарностью признаков, переобучением
модели или ее избыточной сложностью. В этом разделе мы перечислим воз-
можные подводные камни в данной области и подскажем, как их избежать.
7.4.1. Переобучение модели при использовании
избыточного количества признаков
Создание большого количества признаков, особенно полиномиальных и пе-
рекрестных, может приводить к возникновению переобучения модели и сни-
жению ее обобщающей способности.
Что может пойти не так:
переобучение может возникать в результате чрезмерного повышения
сложности модели из-за наличия излишне большого числа признаков;
переобучению в большей степени подвержены модели, построенные на
основе небольших наборов данных, поскольку новые признаки в них
могут обнаруживать случайно возникшие закономерности.
Решение:
всегда применяйте кросс-валидацию при оценке качества модели и сле-
дите за тем, чтобы новые признаки повышали обобщающую способ-
ность модели, а не только улучшали ее точность на обучающих данных;
применяйте техники регуляризации (такие как L1- и L2-регуляризация),
позволяющие штрафовать модели за излишнюю сложность и снижаю-
щие риск возникновения переобучения;
избегайте создания избыточных признаков. Каждый раз проверяйте,
скажется ли добавляемый предиктор на качестве предсказаний.
7.4.2. Возникновение мультиколлинеарности
Создание перекрестных и полиномиальных признаков может приводить
к возникновению мультиколлинеарности, т. е. к ситуации, когда два или
Возможные проблемы 363
несколько признаков сильно коррелируют друг с другом. Построенные в ре-
зультате линейные модели могут отличаться нестабильностью, и может быть
затруднена процедура оценки значимости признаков и интерпретации ко-
эффициентов.
Что может пойти не так:
мультиколлинеарность может стать причиной присваивания не-
корректных коэффициентов некоторым признакам или приводить
к излишней чувствительности модели к небольшим изменениям
в данных;
в таких моделях, как линейная регрессия, мультиколлинеарность мо-
жет затруднять процесс интерпретации коэффициентов в связи с их
большими изменениями при незначительных корректировках данных.
Решение:
воспользуйтесь фактором инфляции дисперсии (Variance Inflation Fac-
tor – VIF) для определения и исключения сильно коррелирующих при-
знаков из набора данных;
рассмотрите вариант удаления одной из коррелирующих переменных
или примените техники снижения размерности, такие как анализ глав-
ных компонент, позволяющие объединить коррелирующие предикто-
ры вместе;
воспользуйтесь техниками регуляризации, позволяющими уменьшить
коэффициенты сильно коррелирующих признаков.
7.4.3. Добавление в модель избыточных признаков
Не поддавайтесь соблазну создания чересчур большого количества пере-
менных взаимодействия, поскольку не все из них могут положительно ска-
зываться на качестве модели. Иногда дополнительные признаки способны
приводить к обратным результатам.
Что может пойти не так:
добавление избыточных признаков в модель может вести к «зашумле-
нию» данных и снижению обобщающей способности модели;
может пострадать интерпретируемость результатов модели.
Решение:
воспользуйтесь техниками отбора признаков, такими как рекурсивное
исключение признаков (recursive feature elimination – RFE), для поиска
и удаления избыточных переменных;
примените такие техники, как вычисление важности признаков (per-
mutation importance) или вычисление SHAP-значений (SHAP value), для
определения того, какие признаки на самом деле добавляют качества
модели;
регулярно проверяйте влияние новых признаков на модель с исполь-
зованием техник кросс-валидации.
364 Конструирование признаков и переменных взаимодействия
7.4.4. Ошибки при интерпретации перекрестных
признаков
Добавление перекрестных признаков может дать модели массу полезной ин-
формации о взаимодействии переменных, но вместе с тем их бывает трудно
интерпретировать, если неправильно понять зависимости между исходными
предикторами. Таким образом, создание подобных признаков без хорошего
знания предметной области может обернуться неправильно сделанными
выводами.
Что может пойти не так:
добавление перекрестных признаков, не имеющих ничего общего с ре-
шаемой задачей, может приводить к ухудшению качества модели;
неправильная интерпретация переменных взаимодействия может
приводить к ошибочным предположениям относительно существую-
щих зависимостей между переменными, вследствие чего модель будет
полагаться на взаимодействия, которых на самом деле нет.
Решение:
всегда проверяйте, что ваши переменные взаимодействия базируются
на действительно существующих зависимостях и отражают здравый
смысл. Избегайте создания избыточных переменных просто потому,
что можете;
визуализируйте взаимодействия между переменными, прежде чем
включать создаваемые признаки в модель;
если новый признак не влияет на качество модели положительным
образом, задумайтесь о том, стоит ли его вообще включать в модель.
7.4.5. Проблемы с производительностью
при использовании полиномиальных признаков
в больших наборах данных
Применение полиномиальных признаков с большой степенью может при-
водить к образованию большого количества новых предикторов, особенно
когда речь идет о наборах данных с большим числом исходных переменных.
Это может замедлять процесс обучения, увеличивать объем расходуемой
памяти и затруднять интерпретацию модели.
Что может пойти не так:
в больших наборах данных добавление излишнего количества поли-
номиальных признаков может повлечь за собой чрезмерное расходо-
вание ресурсов;
Заключение 365
с увеличением количества признаков модель утрачивает интерпрети-
руемость, что выражается в затруднениях при объяснении зависимо-
стей между признаками и целевой переменной.
Решение:
ограничивайте степень при создании полиномиальных признаков зна-
чением 2 или 3;
воспользуйтесь техниками снижения размерности для сокращения
количества признаков после применения полиномиальных преобра-
зований;
применяйте полиномиальные преобразования выборочно – только
к наиболее важным для модели переменным.
Заключение
В этой главе мы рассмотрели различные способы обогащения моделей ма-
шинного обучения за счет новых признаков. Зачастую исходных перемен-
ных, имеющихся в наборе данных, оказывается недостаточно для обнаруже-
ния всех зависимостей между ними и целевой переменной. Создавая новые
предикторы и переменные взаимодействия, мы пытаемся извлечь важные
шаблоны из данных, недоступные в их исходном виде.
В начале главы мы рассмотрели вопросы, связанные с созданием призна-
ков с использованием математических преобразований, а также научились
извлекать компоненты из переменных с датой и временем. Затем обрати-
лись к объемной теме, связанной с созданием переменных взаимодействия,
полиномиальных признаков и их комбинирования.
Далее вас ждет вторая часть контрольного опроса и второй большой про-
ект, посвященный прогнозированию временных рядов.
Контрольный опрос.
Часть II. Конструирование
признаков для сложных
моделей
В этом опросе вы сможете проверить, как усвоили материал второй части
книги. Каждый вопрос будет посвящен конкретному разделу. Ответьте на
вопросы, чтобы убедиться, что вы готовы двигаться дальше.
Вопрос 1: роль конструирования признаков в машинном
обучении
Почему конструирование признаков считается одним из ключевых аспектов
построения моделей машинного обучения?
a) с помощью него можно увеличить количество признаков в наборе дан-
ных;
b) конструирование признаков помогает преобразовать сырые данные
в набор значимых атрибутов, что позволяет повысить качество мо-
дели;
c) помогает снизить количество наблюдений в наборе данных;
d) устраняет необходимость выполнения предварительной подготовки
данных.
Вопрос 2: техники работы с пропущенными значениями
Какой из перечисленных ниже методов не является приемом работы с про-
пущенными значениями?
a) метод подстановки на основе среднего;
b) удаление строк с пропущенными значениями;
c) метод подстановки на основе случайных значений из набора данных;
d) кодирование по меткам.
Контрольный опрос. Часть II. Конструирование признаков для сложных моделей 367
Вопрос 3: техники работы с пропущенными значениями
Какой из продвинутых методов подстановки пропущенных значений следует
использовать, если нам очень важно сохранить шаблоны зависимости между
переменными?
a) метод подстановки на основе медианы;
b) метод подстановки на основе KNN;
c) метод подстановки на основе среднего;
d) метод подстановки на основе моды.
Вопрос 4: преобразование и масштабирование признаков
Какая техника преобразования признаков лучше подходит для стабилизации
дисперсии и выравнивания перекосов в распределении при наличии в дан-
ных только положительных значений?
a) кодирование с одним активным состоянием;
b) стандартизация;
c) логарифмическое преобразование;
d) порядковое кодирование.
Вопрос 5: преобразование и масштабирование признаков
В чем состоит основное отличие минимаксного масштабирования от стан-
дартизации?
a) минимаксное масштабирование приводит значения к фиксированно-
му диапазону, тогда как стандартизация размещает их вокруг нуля
с единичным стандартным отклонением;
b) минимаксное масштабирование помогает уменьшить набор данных,
тогда как стандартизация приводит к увеличению размерности;
c) минимаксное масштабирование применяется только для категориаль-
ных переменных, а стандартизация – для числовых;
d) минимаксное масштабирование помогает избавиться от выбросов,
а стандартизация их игнорирует.
Вопрос 6: кодирование категориальных переменных
В чем состоит главное ограничение метода кодирования с одним активным
состоянием при работе с категориальными переменными с высокой карди-
нальностью?
a) этот метод нельзя применять к числовым переменным;
b) этот метод уменьшает размер набора данных;
c) этот метод может приводить к образованию большого количества
столбцов;
d) использование этого метода приводит к исключению из набора редко
встречающихся категорий.
368 Контрольный опрос. Часть II. Конструирование признаков для сложных моделей
Вопрос 7: кодирование категориальных переменных
Какой метод кодирования позволяет заменить значения в категориальном
столбце на средние значения целевой переменной по соответствующим ка-
тегориям?
a) кодирование с одним активным состоянием;
b) кодирование на основе частоты;
c) кодирование на основе целевой переменной;
d) порядковое кодирование.
Вопрос 8: создание признаков и переменных
взаимодействия
Какова основная цель создания переменных взаимодействия на основе су-
ществующих предикторов?
a) снижение сложности модели;
b) извлечение информации о совокупном влиянии предикторов на целе-
вую переменную;
c) исключение из набора данных сильно коррелирующих переменных;
d) применение преобразований только к категориальным переменным.
Вопрос 9: создание признаков и переменных
взаимодействия
Какой потенциальный риск стоит учитывать при создании полиномиальных
признаков?
a) утечка информации;
b) мультиколлинеарность и переобучение;
c) переменные могут стать категориальными;
d) уменьшение размера набора данных.
Вопрос 10: общие сведения
Какую технику конструирования признаков стоит применить, если вы подо-
зреваете наличие нелинейных зависимостей между числовыми предиктора-
ми и целевой переменной?
a) метод подстановки на основе среднего;
b) создание полиномиальных признаков;
c) кодирование с одним активным состоянием;
d) минимаксное масштабирование.
Контрольный опрос. Часть II. Конструирование признаков для сложных моделей 369
Дополнительный вопрос: общие сведения
Какой метод отбора признаков помогает узнать, какие предикторы вносят
наибольший вклад в качество модели, одновременно снижая шум от незна-
чимых переменных?
a) рекурсивное исключение признаков (recursive feature elimination – RFE);
b) метод подстановки на основе случайных значений из набора данных;
c) порядковое кодирование;
d) кодирование по меткам.
Ответы
Вопрос 1
Правильный ответ – b: конструирование признаков помогает преобразовать
сырые данные в набор значимых атрибутов, что позволяет повысить качест-
во модели.
Вопрос 2
Правильный ответ – d: кодирование по меткам. Этот метод используется
для преобразования категориальных переменных, а не для обработки про-
пущенных значений.
Вопрос 3
Правильный ответ – b: метод подстановки на основе KNN.
Вопрос 4
Правильный ответ – c: логарифмическое преобразование.
Вопрос 5
Правильный ответ – a: минимаксное масштабирование приводит значения
к фиксированному диапазону, тогда как стандартизация размещает их во-
круг нуля с единичным стандартным отклонением.
Вопрос 6
Правильный ответ – c: этот метод может приводить к образованию большого
количества столбцов.
Вопрос 7
Правильный ответ – c: кодирование на основе целевой переменной.
Вопрос 8
Правильный ответ – b: извлечение информации о совокупном влиянии пре-
дикторов на целевую переменную.
370 Контрольный опрос. Часть II. Конструирование признаков для сложных моделей
Вопрос 9
Правильный ответ – b: мультиколлинеарность и переобучение.
Вопрос 10
Правильный ответ – b: создание полиномиальных признаков.
Дополнительный вопрос
Правильный ответ – a: рекурсивное исключение признаков (RFE).
ЧАСТЬ III
Очистка
и предварительная
обработка данных
Проект 2
Прогнозирование
временных рядов
с конструированием
признаков
1
В этом проекте мы поработаем с одной из самых востребованных областей
машинного обучения, а именно с прогнозированием временных рядов (time
series forecasting). Данные на основе временных рядов окружают нас повсю-
ду: на них построены все финансовые и биржевые системы, анализ продаж
и даже прогноз погоды. Способность с достаточной точностью прогнози-
ровать временные ряды позволяет бизнесу принимать важные решения на
основе данных, оптимизировать использование ресурсов, обнаруживать по-
тенциальные риски и строить стратегические планы на будущее.
В основе прогнозирования временных рядов лежит использование шаб-
лонов исторических данных для предсказания тенденций будущих времен.
В этом проекте мы погрузимся в мир временных рядов, но основной упор
будет сделан на аспекте конструирования признаков, которому, как вы пом-
ните, и посвящена данная книга. Хотя мы будем использовать в своей ра-
боте традиционные методы прогнозирования временных рядов, такие как
авторегрессионное интегрированное скользящее среднее (Autoregressive Inte-
grated Moving Average – ARIMA) и экспоненциальное сглаживание (exponential
smoothing), основное внимание мы будем уделять тому, как использование
продвинутых техник конструирования признаков может помочь существен-
но улучшить качество предсказаний моделей на основе временных данных.
Мы узнаем, какие приемы можно применять с целью повышения предсказа-
тельной способности таких моделей, как случайный лес, классический и экс-
тремальный градиентный бустинг (XGBoost) и др.
1
Переработанная и дополненная реализация проекта в редакции А. Гинько.
Введение в прогнозирование временных рядов с использованием конструирования 373
Комбинируя мощные предсказательные модели с продвинутыми техни-
ками конструирования признаков, вы сможете сделать значительный шаг
вперед в отношении точности прогнозирования временных рядов.
Введение в прогнозирование
временных рядов с использованием
конструирования признаков
Основной целью прогнозирования временных рядов является предсказание
будущих значений на основе исторических данных. Временные ряды пред-
ставляют собой совершенно уникальную структуру данных, в которой поря-
док следования наблюдений имеет решающее значение, а все последующие
значения так или иначе связаны с наблюдениями предыдущих периодов.
Эта особенность делает временные ряды такими сложными для прогнози-
рования, но вместе с тем открывает новые возможности для обнаружения
скрытых шаблонов в данных.
Для извлечения максимума полезной информации из временных рядов
вам может понадобиться обогатить данные за счет новых признаков, позво-
ляющих обнаруживать зависимости на основе времени. Мы в нашем проекте
воспользуемся следующими техниками:
исследуем временные компоненты, такие как месяц или день недели,
а также признаки на основе временного лага, позволяющие учитывать
значения предшествующих периодов;
обсудим возможности использования скользящих показателей для об-
наружения общих трендов и сезонных тенденций;
поработаем с разными техниками удаления тренда, или детрендиро-
вания, и специальными преобразованиями, позволяющими привести
временные ряды к более стационарному виду.
В проекте мы спрогнозируем будущие показатели и продемонстрируем,
как конструирование признаков может позволить улучшить качество пред-
сказаний.
Признаки на основе временного лага
в прогнозировании временных рядов
Одной из фундаментальных техник при работе с временными рядами явля-
ется создание признаков на основе временного лага (lag feature). Такие при-
знаки проистекают из исходных данных, но содержат в себе определенные
временные сдвиги. Эти сдвиги позволяют модели использовать историче-
374 Прогнозирование временных рядов с конструированием признаков
ские данные при прогнозировании показателей будущих периодов. Величи-
на сдвига может варьироваться, и в наборе данных может быть создано сразу
несколько признаков на основе временного лага для отслеживания разных
исторических периодов.
Признаки на основе временного лага представляют собой достаточно
мощный инструмент, позволяющий модели учитывать автокорреляцию (au-
tocorrelation), отражающую зависимость между текущим значением пере-
менной и значениями предыдущих периодов. Это критически важно при
работе с временными рядами, где шаблоны часто повторяются или имеют
одинаковые тренды с течением времени. К примеру, сегодняшние цены ак-
ций на фондовой бирже могут некоторым образом зависеть от вчерашних
показателей, а также показателей недельной или даже месячной давности.
Создавая признаки на основе временного лага, мы обеспечиваем нашу мо-
дель этой важной информацией.
Почему признаки на основе временного лага так важны?
Значимость признаков, построенных на базе сдвигов во времени, проистека-
ет из природы задач, решаемых в сценариях с временными рядами. В таких
задачах текущее значение целевой переменной зачастую неразрывно свя-
зано с показателями предыдущих периодов, что называется временной за-
висимостью (temporal dependency). Этот вид зависимости может проявляться
в самых разных формах:
краткосрочные эффекты: на текущее значение переменной могут ока-
зывать сильное влияние недавние показатели. К примеру, объем про-
даж товара сегодня, скорее всего, будет напрямую зависеть от продаж
за последние несколько дней;
сезонные шаблоны: во многих областях применения временные ряды
содержат циклические шаблоны, связанные с определенными времен-
ными интервалами. Допустим, пики продаж в розничных сетях обычно
приходятся на праздничные дни, и этот шаблон сохраняется из года
в год;
долгосрочные тенденции: некоторые временные ряды характеризу-
ются постепенным изменением значений с течением длительного
времени. К примеру, определенные экономические показатели мо-
гут демонстрировать устойчивые тренды на протяжении многих лет,
которые также можно обнаружить при помощи признаков на основе
временного лага.
Стоит отметить, что количество и диапазоны признаков на основе вре-
менного лага могут варьироваться в зависимости от поставленной задачи
и имеющегося набора данных. Здесь вам как никогда могут помочь знания
о предметной области.
Кроме того, гибкость некоторых методов машинного обучения позволяет
им автоматически определять относительную важность созданных призна-
ков на основе временного лага, используя при предсказании только те из
Введение в прогнозирование временных рядов с использованием конструирования 375
них, которые положительно сказываются на качестве модели. Такой подход
к отбору признаков зачастую превосходит в эффективности традиционные
методы работы с временными рядами, полагающиеся на фиксированные
предопределенные структуры.
Пример создания признаков на основе временного лага
Давайте начнем с примера создания дополнительных признаков в наборе
данных по продажам. Допустим, у нас есть данные с ежедневными показа-
телями продаж, и нам необходимо спрогнозировать будущие показатели на
основе имеющихся.
import pandas as pd
# Простой набор данных: ежедневные показатели продаж
data = {'Date': pd.date_range(start='2022-01-01', periods=10, freq='D'),
'Sales': [100, 120, 130, 150, 170, 160, 155, 180, 190, 210]}
df = pd.DataFrame(data)
# Установим в качестве индекса в датафрейме столбец Date
df.set_index('Date', inplace=True)
# Создаем признаки на основе временного лага для 1, 2 и 3 дней
df['Sales_Lag1'] = df['Sales'].shift(1)
df['Sales_Lag2'] = df['Sales'].shift(2)
df['Sales_Lag3'] = df['Sales'].shift(3)
# Выводим датафрейм с новыми признаками
print(df)
Вывод:
Sales Sales_Lag1 Sales_Lag2 Sales_Lag3
Date
2022-01-01 100
NaN
NaN
NaN
2022-01-02 120
100.0
NaN
NaN
2022-01-03 130
120.0
100.0
NaN
2022-01-04 150
130.0
120.0
100.0
2022-01-05 170
150.0
130.0
120.0
2022-01-06 160
170.0
150.0
130.0
2022-01-07 155
160.0
170.0
150.0
2022-01-08 180
155.0
160.0
170.0
2022-01-09 190
180.0
155.0
160.0
2022-01-10 210
190.0
180.0
155.0
В этом примере мы сначала создаем простой датафрейм с датами и сум-
мами продажи, после чего устанавливаем в качестве индекса столбец Date
и создаем три новых столбца со смещением на один, два и три дня соответ-
ственно.
При создании этих столбцов мы воспользовались удобным методом
shift(), позволяющим сдвинуть данные вперед или назад на заданное ко-
личество интервалов.
376 Прогнозирование временных рядов с конструированием признаков
Этот прием является одним из основных при работе с временными ряда-
ми, поскольку он позволяет модели воспользоваться для прогнозирования
значениями из предыдущих периодов, т. е . реализовать обнаружение вре-
менных зависимостей.
Обработка пропущенных значений в признаках
на основе временного лага
При создании признаков на основе временного лага начальные строки
датафрейма неминуемо будут содержать в этих столбцах пропущенные
значения в связи с отсутствием предыдущих периодов. Это распростра-
ненная ситуация при работе с временными рядами, которая требует осо-
бого внимания. Существует несколько способов обработки пропусков во
временных рядах, каждый из которых обладает своими достоинствами
и недостатками.
1. Удаление строк с пропущенными значениями. Этот прямолиней-
ный подход может приводить к потере данных и потенциальной по-
грешности модели, если пропущенные значения распределены неслу-
чайным образом. Вы можете воспользоваться им, если данных у вас
достаточно много и вы можете пожертвовать их малой частью.
2. Замена пропущенных значений. Этот подход подразумевает под-
становку пропусков в данных с использованием разных техник. Ниже
перечислены самые популярные методы:
• прямое заполнение, заключающееся в использовании последнего ва-
лидного значения в столбце для замены текущего пропуска. При
использовании этого метода мы предполагаем, что пропущенные
значения обладают наиболее схожими характеристиками с преды-
дущими известными значениями;
• обратное заполнение аналогично прямому, но пропуски заполняются
первыми валидными значениями в столбце, идущими после пропу-
щенных. Этот подход можно применять, когда у вас есть достаточно
оснований полагать, что тенденции характеристик распространяют-
ся в столбце в обратном направлении;
• заполнение на основе медианы/среднего: мы уже применяли эти мето-
ды в предыдущих главах книги. Использовать их можно тогда, когда
данные распределены относительно нормально и не характеризу-
ются явными трендами и сезонностью;
• интерполяция, состоящая в оценке пропущенных значений на осно-
ве окружающих их заполненных значений. В зависимости от при-
роды ваших данных вы можете воспользоваться линейной, полино-
миальной или сплайновой интерполяцией.
3.
Использование модели машинного обучения для заполнения про-
пусков. Современные модели машинного обучения, включая некото-
рые реализации алгоритма градиентного бустинга (LightGBM, CatBoost
Признаки на основе скользящего окна для обнаружения трендов и сезонности 377
и др.), способны работать с пропущенными значениями без выполне-
ния явной подстановки. Такие модели часто воспринимают пропуски
как отдельную категорию значений и используют эти шаблоны в про-
цессе обучения.
4. Создание отдельных индикаторов для пропущенных значений.
Этот подход заключается в создании двоичных столбцов с индикато-
рами, говорящими о том, пропущено значение в соответствующем на-
блюдении или нет. Это бывает особенно полезно, когда сам факт про-
пуска несет ценную информацию.
5. Использование знаний о предметной области. Иногда в вашем рас-
поряжении оказывается достаточно сведений о предметной области,
чтобы в ручном режиме обработать пропущенные значения. Напри-
мер, вы можете знать, что в определенные дни ваш розничный магазин
не работал, в связи с чем в данных о продажах возникли пропуски.
Выбор метода подстановки пропусков зависит от множества факторов,
включая размер набора данных, природу временных рядов, требования к за-
даче прогнозирования и имеющиеся у вас сведения о пропущенных значени-
ях. Зачастую бывает полезно опробовать разные техники замены значений
и при помощи специальной разновидности кросс-валидации, подходящей
для временных рядов, выбрать наиболее оптимальный.
Признаки на основе скользящего окна
для обнаружения трендов и сезонности
Еще одной мощной техникой для работы с временными рядами является
создание признаков на основе скользящего окна (rolling window feature). Такие
признаки позволяют отлавливать имеющиеся в данных тренды и сезонность
путем агрегирования значений в рамках плавающего окна. Отслеживая зна-
чения сразу нескольких идущих подряд наблюдений, мы получаем возмож-
ность обнаружить динамические тренды в данных с течением времени.
Наиболее распространенные скользящие показатели:
скользящее среднее: этот показатель позволяет эффективно сглаживать
краткосрочные колебания в данных, что дает возможность сконцент-
рироваться на долгосрочных трендах. К примеру, 7-дневный размер
скользящего окна может подсветить недельные тренды продаж, что
облегчит процедуру закупок;
скользящая медиана: представляя собой надежную меру центральной
тенденции, медиана выигрывает у среднего значения при работе с на-
борами данных, характеризующимися выбросами. Выбор серединного
значения показателя в скользящем окне позволяет получить более на-
дежное представление о центральной тенденции данных, что делает
378 Прогнозирование временных рядов с конструированием признаков
эту меру полезной в сценариях с возможными перекосами распреде-
лений, например в финансовой сфере;
скользящее стандартное отклонение: эта мера позволяет количественно
выразить изменчивость дисперсии переменной с течением времени,
что дает возможность судить о стабильности и предсказуемости вре-
менного ряда. Увеличение скользящего стандартного отклонения мо-
жет говорить о периодах с большой волатильностью показателя, что бы-
вает полезно при оценке риска и принятии решений на основе данных;
скользящий минимум и максимум: эти меры могут быть полезны для
обнаружения подъемов и спадов показателя во временных рядах, что
позволяет судить о диапазоне данных и крайних точках в конкретном
временном интервале. Данные меры часто используются в таких сфе-
рах, как анализ фондовой биржи, где понимание границ цены может
влиять на выбор торговой стратегии, или прогноз погоды, где анализ
пиковых значений температуры помогает предсказать те или иные
погодные явления.
Размер окна определяется исходя из конкретных задач и имеющегося
набора данных. Чем больше размер окна, тем более долгосрочные тенден-
ции вы сможете отследить, но тем менее чувствительна будет ваша модель
к краткосрочным изменениям.
Пример использования признаков на основе скользящего окна
Давайте продолжим работать с нашим набором данных по продажам и соз-
дадим несколько признаков на основе скользящего окна. В частности, мы
рассчитаем 7-дневное скользящее среднее и 7-дневное скользящее стандарт-
ное отклонение для обнаружения общего тренда продаж и их изменчивости:
# Простой набор данных: ежедневные показатели продаж
import pandas as pd
data = {'Date': pd.date_range(start='2022-01-01', periods=15, freq='D'),
'Sales': [100, 120, 130, 150, 170, 160, 155, 180, 190, 210, 220, 230, 225, 240, 260]}
df = pd.DataFrame(data)
# Установим в качестве индекса в датафрейме столбец Date
df.set_index('Date', inplace=True)
# Создаем признаки на основе 7-дневного скользящего среднего и 7-дневного скользящего
стандартного отклонения
df['RollingMean_7'] = df['Sales'].rolling(window=7).mean()
df['RollingStd_7'] = df['Sales'].rolling(window=7).std()
# Выводим датафрейм со скользящими признаками
print(df)
Вывод:
Sales RollingMean_7 RollingStd_7
Date
2022-01-01 100
NaN
NaN
Признаки на основе скользящего окна для обнаружения трендов и сезонности 379
2022-01-02 120
NaN
NaN
2022-01-03 130
NaN
NaN
2022-01-04 150
NaN
NaN
2022-01-05 170
NaN
NaN
2022-01-06 160
NaN
NaN
2022-01-07 155
140.714286
24.904580
2022-01-08 180
152.142857
21.185125
2022-01-09 190
162.142857
19.970216
2022-01-10 210
173.571429
21.353041
2022-01-11 220
183.571429
24.616100
2022-01-12 230
192.142857
29.134336
2022-01-13 225
201.428571
27.494588
2022-01-14 240
213.571429
21.739803
2022-01-15 260
225.000000
22.173558
Здесь мы воспользовались методом rolling() для создания двух 7-дневных
скользящих показателей.
Если вы не хотите, чтобы в первых записях в датафрейме появлялись про-
пущенные значения, вы можете передать методу rolling() дополнительный
параметр min_periods, отвечающий за минимальное количество наблюдений
в окне, достаточное для расчета значений. Допустим, если в предыдущем
примере вычисление скользящих показателей прописать следующим об-
разом:
df['RollingMean_7'] = df['Sales'].rolling(window=7, min_periods=1).mean()
df['RollingStd_7'] = df['Sales'].rolling(window=7, min_periods=1).std()
то результаты будут выглядеть так:
Sales RollingMean_7 RollingStd_7
Date
2022-01-01 100
100.000000
NaN
2022-01-02 120
110.000000
14.142136
2022-01-03 130
116.666667
15.275252
2022-01-04 150
125.000000
20.816660
2022-01-05 170
134.000000
27.018512
2022-01-06 160
138.333333
26.394444
2022-01-07 155
140.714286
24.904580
2022-01-08 180
152.142857
21.185125
2022-01-09 190
162.142857
19.970216
2022-01-10 210
173.571429
21.353041
2022-01-11 220
183.571429
24.616100
2022-01-12 230
192.142857
29.134336
2022-01-13 225
201.428571
27.494588
2022-01-14 240
213.571429
21.739803
2022-01-15 260
225.000000
22.173558
Как видите, в столбце RollingMean_7 все значения оказались заполненны-
ми, и даже первое – для него скользящее среднее оказалось равно самому
исходному значению продаж (100). В столбце RollingStd_7 первое значение
осталось пустым, поскольку для расчета стандартного отклонения нужно как
минимум два значения, что вполне логично.
380 Прогнозирование временных рядов с конструированием признаков
Если вы все же решили оставить первые значения пропущенными, то
в дальнейшем можете применить уже известные вам методы подстанов-
ки пропусков. К примеру, вы могли бы заполнить пропуски, появившиеся
в данных после расчета скользящих показателей, посредством обратной под-
становки с помощью метода bfill(). В этом случае пропущенные значения
в начале датафрейма будут заполнены первыми валидными значениями,
следующими за ними:
# Создаем признаки на основе 7-дневного скользящего среднего и 7-дневного скользящего
стандартного отклонения
df['RollingMean_7'] = df['Sales'].rolling(window=7).mean()
df['RollingStd_7'] = df['Sales'].rolling(window=7).std()
df = df.bfill()
# Выводим датафрейм со скользящими признаками и заполненными пропусками
print(df)
Вывод:
Sales RollingMean_7 RollingStd_7
Date
2022-01-01 100
140.714286
24.904580
2022-01-02 120
140.714286
24.904580
2022-01-03 130
140.714286
24.904580
2022-01-04 150
140.714286
24.904580
2022-01-05 170
140.714286
24.904580
2022-01-06 160
140.714286
24.904580
2022-01-07 155
140.714286
24.904580
2022-01-08 180
152.142857
21.185125
2022-01-09 190
162.142857
19.970216
2022-01-10 210
173.571429
21.353041
2022-01-11 220
183.571429
24.616100
2022-01-12 230
192.142857
29.134336
2022-01-13 225
201.428571
27.494588
2022-01-14 240
213.571429
21.739803
2022-01-15 260
225.000000
22.173558
Итак, с помощью скользящих показателей вы можете легко обнаруживать
прогрессирующие шаблоны во временных рядах. Скользящие вычисления
позволяют сглаживать шум в данных и подчеркивать общие тенденции.
В частности, скользящее среднее усредняет значения в окне, подсвечивая
тренды, а скользящее стандартное отклонение позволяет определить меру
изменчивости показателя.
Решающую роль при определении скользящих показателей играет размер
окна. Чем больше размер окна, тем более долгосрочные тенденции сможет
обнаруживать показатель. Для выявления динамики с разными периода-
ми можно воспользоваться сразу несколькими скользящими показателями
с разными размерами окна.
Стоит отметить, что при использовании скользящих показателей необ-
ходимо обращать внимание на возможную связанную с ними погрешность
Циклические признаки на основе гармонических функций 381
модели. Убедитесь в том, что значения из будущих периодов не оказывают
влияния на исторические данные.
Циклические признаки
на основе гармонических функций
Помимо признаков на основе временного лага и на основе скользящего окна,
при анализе временных рядов бывают чрезвычайно полезны циклические
признаки, созданные на базе гармонических функций. Давайте рассмотрим
пример с созданием всех этих переменных в одном наборе данных:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# Простой набор данных с продажами
data={
'Date': ['2022-01-15', '2022-02-10', '2022-03 -20', '2022-04-15', '2022-05-25',
'2022-06-30', '2022-07-05', '2022-08 -12', '2022-09-18', '2022-10-22'],
'Sales': [1000, 1200, 1500, 1300, 1800, 2000, 1900, 2200, 2100, 2300]
}
df = pd.DataFrame(data)
# Преобразуем поле Date в формат datetime
df['Date'] = pd.to_datetime(df['Date'])
# Извлекаем базовые признаки на основе временных компонентов
df['Year'] = df['Date'].dt.year
df['Month'] = df['Date'].dt.month
df['Day'] = df['Date'].dt.day
df['DayOfWeek'] = df['Date'].dt.dayofweek
df['Quarter'] = df['Date'].dt.quarter
# Извлекаем дополнительные признаки
df['WeekOfYear'] = df['Date'].dt.isocalendar().week
df['DayOfYear'] = df['Date'].dt.dayofyear
df['IsWeekend'] = df['DayOfWeek'].isin([5, 6]).astype(int)
# Создаем циклические признаки для месяца и дня недели
df['Month_sin'] = np.sin(2 * np.pi * df['Month'] / 12)
df['Month_cos'] = np.cos(2 * np.pi * df['Month'] / 12)
df['DayOfWeek_sin'] = np.sin(2 * np.pi * df['DayOfWeek'] / 7)
df['DayOfWeek_cos'] = np.cos(2 * np.pi * df['DayOfWeek'] / 7)
# Создаем признаки на основе временного лага
df['Sales_Lag1'] = df['Sales'].shift(1)
df['Sales_Lag7'] = df['Sales'].shift(7)
# Создаем признак на основе скользящего окна
382 Прогнозирование временных рядов с конструированием признаков
df['Sales_RollingMean7'] = df['Sales'].rolling(window=7, min_periods=1).mean()
# Выводим итоговый датафрейм
print(df)
Как именно работают циклические признаки? Некоторые переменные,
относящиеся к анализу временных рядов, такие как день недели или месяц
года, обладают циклической природой, что означает их повторяемость
с неким предсказуемым шаблоном. К примеру, дни недели идут с по-
недельника по воскресенье, после чего снова идет понедельник. Такая
цикличность имеет важное значение при прогнозировании временных
рядов, поскольку позволяет обнаружить повторяющиеся шаблоны или се-
зонность в данных.
В то же время большинство моделей машинного обучения не приспособле-
ны к пониманию и интерпретированию циклической природы переменных.
При обычном порядковом кодировании, когда понедельнику сопоставляется
значение 1, вторнику – 2 и т. д ., алгоритм может посчитать, что воскресенье
с кодом 7 располагается дальше от понедельника (1) и вторника (2), что, как
вы понимаете, совсем не так.
Для решения подобных проблем необходимо кодировать такие перемен-
ные особым образом с целью сохранения их циклической природы. Наиболее
распространенными являются методы синусного (sine encoding) и косинусного
кодирования (cosine encoding). Эти методы проецируют значения цикличе-
ской переменной на окружность с использованием соответствующей гармо-
нической функции. Алгоритм здесь следующий:
каждое значение циклической переменной представляется в виде угла
на окружности (от 0 до 2π радианов);
рассчитывается синус или косинус этого угла, и на основании этого
значения создается новый признак;
этот новый признак будет отражать циклическую природу исходной
переменной.
В результате такого кодирования переменной с месяцами, например, ян-
варь (1) и декабрь (12) получат близкие значения новых признаков, что го-
ворит об их близости в календаре.
Такой способ кодирования позволяет алгоритмам машинного обучения
лучше понимать и использовать циклическую природу переменных, что по-
ложительно сказывается на качестве предсказаний.
Часовые пояса и пропущенные
значения во временных рядах
При работе с временными рядами особое внимание стоит обращать на специ-
фику использования часовых поясов и пропущенных значений в данных.
Часовые пояса и пропущенные значения во временных рядах 383
Присутствие календарных дат с разными часовыми поясами в наборе
данных может привести к нарушению его консистентности. В связи с этим
нужно тщательно следить за этим аспектом и использовать подходящие
функции и методы для преобразования. В библиотеке Pandas реализован
целый спектр инструментов для работы с часовыми поясами. В частности,
функция tz_localize() позволяет присвоить нужный часовой пояс объек-
ту с типом datetime, а функция tz_convert() осуществляет преобразование
между разными часовыми поясами. Эти функции очень удобно исполь-
зовать при работе с наборами данных, содержащими географические или
временные сведения.
Пример использования функции tz_localize():
s = pd.Series(
[1],
index=pd.DatetimeIndex(['2018-09-15 01:30:00']),
)
print(s, end='\n\n')
print(s.tz_localize('CET'))
Вывод:
2018-09-15 01:30:00 1
dtype: int64
2018-09-15 01:30:00+02:00 1
dtype: int64
Здесь мы создали объект Series с индексом временного ряда, после чего
привели его к центральноевропейскому времени (CET).
Пример использования функции tz_convert():
dti = pd.date_range(start='2014-08-01 09:00',
freq='h', periods=3, tz='Europe/Berlin')
print(dti, end='\n\n')
print(dti.tz_convert('US/Central'))
Вывод:
DatetimeIndex(['2014-08 -01 09:00:00+02:00', '2014-08 -01 10:00:00+02:00', '2014-08 -01
11:00:00+02:00'], dtype='datetime64[ns, Europe/Berlin]', freq='h')
DatetimeIndex(['2014-08 -01 02:00:00-05:00', '2014-08 -01 03:00:00-05:00', '2014-08 -01
04:00:00-05:00'], dtype='datetime64[ns, US/Central]', freq='h')
Здесь мы создали объект типа date_range с тремя временными точками
с часовым поясом Europe/Berlin и дискретизацией по часам, а затем выпол-
нили преобразование этого временного ряда к часовому поясу US/Central,
в результате чего вывод изменился.
Еще одной спецификой работы с временными рядами является внима-
тельное обращение с пропущенными значениями. Дело в том, что пропуски
384 Прогнозирование временных рядов с конструированием признаков
во временных данных могут нарушить хронологию событий и негативно
сказаться на результатах прогнозирования.
Для решения подобных проблем обычно применяется техника восстанов-
ления отсутствующих дат, после чего пропуски в значениях заполняются
способом, зависящим от конкретной задачи.
Ниже приведен пример восстановления временных данных с подстанов-
кой пропусков методом прямого прохода:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# Создаем простой временной ряд
date_range = pd.date_range(start='2023-01-01', end='2023-12-31', freq='D')
sales = np.random.randint(100, 1000, size=len(date_range))
df_sales = pd.DataFrame({'Date': date_range, 'Sales': sales})
# Добавляем пропуски в данные
df_sales = df_sales.drop(df_sales.index[5:10]) # Удаляем десять дней
df_sales = df_sales.drop(df_sales.index[150:160]) # Удаляем еще десять дней
# Выводим исходный датафрейм
print("Исходный датафрейм:")
print(df_sales.head(15))
# Восстанавливаем пропущенные даты
df_sales = df_sales.set_index('Date').asfreq('D')
# Заполняем пропуски в значениях методом прямого прохода
df_sales['Sales'] = df_sales['Sales'].ffill()
# Переустанавливаем индекс, чтобы поле Date снова стало столбцом
df_sales = df_sales.reset_index()
# Выводим восстановленный датафрейм
print("\nВосстановленный датафрейм:")
print(df_sales.head(15))
# Базовая статистика
print("\nБазовая статистика:")
print(df_sales['Sales'].describe())
# Проверка на оставшиеся пропуски
print("\nОставшиеся пропуски в данных:")
print(df_sales.isnull().sum())
Вывод:
Исходный датафрейм:
Date Sales
0 2023-01-01 929
1 2023-01-02 671
2 2023-01-03 431
Часовые пояса и пропущенные значения во временных рядах 385
3 2023-01-04 924
4 2023-01-05 682
10 2023-01-11 617
11 2023-01-12 248
12 2023-01-13 816
13 2023-01-14 519
14 2023-01-15 841
15 2023-01-16 178
16 2023-01-17 114
17 2023-01-18 461
18 2023-01-19 366
19 2023-01-20 439
Восстановленный датафрейм:
Date Sales
0 2023-01-01 929.0
1 2023-01-02 671.0
2 2023-01-03 431.0
3 2023-01-04 924.0
4 2023-01-05 682.0
5 2023-01-06 682.0
6 2023-01-07 682.0
7 2023-01-08 682.0
8 2023-01-09 682.0
9 2023-01-10 682.0
10 2023-01-11 617.0
11 2023-01-12 248.0
12 2023-01-13 816.0
13 2023-01-14 519.0
14 2023-01-15 841.0
Базовая статистика:
count 365.000000
mean
557.172603
std
252.287034
min
106.000000
25%
347.000000
50%
546.000000
75%
791.000000
max
991.000000
Name: Sales, dtype: float64
Оставшиеся пропуски в данных:
Date
0
Sales 0
dtype: int64
Здесь мы создали датафрейм с временным рядом и намеренно внесли
в него пропуски с помощью метода drop(). Для восстановления пропущен-
ных дат мы воспользовались методом set_index(), после чего применили
к датафрейму метод asfreq(), позволяющий привести временной ряд к опре-
деленной частотности.
386 Прогнозирование временных рядов с конструированием признаков
После этой операции в нашем датафрейме появились восстановленные
даты со значениями NaN, которые мы заполнили методом прямой подста-
новки при помощи метода ffill(). В заключение мы вызвали метод reset_in-
dex(), чтобы поле с датой снова стало столбцом. Как видим в выводе, про-
пусков в нашем итоговом датафрейме не осталось.
Детрендирование и работа
с сезонностью во временных рядах
Одна из основных сложностей, связанных с прогнозированием временных
рядов, лежит в области эффективного управления трендами и сезонностью
в данных. Тренды, характеризующиеся постоянным ростом или спадом на
протяжении длительного времени, а также сезонность, заключающаяся в по-
вторяющихся шаблонах с фиксированными интервалами (дни, недели, меся-
цы и т. д.), могут оказывать существенное влияние на точность предсказаний.
Без грамотного управления этими аспектами у нашей модели могут возник-
нуть большие сложности с обнаружением и эффективным использованием
существующих шаблонов.
Тренды могут маскировать краткосрочные колебания и затруднять иден-
тификацию мелких шаблонов, тогда как сезонность может характеризовать
циклические изменения, и если не учитывать ее должным образом, это
может приводить к систематическим ошибкам модели. В этом разделе мы
подробно поговорим о техниках удаления тренда, или детрендирования,
которые позволяют справиться с этими сложностями, а также о способах
работы с сезонностью. С помощью этих мощных методик можно эффектив-
но выделять и анализировать ключевые компоненты временных рядов, что
позволит повысить надежность и качество моделей.
Обсуждаемые здесь техники дают возможность нивелировать влияние
долгосрочных трендов и циклических шаблонов на результаты анализа, по-
зволяя модели сосредоточиться на зависимостях в данных. Подобные мето-
дики не только повышают стационарность временных рядов, избавляя их от
строгой зависимости от времени, что является одним из главных требований
многих предсказательных алгоритмов, но и позволяют строить более надеж-
ные и качественные модели, способные учитывать при анализе краткосроч-
ные колебания и долгосрочные шаблоны с высокой точностью.
Что такое детрендирование?
Детрендирование, или удаление тренда, представляет собой одну из важней-
ших техник анализа временных рядов, позволяющую избавиться от тенден-
ций в данных для обнаружения истинных зависимостей. Применение этой
техники дает возможность преобразовать нестационарный временной ряд
Детрендирование и работа с сезонностью во временных рядах 387
в стационарный, характеризующийся постоянством статистических свойств
с течением времени. Таким образом, стационарными мы называем времен-
ные ряды, в которых средние и стандартные отклонения не меняются со
временем, что делает их идеально подходящими для прогнозирования и мо-
делирования.
Важность техники детрендирования связана с предоставляемой ей воз-
можностью обнаруживать скрытые шаблоны в данных. Долгосрочные тренды
с постепенным ростом или снижением на протяжении достаточно протя-
женного времени способны маскировать краткосрочные колебания и цик-
лические шаблоны, которые зачастую представляют куда больший интерес
в плане анализа и предсказаний. Исключая из общей картины подобные
протяженные тренды, мы получаем возможность сфокусироваться на более
тонких и часто более важных шаблонах в данных.
Существует несколько методов детрендирования временных рядов, каж-
дый из которых характеризуется своими сильными и слабыми сторонами:
детрендирование на основе разностей (differencing): этот метод бази-
руется на вычислении разниц показателя между следующими друг за
другом наблюдениями, что позволяет избавиться от линейного тренда;
детрендирование на основе регрессии (regression detrending): в этом
методе строится линейная регрессия на основе значений показателя
и вычитается из исходных данных. Таким образом мы получаем воз-
можность исключить как линейные, так и нелинейные тренды;
детрендирование на основе скользящего среднего (moving average de-
trending): эта техника использует скользящее среднее для оценки су-
ществующего тренда, которое впоследствии вычитается из исходных
данных.
Выбор метода детрендирования зависит от природы данных и специфи-
ческих требований к анализу. Применяя эти техники, аналитик может вы-
явить скрытые зависимости и шаблоны в данных, скрывающиеся за долго-
срочными трендами, что позволяет повысить качество модели и решений,
принимаемых на ее основе.
Методы детрендирования временных рядов
В этой главе мы рассмотрим три наиболее часто используемых метода де-
трендирования временных рядов, а именно детрендирование на основе раз-
ностей, детрендирование на основе регрессии и детрендирование на основе
скользящего среднего.
Детрендирование на основе разностей
Это наиболее простой способ удаления трендов из данных, который основы-
вается на последовательном вычитании предыдущих значений показателя
из последующих. Эта техника позволяет превратить нестационарный вре-
менной ряд в стационарный.
388 Прогнозирование временных рядов с конструированием признаков
Польза от применения детрендирования на основе разностей кроется
в возможности обнаружения и исключения линейных трендов в данных
и некоторых нелинейных зависимостей. К примеру, если в нашем наборе
данных, посвященном продажам по дням, наблюдается постоянно возрас-
тающий тренд, применение метода детрендирования на основе разностей
будет заключаться в последовательном вычитании продаж изо дня в день,
что позволит нам получить информацию о ежедневных разницах в продажах
без участия абсолютных значений. С большой долей вероятности этот новый
временной ряд окажется более подходящим для прогнозирования.
В зависимости от сложности тренда можно применять разные типы де-
трендирования на основе разностей:
детрендирование на основе разностей первого порядка: это наиболее
распространенный способ вычисления, заключающийся в вычитании
каждого предшествующего значения в переменной из каждого после-
дующего. Этот прием является наиболее эффективным при наличии
линейного тренда;
детрендирование на основе разностей второго порядка: этот метод под-
разумевает двукратное вычитание предшествующих значений из по-
следующих для удаления квадратичных трендов;
сезонное детрендирование на основе разностей: при использовании это-
го метода мы производим вычитание значений показателя не между
соседствующими наблюдениями, а между соответствующими наблю-
дениями, относящимися к одному сезону. Например, январские прода-
жи прошлого года мы можем вычесть из январских продаж нынешнего.
Хотя техника детрендирования на основе разностей является достаточно
мощной и эффективной, не стоит забывать, что злоупотребление ей может
привести к избыточному усложнению модели. Необходимо всегда тщательно
исследовать характеристики временных рядов, чтобы не переусердствовать
с операцией удаления трендов.
Пример применения детрендирования на основе разностей
Рассмотрим пример применения техники детрендирования на основе раз-
ностей к нашему набору данных с дневными продажами:
# Простой набор данных: ежедневные показатели продаж
import pandas as pd
import matplotlib.pyplot as plt
data = {'Date': pd.date_range(start='2022-01-01', periods=10, freq='D'),
'Sales': [100, 120, 130, 150, 170, 190, 200, 220, 240, 260]}
df = pd.DataFrame(data)
# Установим в качестве индекса в датафрейме столбец Date
df.set_index('Date', inplace=True)
# Применяем детрендирование на основе разностей
df['Sales_Differenced'] = df['Sales'].diff()
# Выводим детрендированный временной ряд
print(df)
Детрендирование и работа с сезонностью во временных рядах 389
# Визуализируем данные
plt.figure(figsize=(24, 16))
plt.plot(df.index, df['Sales'], label='Фактические продажи')
plt.plot(df.index, df['Sales_Differenced'], label='Продажи после удаления тренда')
plt.title('Применение детрендирования на основе разностей')
plt.xlabel('Дата')
plt.ylabel('Продажи')
plt.legend()
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
Вывод:
Sales Sales_Differenced
Date
2022-01-01 100
NaN
2022-01-02 120
20.0
2022-01-03 130
10.0
2022-01-04 150
20.0
2022-01-05 170
20.0
2022-01-06 190
20.0
2022-01-07 200
10.0
2022-01-08 220
20.0
2022-01-09 240
20.0
2022-01-10 260
20.0
Рис. П2.1 Применение детрендирования на основе разностей
390 Прогнозирование временных рядов с конструированием признаков
Здесь мы сгенерировали простой набор данных, после чего установили
в качестве индекса в датафрейме столбец Date и воспользовались методом
diff() для создания столбца с разницами в ежедневных продажах. Таким
образом мы применили детрендирование на основе разностей первого по-
рядка, позволяющее избавиться от линейного тренда и сделать временной
ряд более стационарным и подходящим для дальнейшего анализа.
На рис. П2.1 видно, что детрендирование действительно позволяет изба-
виться от постоянно растущего тренда.
Детрендирование на основе регрессии
Еще одним эффективным методом удаления тренда является детрендиро-
вание на основе регрессии. При его использовании мы строим модель ли-
нейной регрессии на основе значений показателя и вычитаем полученные
в результате предсказания значения из исходных данных. Такой подход по-
зволяет бороться со сложными трендами, выходящими за пределы простых
линейных шаблонов. Детрендирование на основе регрессии помогает обна-
руживать специфические тренды, включая полиномиальные и экспоненци-
альные, которые могут лучше отражать внутреннюю динамику данных.
На практике при использовании этого метода мы строим прямую или
кривую линейной регрессии поверх временного ряда, используя временную
ось в качестве предиктора, а значение – в качестве целевой переменной.
Предсказанные на основе этой регрессии значения представляют оценку
тенденции показателя. Вычитая эти предсказанные значения из исходных
данных, мы тем самым избавляемся от присутствующего тренда, а остатки
модели сохраняем для дальнейшего анализа.
Одним из главных преимуществ метода детрендирования на основе ре-
грессии является его гибкость. В зависимости от обнаруженного тренда
в данных аналитики могут выбирать для построения регрессии различные
функции, включая линейные, квадратические и даже сложные полиномиаль-
ные. Это позволяет избавить данные от различных трендов.
Пример применения детрендирования на основе регрессии
Воспользуемся линейной регрессией для обнаружения и исключения тренда
в наших данных о продажах:
from sklearn.linear_model import LinearRegression
import numpy as np
import matplotlib.pyplot as plt
data = {'Date': pd.date_range(start='2022-01-01', periods=10, freq='D'),
'Sales': [100, 120, 130, 150, 170, 190, 200, 220, 240, 260]}
df = pd.DataFrame(data)
# Создаем метку времени для модели (порядковый номер дня в числовом выражении)
df['Time'] = np.arange(len(df))
Детрендирование и работа с сезонностью во временных рядах 391
# Обучаем модель линейной регрессии на метке времени и данных о продажах
X = df[['Time']]
y = df['Sales']
model = LinearRegression()
model.fit(X, y)
# Предсказываем тренд
df['Trend'] = model.predict(X)
# Детренидуем данные, вычитая предсказанные значения из фактических
df['Sales_Detrended'] = df['Sales'] - df['Trend']
# Выводим результат
print(df[['Time', 'Sales', 'Trend', 'Sales_Detrended']])
# Визуализируем данные
plt.figure(figsize=(24, 16))
plt.plot(df.index, df['Sales'], label='Фактические продажи')
plt.plot(df.index, df['Sales_Detrended'], label='Продажи после удаления тренда')
plt.title('Применение детрендирования на основе регрессии')
plt.xlabel('Дата')
plt.ylabel('Продажи')
plt.legend()
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
Вывод:
Time Sales
Trend Sales_Detrended
0
0 100 98.909091
1.090909
1
1 120 116.484848
3.515152
2
2 130 134.060606
- 4 .060606
3
3 150 151.636364
- 1 .636364
4
4 170 169.212121
0.787879
5
5 190 186.787879
3.212121
6
6 200 204.363636
- 4 .363636
7
7 220 221.939394
- 1 .939394
8
8 240 239.515152
0.484848
9
9 260 257.090909
2.909091
В этом примере мы сгенерировали исходные данные, добавив к ним метку
времени для модели в виде порядкового номера дня в числовом выражении.
С таким же успехом мы могли бы воспользоваться созданным по умолчанию
индексом датафрейма. Затем мы создали и обучили на созданной метке вре-
мени модель линейной регрессии и предсказали линию тренда. После этого
создали новый столбец Sales_Detrended путем вычитания предсказанных зна-
чений тренда из фактических данных. В результате для дальнейшего анализа
мы получили остатки модели. Фактически в новом столбце Sales_Detrended
мы наблюдаем разницу между ожидаемыми и действительными продажами.
392 Прогнозирование временных рядов с конструированием признаков
Рис. П2.2 Применение детрендирования на основе регрессии
Детрендирование на основе скользящего среднего
Еще одним часто используемым методом избавления от тренда является
детрендирование на основе скользящего среднего. Как вы и могли ожидать,
здесь мы из фактических значений переменной вычитаем скользящее сред-
нее, вычисленное для окна заданного размера. Эта техника позволяет обна-
руживать тренды в данных, при этом маскируя кратковременные колебания
и шум.
Детрендирование на основе скользящего среднего может оказаться полез-
ным, если ваши данные характеризуются значительной изменчивостью или
содержат неравномерные шаблоны. Изменяя размер окна, аналитик может
регулировать степень сглаживания, применяемого к данным. Чем больше
размер окна, тем более долгосрочные тренды вы сможете выявить.
Одним из преимуществ этого способа детрендирования является его
простота и интерпретируемость. В отличие от моделей линейной регрессии,
которые нужно обучать, скользящие средние вычисляются легко и быстро,
а объяснить выводы на их основе людям без специальной подготовки будет
гораздо проще. Кроме того, этот метод применим к самым разным типам
временных рядов, что делает его универсальным инструментом в арсенале
аналитика.
В то же время необходимо помнить, что применение детрендирования на
основе скользящего среднего может приводить к образованию смещений
Детрендирование и работа с сезонностью во временных рядах 393
в итоговом наборе данных. Особенно заметны эти смещения могут быть
в начале и конце временных рядов, где усреднение производится с использо-
ванием меньшего количества наблюдений. Об этой особенности применения
данного метода стоит помнить и принимать соответствующие меры.
Пример применения детрендирования на основе скользящего
среднего
Поработаем с теми же исходными данными, но на этот раз воспользуемся
для обнаружения тренда скользящим средним:
import numpy as np
import matplotlib.pyplot as plt
data = {'Date': pd.date_range(start='2022-01-01', periods=10, freq='D'),
'Sales': [100, 120, 130, 150, 170, 190, 200, 220, 240, 260]}
df = pd.DataFrame(data)
# Рассчитываем скользящее среднее для обнаружения тренда
df['MovingAverage_Trend'] = df['Sales'].rolling(window=3).mean()
# Детрендируем данные, вычитая скользящие значения из фактических
df['Sales_Detrended'] = df['Sales'] - df['MovingAverage_Trend']
# Выводим результат
print(df[['Sales', 'MovingAverage_Trend', 'Sales_Detrended']])
# Визуализируем данные
plt.figure(figsize=(24, 16))
plt.plot(df['Date'], df['Sales'], label='Фактические продажи')
plt.plot(df['Date'], df['Sales_Detrended'], label='Продажи после удаления тренда')
plt.title('Применение детрендирования на основе скользящего среднего')
plt.xlabel('Дата')
plt.ylabel('Продажи')
plt.legend()
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
Вывод:
Sales MovingAverage_Trend Sales_Detrended
0 100
NaN
NaN
1 120
NaN
NaN
2 130
116.666667
13.333333
3 150
133.333333
16.666667
4 170
150.000000
20.000000
5 190
170.000000
20.000000
6 200
186.666667
13.333333
7 220
203.333333
16.666667
8 240
220.000000
20.000000
9 260
240.000000
20.000000
394 Прогнозирование временных рядов с конструированием признаков
Рис. П2.3 Применение детрендирования на основе скользящего среднего
Здесь мы воспользовались методом rolling() для вычисления 3-дневного
среднего, после чего вычли полученные значения из фактических продаж,
тем самым избавившись от тренда.
Работа с сезонностью во временных рядах
Под сезонностью (seasonality) подразумеваются циклические шаблоны или
колебания, возникающие во временных рядах с регулярной периодично-
стью. Сезонность совсем не обязательно должна быть связана с временами
года – ее интервалы могут быть самыми разными: от недели до года или не-
скольких лет. К примеру, розничные продажи могут сильно увеличиваться
на выходных, а показатель потребления электричества изменяется в течение
года в соответствии со средними температурами.
Важность тщательного учета сезонности при работе с временными ря-
дами трудно переоценить. Недостаток внимания к этим аспектам может
негативно сказаться на качестве и надежности моделей. Без должного об-
ращения сезонные колебания могут маскировать существующие в данных
тренды, искажать краткосрочные колебания и приводить к систематиче-
ским ошибкам предсказаний. В связи с этим аналитики применяют разно-
образные техники для определения и количественного выражения сезон-
ности в своих данных.
Детрендирование и работа с сезонностью во временных рядах 395
Выделение сезонности
Выделение сезонности (seasonal differencing) представляет собой мощную тех-
нику для работы с сезонными признаками во временных рядах. В отличие от
техник детрендирования, рассмотренных выше, при которых значения вы-
читаются из соседних наблюдений, выделение сезонности оперирует значе-
ниями, отстоящими друг от друга на целые временные периоды. К примеру,
если в данных с детализацией до дня наблюдается недельная сезонность, мы
будем оперировать значениями из соответствующих дней недели в разных
неделях. Этот метод позволяет эффективно избавиться от повторяющихся
шаблонов, связанных с определенными интервалами, тем самым обнажая
тренды и колебания, не связанные с сезонностью напрямую.
Выделение сезонности можно применять в самых разных сценариях, та-
ких как:
розничные продажи, в которых зачастую пиковые показатели прихо-
дятся на выходные дни;
данные с детализацией по месяцам, в которых могут быть сезонные
перепады, – например, это может касаться пиковых продаж мороже-
ного в летние месяцы;
квартальные отчеты, в которых шаблоны могут быть связаны с финан-
совыми годами.
Метод выделения сезонности можно применять совместно с другими тех-
никами, такими как детрендирование или конструирование признаков, с це-
лью создания более надежных и качественных моделей.
К примеру, для выделения недельной сезонности мы можем добавить при-
знак, в котором значения будут соответствовать разнице между текущим
показателем и показателем недельной давности, следующим образом:
df['Sales_SeasonalDifferenced'] = df['Sales'].diff(7)
Вывод:
Date Sales Sales_SeasonalDifferenced
0 2022-01-01 100
NaN
1 2022-01-02 120
NaN
2 2022-01-03 130
NaN
3 2022-01-04 150
NaN
4 2022-01-05 170
NaN
5 2022-01-06 190
NaN
6 2022-01-07 200
NaN
7 2022-01-08 220
120.0
8 2022-01-09 240
120.0
9 2022-01-10 260
130.0
В результате мы можем исключить из данных недельную сезонность, что
позволит сделать временной ряд более стационарным и облегчит дальней-
шее моделирование.
396 Прогнозирование временных рядов с конструированием признаков
Создание сезонных признаков
Еще один эффективный подход к анализу сезонности в данных состоит в соз-
дании так называемых сезонных признаков (seasonal feature). Этот подход
включает извлечение компонент, связанных с датой и временем, из соответ-
ствующих столбцов, которые помогают модели распознавать и учитывать при
обучении сезонные шаблоны. К примеру, мы можем извлечь из колонки с да-
тами дополнительные признаки с номером месяца, недели или дня недели.
Затем эти дополнительные признаки направляются в модель в качестве пре-
дикторов, что позволяет ей учитывать повторяющиеся сезонные колебания.
Но процесс создания сезонных признаков не ограничивается одним лишь
извлечением информации о них из существующих переменных. Зачастую
он включает в себя процедуру кодирования новых признаков с учетом их
сезонной природы. К примеру, вместо использования порядковых номеров
месяцев от 1 до 12 вы могли бы создать циклические признаки на основе
гармонических функций, таких как синус и косинус, способные передавать
повторяющийся шаблон в течение года. Такой подход, называемый цикли-
ческим кодированием (cyclical encoding), позволит модели воспринимать де-
кабрь (12) и январь (1) как соседствующие месяцы в годичном цикле.
Также в зависимости от природы ваших данных и сезонных шаблонов,
которые вам необходимо извлечь, вы можете создавать более сложные се-
зонные признаки, основываясь на знаниях о предметной области. Они могут
включать в себя:
праздники или особые события, влияющие на ваш временной ряд;
времена года (зима, весна, лето, осень);
информацию о финансовых кварталах и финансовых годах;
семестры для данных об образовательных учреждениях.
Включая сезонные признаки в модель, вы наполняете ее важным кон-
текстом относительно временной структуры ваших данных. Это позволяет
модели учесть при обучении повторяющиеся шаблоны, что потенциально
может повысить качество ее предсказаний. Но важно помнить о том, что
необходимо включать в модель только те сезонные признаки, которые дей-
ствительно отражают характер ваших данных.
Пример создания сезонных признаков:
df.set_index('Date', inplace=True)
# Извлекаем компоненты из данных (месяц и день недели)
df['Month'] = df.index.month
df['DayOfWeek'] = df.index.dayofweek
# Выводим сезонные признаки
print(df[['Sales', 'Month', 'DayOfWeek']])
Вывод:
Sales Month DayOfWeek
Date
2022-01-01 100
1
5
Применение методов из семейства ARIMA и алгоритмов машинного обучения 397
2022-01-02 120
1
6
2022-01-03 130
1
0
2022-01-04 150
1
1
2022-01-05 170
1
2
2022-01-06 190
1
3
2022-01-07 200
1
4
2022-01-08 220
1
5
2022-01-09 240
1
6
2022-01-10 260
1
0
Как детрендирование и выделение сезонности
влияют на качество моделей
Удаляя из исходных данных тренды и особым образом обрабатывая сезон-
ные колебания, мы существенно повышаем степень их стационарности, что
делает их гораздо более подходящими для использования в моделях. Этот
процесс предварительной подготовки данных имеет огромное значение
в связи с тем, что многие алгоритмы машинного обучения и статистические
модели, такие как метод случайного леса или ARIMA, обычно показывают
существенно лучшее качество на стационарных данных, лишенных долго-
срочных трендов и циклических сезонных эффектов.
Свойство стационарности временного ряда предполагает постоянство ста-
тистических свойств, таких как среднее значение и дисперсия, в отношении
времени, являющееся одним из основных требований многих техник про-
гнозирования.
Применение методов из семейства
ARIMA и алгоритмов машинного
обучения для прогнозирования
временных рядов
Завершив объемный процесс конструирования признаков, включающий соз-
дание признаков на основе временного лага и скользящих окон, применение
операции детрендирования и использование техник для работы с сезонными
показателями, мы выходим на финишную прямую, символизирующую при-
менение того или иного метода машинного обучения с целью предсказания
будущих значений в нашем временном ряду. В этом разделе мы покажем
примеры использования с временными рядами таких алгоритмов машин-
ного обучения, как авторегрессионная модель (AR), модель скользящего
среднего (MA), авторегрессионная модель скользящего среднего (ARMA),
398 Прогнозирование временных рядов с конструированием признаков
интегрированная авторегрессионная модель скользящего среднего (ARIMA),
интегрированная авторегрессионная модель скользящего среднего с учетом
сезонности (SARIMA), метод случайного леса, градиентный бустинг и его экс-
тремальный вариант XGBoost. Последние три модели отлично зарекомендо-
вали себя при работе со структурированными данными, и ничто не мешает
им проявить себя на поприще прогнозирования временных рядов. В отличие
от традиционных методов работы с временными рядами, таких как ARIMA,
эти алгоритмы обладают способностью использовать при обучении модели
все созданные признаки. Это делает их невероятно гибкими и надежными
и позволяет обнаруживать краткосрочные колебания и долгосрочные трен-
ды с невероятной точностью. В следующих разделах мы подготовим данные
и воспользуемся всеми перечисленными методами машинного обучения для
прогнозирования, после чего сравним полученные результаты.
Шаг 1. Подготовка данных для алгоритма
машинного обучения
Перед применением методов машинного обучения нам необходимо долж-
ным образом подготовить данные и разбить их на обучающую и тестовую
выборки.
Мы в нашем анализе воспользуемся набором данных, загруженным с сай-
та Kaggle по адресу https://www.kaggle.com/datasets/matthieugimbert/french-
bakery-daily-sales. Также этот набор данных в виде файла CSV содержится
в сопроводительных материалах к книге. Этот набор посвящен ежедневным
продажам выпечки в одной из французских булочных. Мы проведем пред-
варительную обработку данных, добавим нужные нам столбцы, восстановим
пропущенные значения и разобьем данные на обучающую и тестовую вы-
борки в соотношении 85 на 15.
Начнем с просмотра набора данных:
import pandas as pd
import numpy as np
import seaborn as sns
# Загружаем данные о продажах выпечки
df_bak = pd.read_csv('Bakery sales.csv')
print(df_bak)
Вывод:
Unnamed: 0
date time ticket_number
article \
0
0 2021-01-02 08:38
150040.0
BAGUETTE
1
1 2021-01-02 08:38
150040.0
PAIN AU CHOCOLAT
2
4 2021-01-02 09:14
150041.0
PAIN AU CHOCOLAT
3
5 2021-01-02 09:14
150041.0
PAIN
4
8 2021-01-02 09:25
150042.0 TRADITIONAL BAGUETTE
...
...
...
...
...
...
Применение методов из семейства ARIMA и алгоритмов машинного обучения 399
234000
511387 2022-09-30 18:52
288911.0
COUPE
234001
511388 2022-09-30 18:52
288911.0
BOULE 200G
234002
511389 2022-09-30 18:52
288911.0
COUPE
234003
511392 2022-09-30 18:55
288912.0 TRADITIONAL BAGUETTE
234004
511395 2022-09-30 18:56
288913.0 TRADITIONAL BAGUETTE
Quantity unit_price
0
1.0
0,90 €
1
3.0
1,20 €
2
2.0
1,20 €
3
1.0
1,15 €
4
5.0
1,20 €
...
...
...
234000
1.0
0,15 €
234001
1.0
1,20 €
234002
2.0
0,15 €
234003
1.0
1,30 €
234004
1.0
1,30 €
[234005 rows x 7 columns]
Как видите, в исходных данных у нас есть более 234 тыс. строк с транзакция-
ми по продажам. Мы хотим сгруппировать данные до дня и оставить записи
только до 10 июля 2022 года.
Для начала приведем цену к числовому виду, поскольку сейчас она строко-
вая и со знаком евро. Также перемножим цену на количество в транзакции,
чтобы получить сумму транзакции. Это можно сделать очень просто:
# Приводим столбец с ценой к числовому виду и считаем сумму
df_bak['unit_price'] = df_bak['unit_price'].str.replace(' €', '').str.replace(',', '.').
astype(float)
df_bak['sales'] = df_bak['Quantity'] * df_bak['unit_price']
При использовании моделей машинного обучения мы будем использовать
дополнительные предикторы, чтобы продемонстрировать потенциал этих
алгоритмов по сравнению с традиционными методами работы с временны-
ми рядами вроде ARMA или ARIMA. Так что давайте в процессе группировки
данных по дням заранее добавим признак, говорящий о доле продажи кру-
ассанов в день. Возможно, в будущем учет этого параметра поможет нашей
модели показать лучшее качество:
# Рассчитываем долю круассанов в продажах дня и группируем данные по дням
df_bak['IS_CROISSANT'] = df_bak['article'] == 'CROISSANT'
df_bak = df_bak.groupby(['date'], as_index=False).agg({'sales': 'sum', 'IS_CROISSANT':
['sum', 'count']})
df_bak.columns = list(map(''.join, df_bak.columns.values))
df_bak['croissant_perc'] = df_bak['IS_CROISSANTsum'] / df_bak['IS_CROISSANTcount']
Теперь переведем столбец date в формат даты и сделаем его индексом:
# Приводим столбец date к формату даты и делаем его индексом
df_bak['date'] = pd.to_datetime(df_bak['date'])
df_bak = df_bak.set_index('date')
400 Прогнозирование временных рядов с конструированием признаков
На этот момент наши данные выглядят следующим образом:
salessum IS_CROISSANTsum IS_CROISSANTcount croissant_perc
date
2021-01-02 987.85
24
365
0.065753
2021-01-03 1014.30
23
375
0.061333
2021-01-04 461.90
5
210
0.023810
2021-01-05 515.70
8
235
0.034043
2021-01-07 544.00
5
215
0.023256
...
...
...
...
...
2022-09-26 693.75
16
295
0.054237
2022-09-27 746.45
12
325
0.036923
2022-09-28 610.70
12
278
0.043165
2022-09-29 689.20
16
303
0.052805
2022-09-30 795.95
22
341
0.064516
[600 rows x 4 columns]
Если вы заметили, наша булочная работает не каждый день, у нее есть
выходные. К примеру, 6 января 2021 года сотрудники булочной на работу не
выходили. Нам для анализа временных рядов желательно обеспечить пол-
ное заполнение столбца с датами из календаря, чтобы не было пропусков.
При этом мы не собираемся анализировать фактические продажи булочной,
в связи с чем можем без проблем заполнить пропущенные дни цифрами,
взятыми из последующих дней. Таким образом, мы предположим, что 6 ян-
варя, если бы булочная работала, мы продали бы столько же выпечки, сколько
и днем позже.
Для восстановления календарного порядка дат воспользуемся функцией
pd.date_range() и методом reindex() с обратным заполнением, как показано
ниже. А заодно удалим вспомогательные поля и оставим в наборе только
данные до 10 июля 2022 года:
# Восстанавливаем все пропущенные даты и заполняем пропуски с помощью метода bfill
idx = pd.date_range(df_bak.index[0], df_bak.index[-1])
df_bak = df_bak.reindex(idx, method='bfill')
# Фильтруем до 2022-07-10
df_bak = df_bak[df_bak.index < '2022-07-10']
df_bak = df_bak[['salessum', 'croissant_perc']]
print(df_bak)
Теперь наши данные приняли очень строгий вид: у нас есть индекс с да-
тами без пропусков, сумма продаж за день и доля круассанов (кстати, не
такая большая, как можно было предположить с учетом того, что мы имеем
дело с французской булочной, – во всем виноваты их не менее знаменитые
багеты):
salessum croissant_perc
2021-01-02 987.85
0.065753
2021-01-03 1014.30
0.061333
Применение методов из семейства ARIMA и алгоритмов машинного обучения 401
2021-01-04 461.90
0.023810
2021-01-05 515.70
0.034043
2021-01-06 544.00
0.023256
...
...
...
2022-07-05 862.20
0.035714
2022-07-06 753.25
0.035040
2022-07-07 960.95
0.036145
2022-07-08 952.20
0.026829
2022-07-09 1355.65
0.053131
[554 rows x 2 columns]
В нашем распоряжении 554 наблюдения и два столбца. Давайте, наконец,
посмотрим, как выглядят отобранные нами продажи по дням:
import matplotlib.pyplot as plt
plt.figure(figsize=(12, 6))
# Строим линейную диаграмму с суммами и датами
plt.plot(df_bak.index, df_bak['salessum'], label='Сумма')
# Легенда и заголовки
plt.title('График продаж выпечки')
plt.xlabel('Дата')
plt.ylabel('Сумма')
plt.legend()
plt.show()
Вывод показан на рис. П2.4 .
Рис. П2.4 График продаж выпечки
402 Прогнозирование временных рядов с конструированием признаков
Мы уже упоминали ранее главное свойство стационарности временно-
го ряда, предполагающее постоянство статистических свойств, таких как
среднее значение и дисперсия, и являющееся одним из основных требова-
ний многих техник прогнозирования. А как узнать, является ли временной
ряд стационарным? Для этого можно использовать разные техники, такие
как тест Дики−Фуллера или тест Квятковского−Филлипса−Шмидта−Шина.
Мы воспользуемся первым из них. Нулевая гипотеза при проведении теста
Дики−Фуллера заключается в том, что временной ряд содержит единичный
корень, что говорит о его нестационарности. Для проверки нашего времен-
ного ряда выполним следующий код:
from statsmodels.tsa.stattools import adfuller
ts = df_bak['salessum']
# Тест Дики–Фуллера
result_adf = adfuller(ts)
print('Тест Дики–Фуллера:')
print(f'Показатель статистики: {result_adf[0]}')
print(f'P-значение: {result_adf[1]}')
Вывод:
Тест Дики–Фуллера:
Показатель статистики: -3 .0584510797320985
P-значение: 0.029784735723492224
Здесь мы сохранили в переменной ts (time series) нашу целевую пере-
менную с продажами, после чего вызвали функцию adfuller() и вывели по-
казатель статистики и p-значение.
Поскольку нулевая гипотеза выступает в пользу нестационарности вре-
менного ряда, то p-значения, меньшие 0.05, говорят о возможности ее от-
клонить и признать временной ряд стационарным. Мы уже говорили ранее
о способах превращения нестационарных временных рядов в стационарные,
но в данном случае эти методики нам не понадобятся.
Далее нам необходимо разделить наш набор данных на обучающую и тес-
товую выборки в рамках подготовки для дальнейшего моделирования. Сде-
лаем это:
# Разбиваем набор данных на обучающую и тестовую выборки
train_size = int(len(ts) * 0.85)
ts_train, ts_test = ts[:train_size], ts[train_size:]
print(f'Размер обучающей выборки: {len(ts_train)}')
print(f'Размер тестовой выборки: {len(ts_test)}')
Вывод:
Размер обучающей выборки: 470
Размер тестовой выборки: 84
Применение методов из семейства ARIMA и алгоритмов машинного обучения 403
Шаг 2. Применение методов прогнозирования
временных рядов
Итак, мы готовы к тому, чтобы предпринять первую робкую попытку спрог-
нозировать временной ряд. Для начала воспользуемся простейшими мето-
дами, после чего будем постепенно усложнять процесс моделирования.
Авторегрессионная модель
Начнем с простой авторегрессионной модели (autoregressive model, AR), пола-
гающейся исключительно на значения прошлых периодов. Эта модель может
быть успешно применена тогда, когда между предыдущими и последующи-
ми значениями во временном ряде есть довольно четкая зависимость. По
своей сути авторегрессионная модель представляет собой обычную линей-
ную регрессию, в которой в качестве предикторов используются значения
того же временного ряда прошлых периодов.
Для построения модели воспользуемся классом AutoReg из модуля stats-
models.tsa.ar _model. В качестве значения параметра lags мы передадим 7,
что будет означать намерение включить в модель все временные смещения
от 1 до 7 дней. Если вам необходимо включить только конкретные смеще-
ния, вы можете передать их в списке. Например, при передаче списка [1,
4, 7] в модель будут включены только смещения на один, четыре и семь
дней назад:
from statsmodels.tsa.ar _model import AutoReg
# Создаем и обучаем авторегрессионную модель с временным лагом 7
model_ar = AutoReg(ts_train, lags=7).fit()
# Прогнозируем продажи на всем наборе данных, включая тестовую выборку
forecast_ar = model_ar .predict(start=0, end=len(ts)-1, dynamic=False)
# Создаем индекс для вывода на графике
forecast_index_ar = pd.date_range(start=ts.index[0], periods=len(ts))
# Строим график
plt.figure(figsize=(14, 7))
plt.plot(ts, label='Исходные данные')
plt.plot(forecast_index_ar[:len(ts_train)], forecast_ar[:len(ts_train)],
label='Смоделированный ряд', color='red')
plt.plot(forecast_index_ar[len(ts_train):], forecast_ar[len(ts_train):], label='Прогноз',
color='green')
plt.title('Прогнозирование временного ряда с помощью авторегрессии (класс AutoReg)')
plt.xlabel('Дата')
plt.ylabel('Продажи')
plt.legend()
plt.show()
print(model_ar .summary())
404 Прогнозирование временных рядов с конструированием признаков
Вывод:
AutoReg Model Results
==============================================================================
Dep. Variable:
salessum No. Observations:
470
Model:
AutoReg(7) Log Likelihood
- 3158.036
Method:
Conditional MLE S.D . of innovations
221.822
Date:
Wed, 23 Apr 2025 AIC
6334.072
Time:
15:31:47 BIC
6371.312
Sample:
01-09-2021 HQIC
6348.732
-
04-16-2022
===============================================================================
coef
std err
z
P>|z|
[0.025
0.975]
-------------------------------------------------------------------------------
const
50.0144
28.915
1.730
0.084
-6.658
106.687
salessum.L1
0.4048
0.039
10.249
0.000
0.327
0.482
salessum.L2 -0 .0172
0.044
-0.392
0.695
-0.103
0.069
salessum.L3
0.0246
0.044
0.561
0.575
-0.061
0.110
salessum.L4
- 0 .0305
0.044
-0.695
0.487
-0.116
0.055
salessum.L5
0.0117
0.044
0.266
0.790
-0.074
0.097
salessum.L6
0.0088
0.044
0.200
0.841
-0.077
0.094
salessum.L7
0.5403
0.039
13.718
0.000
0.463
0.617
Roots
=============================================================================
Real
Imaginary
Modulus
Frequency
-----------------------------------------------------------------------------
AR.1
1.0132
-0 .0000j
1.0132
- 0 .0000
AR.2
0.7012
-0 .7828j
1.0510
- 0 .1337
AR.3
0.7012
+0.7828j
1.0510
0.1337
AR.4
- 0 .1952
-1 .0993j
1.1165
- 0 .2780
AR.5
- 0 .1952
+1.0993j
1.1165
0.2780
AR.6
- 1 .0207
-0 .5337j
1.1518
- 0 .4233
AR.7
- 1 .0207
+0.5337j
1.1518
0.4233
-----------------------------------------------------------------------------
Как читать этот весьма мудреный вывод сводной информации о модели?
На что обращать внимание? Давайте посмотрим.
Что мы видим в верхней секции таблицы? Название целевой переменной,
выбранную модель с горизонтом смещения, дату и время запуска, диапазон
дат, количество наблюдений и т. д . Но нас больше всего интересует метрика
AIC, представляющая собой информационный критерий Акаике. Этот кри-
терий традиционно и очень давно используется для выбора оптимальной
статистической модели на одних и тех же данных с применением логариф-
мической функции правдоподобия. Критерий Акаике позволяет найти ком-
промисс между сложностью модели (т. е . числом параметров) и ее точностью.
Чем он меньше, тем более качественной можно считать модель. Вместе с уже
знакомой вам метрикой MSE мы в этой главе будем активно пользовать-
ся этим критерием, и для первой нашей модели его значение составило
6334.072. Запомним порядок чисел.
Применение методов из семейства ARIMA и алгоритмов машинного обучения 405
Рис. П2.5 Прогнозирование временного ряда
с помощью авторегрессии (класс AutoReg)
Во второй секции таблицы представлена информация о значимости вы-
бранных предикторов. Здесь мы, помимо константы, видим семь созданных
признаков на основе временного лага: от salessum.L1 до salessum.L7. Влияние
переменной на предсказания модели принято определять по столбцу, обо-
значенному как P>|z|. Это p-значение, и чем оно ближе к нулю, тем более зна-
чимым является предиктор. Мы видим, что наиболее значимыми в нашем
случае являются переменные salessum.L1 и salessum.L7, что говорит о тесной
связи текущего значения со значением предыдущего дня и такого же дня
неделю назад. Судить о значимости переменных можно и по столбцу coef,
в котором указаны коэффициенты соответствующих предикторов в линей-
ной модели. Как видите, самыми высокими коэффициентами отмечены те
же две переменные.
График прогноза представлен на рис. П2.5 . Здесь и далее на подобных гра-
фиках синим цветом показаны фактические показатели продаж, красным –
смоделированные показатели на обучающей выборке, а зеленым – прогноз
на тестовой выборке. Именно по этой зеленой части графика мы и будем
считать метрику MSE. Для этого воспользуемся уже знакомой вам функцией
mean_squared_error() из модуля sklearn.metrics:
from sklearn.metrics import mean_squared_error
y_test = ts_test
y_pred = forecast_ar[len(ts_train):]
mse_ar = mean_squared_error(y_test, y_pred)
print(f'MSE авторегрессии (класс AutoReg): {mse_ar}')
406 Прогнозирование временных рядов с конструированием признаков
Вывод:
MSE авторегрессии (класс AutoReg): 155219.88540028094
Запомним это число.
Теперь посмотрим, как можно построить авторегрессионную модель про-
гноза иным способом. Для этого мы воспользуемся универсальным клас-
сом ARIMA из модуля statsmodels.api.tsa. Данный класс мы будем применять
в этой главе не раз для построения разных моделей прогнозирования.
Давайте посмотрим на код:
# Параметры для модели AR
p = 7 # Значение для порядка авторегрессии
d = 0 # Значение для количества вычислений разностей
q = 0 # Значение для порядка скользящего среднего
model_ar _arima = sm.tsa.ARIMA(ts_train, order=(p, d, q)).fit()
# Прогнозируем весь временной ряд
forecast_ar _arma = model_ar _arima.predict(start=0, end=len(ts)-1)
# Создаем индекс для вывода на графике
forecast_ar _index_arima = pd.date_range(start=ts.index[0], periods=len(ts))
# Строим график
plt.figure(figsize=(12, 6))
plt.plot(ts.index, ts, label='Исходные данные')
plt.plot(forecast_ar _index_arima[:len(ts_train)], forecast_ar _arma[:len(ts_train)],
label='Смоделированный ряд', color='red')
plt.plot(forecast_ar _index_arima[len(ts_train):], forecast_ar _arma[len(ts_train):],
label='Прогноз', color='green')
plt.title('Прогнозирование временного ряда с помощью авторегрессии (модель ARIMA)')
plt.xlabel('Дата')
plt.ylabel('Продажи')
plt.legend()
plt.show()
print(model_ar _arima.summary())
Вывод:
SARIMAX Results
==============================================================================
Dep. Variable:
salessum No. Observations:
470
Model:
ARIMA(7, 0, 0) Log Likelihood
- 3206.393
Date:
Wed, 23 Apr 2025 AIC
6430.786
Time:
16:40:00 BIC
6468.160
Sample:
01-02-2021 HQIC
6445.490
-
04-16-2022
Covariance Type:
opg
==============================================================================
coef
std err
z
P>|z|
[0.025
0.975]
------------------------------------------------------------------------------
Применение методов из семейства ARIMA и алгоритмов машинного обучения 407
const
824.6796 187.082
4.408
0.000
458.006 1191.353
ar.L1
0.4051
0.036
11.263
0.000
0.335
0.476
ar.L2
- 0 .0193
0.055
-0.353
0.724
-0.127
0.088
ar.L3
0.0253
0.045
0.566
0.571
-0.062
0.113
ar.L4
- 0 .0295
0.041
-0.723
0.469
-0.109
0.050
ar.L5
0.0090
0.044
0.205
0.838
-0.077
0.095
ar.L6
0.0088
0.043
0.202
0.840
-0.076
0.094
ar.L7
0.5372
0.032
16.726
0.000
0.474
0.600
sigma2
4.921e+04 2040.605
24.116
0.000 4.52e+04 5.32e+04
===================================================================================
Ljung-Box (L1) (Q):
25.18 Jarque-Bera (JB):
370.30
Prob(Q):
0.00 Prob(JB):
0.00
Heteroskedasticity (H):
0.65 Skew:
0.68
Prob(H) (two-sided):
0.01 Kurtosis:
7.13
===================================================================================
Рис. П2.6 Прогнозирование временного ряда
с помощью авторегрессии (модель ARIMA)
В первых строках кода мы задаем параметры для авторегрессионной мо-
дели. Переменная p отвечает за максимальное смещение назад во времени
и соответствует аргументу lags, который мы передавали классу AutoReg при
его инициализации. С помощью переменной d можно указать, какое коли-
чество раз необходимо вычислить разности между значениями, чтобы при-
вести ряд к стационарному состоянию. Переменная q отвечает за размер
окна, используемый для расчета скользящих средних при сглаживании вре-
менного ряда. При инициализации класса ARIMA эти переменные передаются
в виде кортежа в аргумент order=(p, d, q). Как видите, в данном случае мы
заполнили только значение переменной p, поскольку работаем с авторе-
грессионной моделью, а две другие переменные оставили равными нулю.
408 Прогнозирование временных рядов с конструированием признаков
Позже мы чуть подробнее поговорим о способах определения оптимальных
значений для этих переменных и даже выполним автоматический подбор
этих параметров.
Сам процесс обучения и прогнозирования ничем не отличается от первого
примера – мы используем те же методы fit() и predict().
Как видите, значение метрики AIC оказалось сопоставимо с первым при-
мером, как и коэффициенты переменных.
Снова рассчитаем метрику MSE, по которой можно судить о точности про-
гноза:
from sklearn.metrics import mean_squared_error
y_test = ts_test
y_pred = forecast_ar _arma[len(ts_train):]
mse_ar2 = mean_squared_error(y_test, y_pred)
print(f'MSE авторегрессии (модель ARIMA): {mse_ar2}')
Вывод:
MSE авторегрессии (модель ARIMA): 156886.3824193297
Если мы взглянем на процедуру заполнения прогнозных значений двух
рассмотренных методов, то заметим, что модель AutoReg оставляет незапол-
ненными первые значения временного ряда, соответствующие смещению,
тогда как модель ARIMA их заполняет:
print(pd.DataFrame({'AutoReg': forecast_ar, 'ARIMA': forecast_ar _arma}).head(20))
Вывод:
AutoReg
ARIMA
2021-01-02
NaN 824.679609
2021-01-03
NaN 936.835835
2021-01-04
NaN 955.804107
2021-01-05
NaN 589.502484
2021-01-06
NaN 649.790692
2021-01-07
NaN 674.159471
2021-01-08
NaN 724.229619
2021-01-09 802.315704 800.232984
2021-01-10 865.488304 863.260156
2021-01-11 656.100220 655.319342
2021-01-12 502.486508 501.008869
2021-01-13 501.434019 501.135442
2021-01-14 542.660226 542.048005
2021-01-15 550.616567 548.656469
2021-01-16 624.964437 623.848487
2021-01-17 778.839263 777.342653
2021-01-18 620.812400 620.212746
2021-01-19 431.807145 430.912859
2021-01-20 502.034759 501.764291
2021-01-21 497.901249 497.343642
Применение методов из семейства ARIMA и алгоритмов машинного обучения 409
Модель скользящего среднего
Модель скользящего среднего (moving average model), как ясно из названия,
полагается на скользящие показатели с разными размерами окна, о которых
мы уже говорили ранее. Эта модель позволяет обнаружить в данных долго-
срочные тренды, а краткосрочные колебания нивелируются.
Существуют разные методы расчета скользящих показателей, включая
вычисление экспоненциального скользящего среднего, мы же рассмотрим
только пример с простым скользящим средним.
Для создания модели скользящего среднего мы воспользуемся тем же уни-
версальным классом ARIMA, но на этот раз, как вы уже, наверное, догадались,
заполним переменную q, а переменные d и p оставим нулевыми:
# Параметры для модели MA
p = 0 # Значение для порядка авторегрессии
d = 0 # Значение для количества вычислений разностей
q = 7 # Значение для порядка скользящего среднего
model_ma _arima = sm.tsa.ARIMA(ts_train, order=(p, d, q)).fit()
# Прогнозируем весь временной ряд
forecast_ma _arma = model_ma _arima.predict(start=0, end=len(ts)-1)
# Создаем индекс для вывода на графике
forecast_ma _index_arima = pd.date_range(start=ts.index[0], periods=len(ts))
# Строим график
plt.figure(figsize=(12, 6))
plt.plot(ts.index, ts, label='Исходные данные')
plt.plot(forecast_ma _index_arima[:len(ts_train)], forecast_ma _arma[:len(ts_train)],
label='Смоделированный ряд', color='red')
plt.plot(forecast_ma _index_arima[len(ts_train):], forecast_ma _arma[len(ts_train):],
label='Прогноз', color='green')
plt.title('Прогнозирование временного ряда с помощью скользящих средних (модель ARIMA)')
plt.xlabel('Дата')
plt.ylabel('Продажи')
plt.legend()
plt.show()
print(model_ma _arima.summary())
Вывод:
SARIMAX Results
==============================================================================
Dep. Variable:
salessum No. Observations:
470
Model:
ARIMA(0, 0, 7) Log Likelihood
- 3260.822
Date:
Wed, 23 Apr 2025 AIC
6539.643
Time:
18:09:48 BIC
6577.018
Sample:
01-02-2021 HQIC
6554.347
-
04-16-2022
Covariance Type:
opg
==============================================================================
410 Прогнозирование временных рядов с конструированием признаков
coef
std err
z
P>|z|
[0.025
0.975]
------------------------------------------------------------------------------
const
824.8918
43.446
18.987
0.000
739.740
910.044
ma.L1
0.5864
0.038
15.333
0.000
0.511
0.661
ma.L2
0.3129
0.043
7.251
0.000
0.228
0.398
ma.L3
0.2044
0.038
5.354
0.000
0.130
0.279
ma.L4
0.1104
0.041
2.685
0.007
0.030
0.191
ma.L5
0.1054
0.043
2.436
0.015
0.021
0.190
ma.L6
0.1883
0.039
4.768
0.000
0.111
0.266
ma.L7
0.5092
0.035
14.363
0.000
0.440
0.579
sigma2
6.183e+04 3264.994
18.936
0.000 5.54e+04 6.82e+04
===================================================================================
Ljung-Box (L1) (Q):
8.09 Jarque-Bera (JB):
171.07
Prob(Q):
0.00 Prob(JB):
0.00
Heteroskedasticity (H):
0.69 Skew:
0.97
Prob(H) (two-sided):
0.02 Kurtosis:
5.22
===================================================================================
Рис. П2.7 Прогнозирование временного ряда
с помощью скользящих средних (модель ARIMA)
Как видим, показатель метрики AIC возрос незначительно – до 6539.643,
и это говорит о том, что обе модели – авторегрессионная и на основе скользя-
щих средних – обладают примерно одинаковым качеством предсказаний. Об
этом же говорит и сильно не изменившаяся метрика MSE:
from sklearn.metrics import mean_squared_error
y_test = ts_test
y_pred = forecast_ma _arma[len(ts_train):]
Применение методов из семейства ARIMA и алгоритмов машинного обучения 411
mse_ma = mean_squared_error(y_test, y_pred)
print(f'MSE скользящих средних (модель ARIMA): {mse_ma}')
Вывод:
MSE скользящих средних (модель ARIMA): 156397.4970828218
А что, если мы объединим авторегрессионную модель и модель на осно-
ве скользящих средних? Получим авторегрессионную модель скользящего
среднего.
Модель ARMA
Модель авторегрессии и скользящего среднего (autoregressive moving average –
ARMA) представляет комбинацию двух описанных выше моделей и полага-
ется как на признаки на основе временного лага, так и на переменные на
основе скользящих средних. Эта модель не включает в себя операцию диф-
ференцирования, в связи с чем может применяться только к стационарным
временным рядам, коим наш ряд и является.
Для реализации этой модели на практике можно воспользоваться тем же
классом ARIMA, с которым мы уже работали ранее. Но на этот раз мы зададим
значения обеих переменных – p и q:
# Параметры для модели ARMA
p = 7 # Значение для порядка авторегрессии
d = 0 # Значение для количества вычислений разностей
q = 7 # Значение для порядка скользящего среднего
model_arma_arima = sm.tsa.ARIMA(ts_train, order=(p, d, q)).fit()
# Прогнозируем весь временной ряд
forecast_arma _arma = model_arma _arima.predict(start=0, end=len(ts)-1)
# Создаем индекс для вывода на графике
forecast_arma _index_arima = pd.date_range(start=ts.index[0], periods=len(ts))
# Строим график
plt.figure(figsize=(12, 6))
plt.plot(ts.index, ts, label='Исходные данные')
plt.plot(forecast_arma _index_arima[:len(ts_train)], forecast_arma _arma[:len(ts_train)],
label='Смоделированный ряд', color='red')
plt.plot(forecast_arma _index_arima[len(ts_train):], forecast_arma _arma[len(ts_train):],
label='Прогноз', color='green')
plt.title('Прогнозирование временного ряда с помощью авторегрессии и скользящих средних
(модель ARIMA)')
plt.xlabel('Дата')
plt.ylabel('Продажи')
plt.legend()
plt.show()
print(model_arma _arima.summary())
412 Прогнозирование временных рядов с конструированием признаков
Вывод:
SARIMAX Results
==============================================================================
Dep. Variable:
salessum No. Observations:
470
Model:
ARIMA(7, 0, 7) Log Likelihood
- 3115.454
Date:
Thu, 24 Apr 2025 AIC
6262.908
Time:
11:24:32 BIC
6329.351
Sample:
01-02-2021 HQIC
6289.048
-
04-16-2022
Covariance Type:
opg
==============================================================================
coef
std err
z
P>|z|
[0.025
0.975]
------------------------------------------------------------------------------
const
824.6797 670.528
1.230
0.219 -489.531 2138.890
ar.L1
0.0226
0.025
0.919
0.358
-0.026
0.071
ar.L2
- 0 .0157
0.026
-0.615
0.538
-0.066
0.034
ar.L3
- 0 .0089
0.025
-0.363
0.717
-0.057
0.039
ar.L4
0.0158
0.025
0.625
0.532
-0.034
0.065
ar.L5
- 0 .0390
0.024
-1.616
0.106
-0.086
0.008
ar.L6
0.0333
0.023
1.465
0.143
-0.011
0.078
ar.L7
0.9350
0.025
37.316
0.000
0.886
0.984
ma.L1
0.5996
0.049
12.283
0.000
0.504
0.695
ma.L2
0.5155
0.061
8.480
0.000
0.396
0.635
ma.L3
0.5426
0.062
8.797
0.000
0.422
0.663
ma.L4
0.3721
0.064
5.780
0.000
0.246
0.498
ma.L5
0.4656
0.052
8.873
0.000
0.363
0.568
ma.L6
0.3224
0.054
6.016
0.000
0.217
0.427
ma.L7
- 0 .3372
0.054
-6.297
0.000
-0.442
- 0.232
sigma2
3.721e+04 1633.191
22.785
0.000
3.4e+04 4.04e+04
===================================================================================
Ljung-Box (L1) (Q):
1.44 Jarque-Bera (JB):
968.63
Prob(Q):
0.23 Prob(JB):
0.00
Heteroskedasticity (H):
0.81 Skew:
0.42
Prob(H) (two-sided):
0.19 Kurtosis:
9.98
===================================================================================
Как видите, сочетание авторегрессии с учетом скользящих средних по-
зволило нам снизить показатель AIC до 6262.908 . Посмотрим, каким будет
значение метрики MSE:
from sklearn.metrics import mean_squared_error
y_test = ts_test
y_pred = forecast_arma _arma[len(ts_train):]
mse_arma = mean_squared_error(y_test, y_pred)
print(f'MSE авторегрессии и скользящих средних (модель ARIMA): {mse_arma}')
Вывод:
MSE авторегрессии и скользящих средних (модель ARIMA): 121122.25860792094
Применение методов из семейства ARIMA и алгоритмов машинного обучения 413
Рис. П2.8 Прогнозирование временного ряда
с помощью авторегрессии и скользящих средних (модель ARIMA)
Мы видим, что и MSE существенно снизилась – с 156 397 в модели MA до
121 122 в модели ARMA. Это неплохая прибавка к предсказательной точности
модели.
А как же все-таки мы выбираем значения для параметров p и q? Здесь нам
на помощь приходит...
Анализ ACF и PACF
Под загадочными аббревиатурами ACF и PACF скрываются функция автокор-
реляции (autocorrelation function – ACF) и частичная функция автокорреля-
ции (partial autocorrelation function, PACF). Эти статистические меры позво-
ляют анализировать зависимости между текущими значениями временного
ряда и значениями прошлых периодов.
Функция автокорреляции измеряет линейную зависимость между значе-
ниями временного ряда и значениями, смещенными во времени. Иначе го-
воря, эта функция отвечает за то, как текущие значения переменной зависят
от значений предыдущих периодов. В то же время частичная функция авто-
корреляции позволяет исключить влияние промежуточных смещений, что
помогает выявлять прямые зависимости между текущим значением пере-
менной и смещенным. В отличие от функции автокорреляции, частичная
функция автокорреляции показывает непосредственную корреляцию для
каждого временного сдвига.
График функции автокорреляции можно использовать для подбора пара-
метра скользящих средних, а график частичной функции автокорреляции –
для подбора параметра, отвечающего за сдвиги во времени (авторегрессия).
414 Прогнозирование временных рядов с конструированием признаков
Взглянуть на эти графики нам помогут функции plot_acf() и plot_pacf() и з
модуля statsmodels.api.graphics.tsa:
# Анализ графиков ACF и PACF
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))
sm.graphics.tsa.plot_acf(ts, ax=ax1, lags=75, title='График автокорреляции (ACF)')
sm.graphics.tsa.plot_pacf(ts, ax=ax2, lags=75, title='График частичной автокорреляции
(PACF)')
plt.show()
Рис. П2.9 Графики автокорреляции (ACF) и частичной автокорреляции (PACF)
На рис. П2.9 мы наблюдаем хорошо заметные пики автокорреляции на от-
метке в семь дней, что связано со вполне объяснимой недельной циклично-
стью продаж в булочных. Именно поэтому мы использовали такие значения
для параметров p и q.
Теперь давайте перейдем к рассмотрению модели ARIMA, позволяющей
проводить внутреннее дифференцирование временного ряда для приведе-
ния его к стационарному виду.
Модель ARIMA
Интегрированная модель авторегрессии и скользящего среднего (autore-
gressive integrated moving average – ARIMA) обладает всеми свойствами рас-
смотренной выше модели ARMA, но также дает возможность выполнить
однократное или многократное дифференцирование (расчет разностей)
временного ряда, чтобы привести его к стационарному виду. За количество
этих операций отвечает параметр d. Давайте установим его в единицу и по-
смотрим, как изменится наша модель:
Применение методов из семейства ARIMA и алгоритмов машинного обучения 415
# Параметры для модели ARIMA
p = 7 # Значение для порядка авторегрессии
d = 1 # Значение для количества вычислений разностей
q = 7 # Значение для порядка скользящего среднего
model_arima_arima = sm.tsa.ARIMA(ts_train, order=(p, d, q)).fit()
# Прогнозируем весь временной ряд
forecast_arima_arima = model_arima_arima.predict(start=0, end=len(ts)-1)
# Создаем индекс для вывода на графике
forecast_arima_index_arima = pd.date_range(start=ts.index[0], periods=len(ts))
# Строим график
plt.figure(figsize=(12, 6))
plt.plot(ts.index, ts, label='Исходные данные')
plt.plot(forecast_arima_index_arima[:len(ts_train)], forecast_arima_arima[:len(ts_train)],
label='Смоделированный ряд', color='red')
plt.plot(forecast_arima_index_arima[len(ts_train):], forecast_arima_arima[len(ts_train):],
label='Прогноз', color='green')
plt.title('Прогнозирование временного ряда с помощью метода ARIMA (модель ARIMA)')
plt.xlabel('Дата')
plt.ylabel('Продажи')
plt.legend()
plt.show()
print(model_arima_arima.summary())
Вывод:
SARIMAX Results
==============================================================================
Dep. Variable:
salessum No. Observations:
470
Model:
ARIMA(7, 1, 7) Log Likelihood
- 3 095.737
Date:
Thu, 24 Apr 2025 AIC
6221.474
Time:
12:58:05 BIC
6283.733
Sample:
01-02-2021 HQIC
6245.971
-
04-16-2022
Covariance Type:
opg
==============================================================================
coef
std err
z
P>|z|
[0.025
0.975]
------------------------------------------------------------------------------
ar.L1
- 0 .5132
0.097
-5.316
0.000
-0.702
- 0.324
ar.L2
- 0 .5020
0.094
-5.313
0.000
-0.687
- 0.317
ar.L3
- 0 .5054
0.093
-5.441
0.000
-0.687
- 0.323
ar.L4
- 0 .4974
0.092
-5.394
0.000
-0.678
- 0.317
ar.L5
- 0 .4991
0.092
-5.412
0.000
-0.680
- 0.318
ar.L6
- 0 .5021
0.091
-5.494
0.000
-0.681
- 0.323
ar.L7
0.4810
0.092
5.235
0.000
0.301
0.661
ma.L1
0.2251
0.085
2.652
0.008
0.059
0.391
ma.L2
0.1813
0.079
2.285
0.022
0.026
0.337
ma.L3
0.1879
0.067
2.806
0.005
0.057
0.319
416 Прогнозирование временных рядов с конструированием признаков
ma.L4
0.1157
0.054
2.153
0.031
0.010
0.221
ma.L5
0.1313
0.059
2.237
0.025
0.016
0.246
ma.L6
0.1261
0.060
2.116
0.034
0.009
0.243
ma.L7
- 0 .6481
0.052 -12.485
0.000
-0.750
- 0.546
sigma2
3.519e+04 1442.600
24.394
0.000 3 .24e+04
3.8e+04
===================================================================================
Ljung-Box (L1) (Q):
0.04 Jarque-Bera (JB):
1022.30
Prob(Q):
0.84 Prob(JB):
0.00
Heteroskedasticity (H):
0.90 Skew:
0.67
Prob(H) (two-sided):
0.53 Kurtosis:
10.11
===================================================================================
Рис. П2.10 Прогнозирование временного ряда
с помощью метода ARIMA (модель ARIMA)
Как видите, несмотря на то что мы считали наш временной ряд стацио-
нарным, добавление одного шага дифференцирования помогло нам снизить
показатель AIC с 6262.908 до 6221.474. Посмотрим, что скажет метрика MSE:
from sklearn.metrics import mean_squared_error
y_test = ts_test
y_pred = forecast_arima_arima[len(ts_train):]
mse_arima = mean_squared_error(y_test, y_pred)
print(f'MSE метода ARIMA (модель ARIMA): {mse_arima}')
Вывод:
MSE метода ARIMA (модель ARIMA): 110398.83222253955
Применение методов из семейства ARIMA и алгоритмов машинного обучения 417
Неплохо – метрика MSE также снизилась с 121 122 до 110 398.
Вы заметили, что параметров в нашей модели становится все больше,
а дальше их количество еще увеличится. Как же мы их подобрали? Вы можете
устанавливать значения этих параметров вручную, но лучше будет подобрать
их с помощью простого перебора всех возможных комбинаций. Такая мето-
дика довольно часто применяется при работе с различными алгоритмами
машинного обучения. Рассмотрим ее подробнее.
Подбор параметров для модели ARIMA
Подбор гиперпараметров при помощи перебора – это всегда дорого с точки
зрения использования ресурсов. В связи с этим лучше задействовать все до-
ступные вам техники распределенных вычислений, чтобы ускорить процесс.
Мы воспользуемся классом Pool из модуля multiprocessing, с помощью ко-
торого создадим пул процессов, – он поможет нам задействовать все имею-
щиеся на нашем компьютере ядра центрального процессора:
import pandas as pd
import statsmodels.api as sm
from multiprocessing import Pool
from datetime import datetime
import itertools
def funcCPU(params):
ts_train = params['ts_train']
pdq = params['pdq']
prc_id = params['prc_id']
print(f'Процесс {prc_id} стартовал: pdq = {pdq}')
try:
mod = sm.tsa.ARIMA(ts_train, order=pdq)
results = mod.fit()
res_aic = results.aic
except:
res_aic = 0
finally:
print(f'Процесс {prc_id} финишировал:')
print('ARIMA {} - AIC: {}'.format(pdq, res_aic))
return dict(res_aic=res_aic, pdq=pdq, prc_id=prc_id)
def main():
# Считываем данные, переименовываем столбец и заполняем пропуски
df_bak = pd.read_csv('Bakery sales.csv')
df_bak['unit_price'] = df_bak['unit_price'].str.replace(' €', '').str.replace(',',
'.').astype(float)
df_bak['sales'] = df_bak['Quantity'] * df_bak['unit_price']
df_bak['IS_CROISSANT'] = df_bak['article'] == 'CROISSANT'
df_bak = df_bak.groupby(['date'], as_index=False).agg({'sales': 'sum', 'IS_CROISSANT':
['sum', 'count']})
df_bak.columns = list(map(''.join, df_bak.columns.values))
df_bak['croissant_perc'] = df_bak['IS_CROISSANTsum'] / df_bak['IS_CROISSANTcount']
418 Прогнозирование временных рядов с конструированием признаков
df_bak['date'] = pd.to_datetime(df_bak['date'])
df_bak = df_bak.set_index('date')
idx = pd.date_range(df_bak.index[0], df_bak.index[-1])
df_bak = df_bak.reindex(idx, method='bfill')
df_bak = df_bak[df_bak.index < '2022-07-10']
df_bak = df_bak.bfill()
ts = df_bak['salessum']
# Разбиваем набор данных на обучающую и тестовую выборки
train_size = int(len(ts) * 0.85)
ts_train, ts_test = ts[:train_size], ts[train_size:]
# Определим диапазоны переменных p, d и q
p=[1,3,5,7,14]
d=[0,1]
q=[1,3,5,7,14]
# Сгенерируем все комбинации p, d и q
pdq = list(itertools.product(p, d, q))
params = []
i=1
for param in pdq:
params.append(dict(ts_train=ts_train, pdq=param, prc_id=i))
i+=1
start_time = datetime.now()
pool = Pool()
res = pool.map(funcCPU, params)
# Убираем нули
res = [r for r in res if r['res_aic'] > 0]
# Берем минимум
print(f'Лучший набор: {sorted(res, key=lambda d: d["res_aic"])[0]}')
finish_time = datetime.now() - start_time
print(f'Прошло: {finish_time}')
if __name __ == ' _ _main__':
main()
Вывод:
Процесс 1 стартовал: pdq = (1, 0, 1)
Процесс 3 стартовал: pdq = (1, 0, 5)
Процесс 5 стартовал: pdq = (1, 0, 14)
Процесс 1 финишировал:
ARIMA (1, 0, 1) - AIC: 6656.966044792375
Процесс 2 стартовал: pdq = (1, 0, 3)
Процесс 7 стартовал: pdq = (1, 1, 3)
Процесс 9 стартовал: pdq = (1, 1, 7)
...
Процесс 44 финишировал:
ARIMA (14, 0, 7) - AIC: 6241.352944404989
Процесс 50 финишировал:
Применение методов из семейства ARIMA и алгоритмов машинного обучения 419
ARIMA (14, 1, 14) - AIC: 6241.473825093013
Лучший набор: {'res_aic': 6221.474296022096, 'pdq': (7, 1, 7), 'prc_id': 39}
Прошло: 0:00:14.669738
Сначала рассмотрим функцию funcCPU(), в которой выполняется вся работа
по проверке параметров. В ней мы создаем экземпляр класса ARIMA, обучаем
модель и возвращаем в составе словаря набор переданных параметров и ито-
говое значение метрики AIC.
В основной функции main() мы загружаем и предобрабатываем наш набор
данных, затем разбиваем датафрейм на обучающую и тестовую выборки
и определяем диапазоны значений для переменных p , d и q . После этого
в переменной pdq мы собираем все возможные комбинации этих значений
и формируем список params, в котором будут храниться параметры для оче-
редного запуска функции funcCPU().
Далее мы применяем метод map() для параллельного запуска функции
funcCPU() на всех доступных ядрах процессора. В результате в переменной res
у нас накапливаются все результаты вызова функции, и нам остается выбрать
вариант с минимальным значением метрики AIC.
Мы сократили вывод функции, но видим, что в результате минимальный
показатель AIC, равный 6221.474296022096, оказался у сочетания параметров
(7, 1, 7). Именно с такими параметрами мы и запустили нашу модель ARIMA
в предыдущем примере.
Теперь перейдем к описанию расширения модели ARIMA, получившей
название SARIMA.
Модель SARIMA
Интегрированная модель авторегрессии и скользящего среднего (seasonal
autoregressive integrated moving average – SARIMA) добавляет к модели ARIMA
сезонные составляющие, что увеличивает общее количество параметров для
подбора.
Новые параметры – это P, D, Q и s, где s – это длина сезонного цикла. На-
пример, для месячных данных s = 12, для квартальных – s = 4 и т. д. Полная
запись модели выглядит как SARIMA(p,d,q)(P,D,Q)s. Модель SARIMA может
значительно превосходить ARIMA при работе с сезонными данными.
В классе ARIMA этим новым параметрам соответствует аргумент seasonal_
order, который мы также будем подбирать опытным путем:
import pandas as pd
import statsmodels.api as sm
from multiprocessing import Pool
from datetime import datetime
import itertools
def funcCPU(params):
ts_train = params['ts_train']
pdq = params['pdq']
420 Прогнозирование временных рядов с конструированием признаков
seasonal_pdq = params['seasonal_pdq']
prc_id = params['prc_id']
print(f'Процесс {prc_id} стартовал: pdq = {pdq}, spdq = {seasonal_pdq}')
try:
mod = sm.tsa.ARIMA(ts_train, order=pdq, seasonal_order=seasonal_pdq)
results = mod.fit()
res_aic = results.aic
except:
res_aic = 0
finally:
print(f'Процесс {prc_id} финишировал:')
print('ARIMA {} x {} - AIC: {}'.format(pdq, seasonal_pdq, res_aic))
return dict(res_aic=res_aic, pdq=pdq, seasonal_pdq=seasonal_pdq, prc_id=prc_id)
def main():
# Считываем данные, переименовываем столбец и заполняем пропуски
df_bak = pd.read_csv('Bakery sales.csv')
df_bak['unit_price'] = df_bak['unit_price'].str.replace(' €', '').str.replace(',',
'.').astype(float)
df_bak['sales'] = df_bak['Quantity'] * df_bak['unit_price']
df_bak['IS_CROISSANT'] = df_bak['article'] == 'CROISSANT'
df_bak = df_bak.groupby(['date'], as_index=False).agg({'sales': 'sum', 'IS_CROISSANT':
['sum', 'count']})
df_bak.columns = list(map(''.join, df_bak.columns.values))
df_bak['croissant_perc'] = df_bak['IS_CROISSANTsum'] / df_bak['IS_CROISSANTcount']
df_bak['date'] = pd.to_datetime(df_bak['date'])
df_bak = df_bak.set_index('date')
# Заполняем пропуски в датах с bfill
idx = pd.date_range(df_bak.index[0], df_bak.index[-1])
df_bak = df_bak.reindex(idx, method='bfill')
# До 2022-07-10
df_bak = df_bak[df_bak.index < '2022-07-10']
df_bak['lag_7'] = df_bak['salessum'].shift(7)
df_bak['RollingMean_7'] = df_bak['salessum'].rolling(window=7, min_periods=3).mean()
df_bak = df_bak.bfill()
ts = df_bak['salessum']
# Разбиваем набор данных на обучающую и тестовую выборки
train_size = int(len(ts) * 0.85)
ts_train, ts_test = ts[:train_size], ts[train_size:]
print(f'Размер обучающей выборки: {len(ts_train)}')
print(f'Размер тестовой выборки: {len(ts_test)}')
# Определим диапазоны переменных p, d и q
p=[0,1,7]
d=[1]
q=[0,1,7]
# Сгенерируем все комбинации p, d и q
pdq = list(itertools.product(p, d, q))
# Сгенерируем все комбинации сезонных параметров P, D и Q
seasonal_pdq = [(x[0], x[1], x[2], 12) for x in pdq]
params = []
Применение методов из семейства ARIMA и алгоритмов машинного обучения 421
i=1
for param in pdq:
for param_seasonal in seasonal_pdq:
params.append(dict(ts_train=ts_train, pdq=param, seasonal_pdq=param_seasonal,
prc_id=i))
i+=1
start_time = datetime.now()
pool = Pool()
res = pool.map(funcCPU, params)
# Убираем нули
res = [r for r in res if r['res_aic'] > 0]
# Берем минимум
print(f'Лучший набор: {sorted(res, key=lambda d: d["res_aic"])[0]}')
finish_time = datetime.now() - start_time
print(f'Прошло: {finish_time}')
if __name __ == ' _ _main__':
main()
Вывод:
Процесс 1 стартовал: pdq = (0, 1, 0), spdq = (0, 1, 0, 12)
Процесс 4 стартовал: pdq = (0, 1, 0), spdq = (1, 1, 0, 12)
Процесс 1 финишировал:
ARIMA (0, 1, 0) x (0, 1, 0, 12) - AIC: 6916.549455823246
Процесс 2 стартовал: pdq = (0, 1, 0), spdq = (0, 1, 1, 12)
Процесс 7 стартовал: pdq = (0, 1, 0), spdq = (7, 1, 0, 12)
Процесс 10 стартовал: pdq = (0, 1, 1), spdq = (0, 1, 0, 12)
Процесс 13 стартовал: pdq = (0, 1, 1), spdq = (1, 1, 0, 12)
Процесс 16 стартовал: pdq = (0, 1, 1), spdq = (7, 1, 0, 12)
Процесс 19 стартовал: pdq = (0, 1, 7), spdq = (0, 1, 0, 12)
Процесс 4 финишировал:
ARIMA (0, 1, 0) x (1, 1, 0, 12) - AIC: 6783.781507004731
...
Процесс 80 финишировал:
ARIMA (7, 1, 7) x (7, 1, 1, 12) - AIC: 6256.878068398155
Процесс 81 стартовал: pdq = (7, 1, 7), spdq = (7, 1, 7, 12)
Процесс 81 финишировал:
ARIMA (7, 1, 7) x (7, 1, 7, 12) - AIC: 6261.1264102463865
Лучший набор: {'res_aic': 6211.92700338764, 'pdq': (7, 1, 0), 'seasonal_pdq': (1, 1, 1,
12), 'prc_id': 59}
Прошло: 0:12:06.717818
Как видите, лучшим вариантом оказался набор параметров 'pdq': (7, 1, 0),
'seasonal_pdq': (1, 1, 1, 12). С этим набором и запустим нашу модель. Здесь мы
воспользуемся классом SARIMAX из модуля statsmodels.tsa.statespace.sarimax:
from statsmodels.tsa.statespace.sarimax import SARIMAX
# Оценка модели ARIMA
p = 7 # Значение для порядка авторегрессии
422 Прогнозирование временных рядов с конструированием признаков
d = 1 # Значение для количества вычислений разностей
q = 0 # Значение для порядка скользящего среднего
# Оценка модели SARIMA
seas_p = 1 # Значение для порядка авторегрессии
seas_d = 1 # Значение для количества вычислений разностей
seas_q = 1 # Значение для порядка скользящего среднего
seas_s = 12 # Сезонность
model_sarima = SARIMAX(ts_train, order=(p, d, q), seasonal_order=(seas_p, seas_d, seas_q,
seas_s)).fit()
# Прогнозируем весь временной ряд
forecast_sarima = model_sarima.predict(start=0, end=len(ts)-1)
# Создаем индекс для вывода на графике
forecast_sarima_index = pd.date_range(start=ts.index[0], periods=len(ts))
# Строим график
plt.figure(figsize=(12, 6))
plt.plot(ts.index, ts, label='Исходные данные')
plt.plot(forecast_sarima_index[:len(ts_train)], forecast_sarima[:len(ts_train)],
label='Смоделированный ряд', color='red')
plt.plot(forecast_sarima_index[len(ts_train):], forecast_sarima[len(ts_train):],
label='Прогноз', color='green')
plt.title('Прогнозирование временного ряда с помощью метода SARIMA (модель SARIMAX)')
plt.xlabel('Дата')
plt.ylabel('Продажи')
plt.legend()
plt.show()
print(model_sarima.summary())
Вывод:
SARIMAX Results
==========================================================================================
==
Dep. Variable:
salessum No. Observations:
470
Model:
SARIMAX(7, 1, 0)x(1, 1, [1], 12) Log Likelihood
- 3095.964
Date:
Thu, 24 Apr 2025 AIC
6211.927
Time:
15:39:17 BIC
6253.174
Sample:
01-02-2021 HQIC
6228.174
-
04-16-2022
Covariance Type:
opg
==============================================================================
coef
std err
z
P>|z|
[0.025
0.975]
------------------------------------------------------------------------------
ar.L1
- 0 .3387
0.031 -10.906
0.000
-0.400
- 0.278
Применение методов из семейства ARIMA и алгоритмов машинного обучения 423
ar.L2
- 0 .3379
0.037
-9.212
0.000
-0.410
- 0.266
ar.L3
- 0 .3010
0.032
-9.417
0.000
-0.364
- 0.238
ar.L4
- 0 .3404
0.031 -10.995
0.000
-0.401
- 0.280
ar.L5
- 0 .3011
0.044
-6.886
0.000
-0.387
- 0.215
ar.L6
- 0 .2874
0.034
-8.434
0.000
-0.354
- 0.221
ar.L7
0.4330
0.032
13.523
0.000
0.370
0.496
ar.S .L12
- 0 .0427
0.053
-0.801
0.423
-0.147
0.062
ma.S .L12
- 0 .9998
11.202
-0.089
0.929
-22.955
20.956
sigma2
3.993e+04 4.47e+05
0.089
0.929
-8 .36e+05 9.16e+05
===================================================================================
Ljung-Box (L1) (Q):
0.00 Jarque-Bera (JB):
247.28
Prob(Q):
0.98 Prob(JB):
0.00
Heteroskedasticity (H):
0.74 Skew:
0.26
Prob(H) (two-sided):
0.07 Kurtosis:
6.57
===================================================================================
Рис. П2.11 Прогнозирование временного ряда
с помощью метода SARIMA (модель SARIMAX)
Значение метрики AIC осталось почти на том же уровне – 6211.927 против
6221.474. Посмотрим, что произошло с MSE:
from sklearn.metrics import mean_squared_error
y_test = ts_test
y_pred = forecast_sarima[len(ts_train):]
mse_sarima = mean_squared_error(y_test, y_pred)
print(f'MSE метода SARIMA (модель ARIMA): {mse_sarima}')
424 Прогнозирование временных рядов с конструированием признаков
Вывод:
MSE метода SARIMA (модель ARIMA): 156413.6223265438
Как видите, значение MSE (156 413) сильно выросло – модель ARIMA выда-
вала нам значение 110 398. Таким образом, можно прийти к выводу, что нуж-
но смотреть не только на AIC, но и отталкиваться от метрики MSE. Если вы
заметили, мы в этот раз задали показатель q равным нулю, т. е . исключили из
базового анализа составляющую, отвечающую за скользящее среднее. Если
задать значение 7, мы получим почти такой же показатель AIC (6239.085), но
при этом более низкую MSE на уровне 107 193.
Теперь рассмотрим в качестве альтернативы применение алгоритмов ма-
шинного обучения с целью выполнения прогнозирования временных рядов.
Шаг 2(б). Применение методов машинного обучения
для прогнозирования временных рядов
Итак, мы рассмотрели варианты с использованием моделей AR, MA, ARMA,
ARIMA и SARIMA для прогнозирования временных рядов. Теперь попробуем
с той же целью воспользоваться традиционными алгоритмами машинного
обучения, такими как метод случайного леса или метод градиентного спуска.
Метод случайного леса
Метод случайного леса представляет собой ансамблевый алгоритм, который
традиционно хорошо справляется с задачей прогнозирования временных
рядов по причине возможности учета сложных зависимостей между предик-
торами. В ходе использования этого метода строится большое количество
деревьев, на основе результатов которых делаются предсказания.
Преимущество использования метода случайного леса заключается в воз-
можности использовать нелинейные зависимости между переменными
и устойчивости к переобучению. Применительно к задаче прогнозирования
временных рядов это позволяет методу использовать разные переменные на
основе временного лага, скользящие показатели и индикаторы сезонности.
В результате метод получает возможность обнаруживать шаблоны, незамет-
ные в исходных данных.
Кроме того, метод случайного леса обладает способностью ранжирова-
ния предикторов по значимости, что позволяет оставить в модели только те
переменные, которые оказывают наибольшее влияние на прогноз.
Применение методов из семейства ARIMA и алгоритмов машинного обучения 425
Давайте воспользуемся этим методом для предсказания значений нашего
временного ряда:
# Готовим предикторы
# Добавляем 14 переменных на основе временного лага
for i in range(14, 0, -1):
df_bak['t-' + str(i)] = df_bak['salessum'].shift(i)
# Добавляем переменную на основе недельного скользящего среднего
df_bak['RollingMean_7'] = df_bak['salessum'].rolling(window=7, min_periods=3).mean()
# Номер месяца
df_bak['month'] = df_bak.index.month
# Обратное заполнение
df_bak = df_bak.bfill()
print(df_bak.head())
Вывод:
salessum croissant_perc t-14 t-13 ...
t-2
t-1 RollingMean_7 month
2021-01-02 987.85
0.065753 987.85 987.85 ... 987.85 987.85
821.3500
1
2021-01-03 1014.30
0.061333 987.85 987.85 ... 987.85 987.85
821.3500
1
2021-01-04 461.90
0.023810 987.85 987.85 ... 987.85 1014.30
821.3500
1
2021-01-05 515.70
0.034043 987.85 987.85 ... 1014.30 461.90
744.9375
1
2021-01-06 544.00
0.023256 987.85 987.85 ... 461.90 515.70
704.7500
1
[5 rows x 18 columns]
Мы добавили в наш набор данных 14 переменных со смещением, одно
скользящее среднее с окном в неделю и номер месяца. Давайте посмотрим,
как наши продажи зависят от продаж недельной давности и от показателя
скользящего среднего за неделю:
fig, ax = plt.subplots()
ax = sns.regplot(x='t-7', y='salessum', data=df_bak, ci=None, scatter_
kws=dict(color='0.25'))
# Устанавливаем заголовок и подписи осей
ax.set_title('Диаграмма рассеяния с признаком на основе лага (запаздывание 7 дней)')
ax.set_xlabel('Сумма с запаздыванием (lag = 7)')
ax.set_ylabel('Сумма')
plt.show()
426 Прогнозирование временных рядов с конструированием признаков
Рис. П2.12 Диаграмма рассеяния с признаком на основе лага
(запаздывание 7 дней)
fig, ax = plt.subplots()
ax = sns.regplot(x='RollingMean_7', y='salessum', data=df_bak, ci=None, scatter_
kws=dict(color='0.25'))
# Устанавливаем заголовок и подписи осей
ax.set_title('Диаграмма рассеяния со скользящим признаком (окно 7)')
ax.set_xlabel('Скользящая сумма (окно 7)')
ax.set_ylabel('Сумма')
plt.show()
Применение методов из семейства ARIMA и алгоритмов машинного обучения 427
Рис. П2.13 Диаграмма рассеяния со скользящим признаком (окно 7)
Как видим, общая тенденция зависимости существует, хотя и не является
доминирующей.
Мы создали довольно много новых предикторов, но использование их всех
может приводить к чрезмерному усложнению модели и ее переобучению.
В связи с этим мы воспользуемся методом рекурсивного исключения призна-
ков (recursive feature elimination – RFE) для поиска и удаления избыточных
переменных:
from sklearn.ensemble import RandomForestRegressor
from sklearn.feature_selection import RFE
# Определяем предикторы и целевую переменную
X = df_bak[['t-14', 't-13', 't-12', 't-11', 't-10', 't-9', 't-8', 't-7', 't-6', 't-5', 't-
4', 't-3', 't-2', 't-1', 'RollingMean_7']]
y = df_bak['salessum']
# Разбиваем набор данных на обучающую и тестовую выборки
train_size = int(len(X) * 0.85)
428 Прогнозирование временных рядов с конструированием признаков
X_train, X_test = X[:train_size], X[train_size:]
y_train, y_test = y[:train_size], y[train_size:]
print(f'Размер обучающей выборки: {len(X_train)}')
print(f'Размер тестовой выборки: {len(X_test)}')
print(f'Всего предикторов: {X.shape[1]+1}')
mses = []
# Подбор параметра n_features_to_select
for n in range(1, X.shape[1]+1):
rfe = RFE(RandomForestRegressor(n_estimators=100, random_state=1), n_features_to_
select=n)
fit = rfe.fit(X_train, y_train)
y_pred = fit.predict(X_test)
mse_rf = mean_squared_error(y_test, y_pred)
mses.append(dict(n=n, mse_rf=mse_rf))
best_choice = sorted(mses, key=lambda d: d['mse_rf'])[0]
best_n = best_choice['n']
print(f'Лучший выбор n: {best_choice}')
# Строим оптимальную модель
rfe = RFE(RandomForestRegressor(n_estimators=100, random_state=1), n_features_to_
select=best_n)
fit = rfe.fit(X_train, y_train)
y_pred = fit.predict(X_test)
# Выводим график
plt.figure(figsize=(12, 6))
plt.plot(df_bak.index, y, label='Исходные данные')
plt.plot(df_bak.index[len(X_train):], y_pred, label='Прогноз', color='green')
plt.title('Прогнозирование временного ряда с помощью метода случайного леса')
plt.xlabel('Дата')
plt.ylabel('Продажи')
plt.legend()
plt.show()
Вывод:
Размер обучающей выборки: 470
Размер тестовой выборки: 84
Всего предикторов: 17
Лучший выбор n: {'n': 13, 'mse_rf': 74823.739970848}
Здесь мы определяем исходный список предикторов для модели и целевую
переменную, после чего разделяем наш набор данных на обучающую и тес-
товую выборки. После этого мы в цикле от единицы до общего количества
предикторов создаем модель случайного леса и с помощью класса RFE, остав-
ляем в ней заданное количество переменных. В списке mses накапливаем
информацию о метрике MSE для полученных моделей.
Завершив этот процесс, мы выбираем минимальное значение показателя
MSE и контрольно строим модель с таким количеством предикторов. В на-
шем случае оптимальное число предикторов оказалось равно 16, а значение
Применение методов из семейства ARIMA и алгоритмов машинного обучения 429
MSE остановилось на отметке 74 823. Заметим, что при использовании ме-
тодов из семейства ARIMA мы смогли добиться только результата 107 193.
Рис. П2.14 Прогнозирование временного ряда
с помощью метода случайного леса
Здесь мы использовали в качестве предикторов только созданные пере-
менные на основе временного лага и переменную с недельным скользящим
средним. Теперь давайте продемонстрируем, как можно улучшить качество
модели за счет добавления в нее компонентов даты и переменных взаимо-
действия из исходного набора.
Добавим к списку предикторов созданную ранее переменную month, в ко-
торой содержится номер месяца, и производную переменную croissant_perc,
показывающую долю продажи круассанов в заданный день:
X = df_bak[['t-14', 't-13', 't-12', 't-11', 't-10', 't-9', 't-8', 't-7', 't-6', 't-5', 't-
4', 't-3', 't-2', 't-1', 'RollingMean_7', 'month', 'croissant_perc']]
Теперь вывод будет уже таким:
Размер обучающей выборки: 470
Размер тестовой выборки: 84
Всего предикторов: 18
Лучший выбор n: {'n': 16, 'mse_rf': 69844.30916147609}
Как видите, нам удалось снизить MSE на 5000 единиц за счет учета скрытых
зависимостей исходных переменных. Если взглянуть на корреляционную
матрицу, можно заметить, что переменная croissant_perc положительным
430 Прогнозирование временных рядов с конструированием признаков
образом коррелирует с целевой переменной, но мы использовали ее лишь
в качестве демонстрации того, как модель случайного леса может использо-
вать не только временные предикторы для прогноза временных рядов:
print(df_bak[['croissant_perc', 'salessum']].corr())
Вывод:
croissant_perc salessum
croissant_perc
1.000000 0 .378857
salessum
0.378857 1.000000
Среди преимуществ использования метода случайного леса для прогнози-
рования временных рядов можно выделить следующие:
способность обнаруживать нелинейные зависимости между предикто-
рами и целевой переменной;
ансамблевое обучение, позволяющее комбинировать предсказания из
разных деревьев решений. Это помогает снизить вероятность пере-
обучения и повысить обобщающую способность модели, что бывает
особенно полезно при работе с зашумленными временными рядами;
возможность определять значимость предикторов, что позволяет ана-
литику оставить в модели только переменные, наиболее сильно влия-
ющие на предсказания;
способность работать с многомерными данными. При создании боль-
шого количества предикторов на основе временных лагов и скользя-
щих значений модель рискует обрести довольно большую размерность.
Модель случайного леса обычно хорошо справляется с такими ситуа-
циями и не страдает от проклятия размерности;
устойчивость к выбросам. Временные ряды зачастую содержат выбро-
сы и аномальные значения. Процесс бэггинга, лежащий в основе мето-
да случайного леса, и использование множества деревьев делает этот
алгоритм более устойчивым к выбросам по сравнению с подходами на
основе единственной модели;
обнаружение сезонности и трендов. Работая с предикторами на основе
временных лагов и скользящих значений, модель неявным образом
улавливает краткосрочные и долгосрочные шаблоны в данных, вклю-
чая сезонность и тренды;
отсутствие предположения о стационарности временного ряда. В отли-
чие от традиционных методов для работы с временными рядами вроде
ARMA, модель случайного леса не накладывает на исходные данные
требования относительно стационарности. Эта особенность позволя-
ет эффективно применять ее к временным рядам с изменчивыми во
времени статистическими свойствами;
параллельная обработка. Процесс обучения модели случайного леса
может быть легко запущен в распределенной среде, что повышает его
эффективность при работе с большими данными.
Применение методов из семейства ARIMA и алгоритмов машинного обучения 431
Метод градиентного бустинга
Теперь опробуем еще один эффективный ансамблевый метод машинного
обучения применительно к задаче прогнозирования временных рядов. Ме-
тод градиентного бустинга основан на итеративной корректировке ошибок
предыдущих моделей в процессе обучения.
Применительно к прогнозированию временных рядов польза от этого
метода базируется на его способности адаптироваться к различным типам
сконструированных признаков. К примеру, он может очень эффективно ис-
пользовать переменные на основе временного лага. Кроме того, бустинг от-
лично справляется с признаками на основе скользящих показателей, будь то
средние или стандартные отклонения.
Рассмотрим пример применения метода градиентного бустинга для про-
гнозирования нашего временного ряда:
from sklearn.ensemble import GradientBoostingRegressor
print(f'Размер обучающей выборки: {len(X_train)}')
print(f'Размер тестовой выборки: {len(X_test)}')
print(f'Всего предикторов: {X.shape[1]+1}')
mses = []
# Подбор параметра n_features_to_select
for n in range(1, X.shape[1]+1):
rfe = RFE(GradientBoostingRegressor(n_estimators=100, random_state=1), n_features_to_
select=n)
fit = rfe.fit(X_train, y_train)
y_pred = fit.predict(X_test)
mse_gb = mean_squared_error(y_test, y_pred)
mses.append(dict(n=n, mse_gb=mse_gb))
best_choice = sorted(mses, key=lambda d: d['mse_gb'])[0]
best_n = best_choice['n']
print(f'Лучший выбор n: {best_choice}')
# Строим оптимальную модель
rfe = RFE(GradientBoostingRegressor(n_estimators=100, random_state=1), n_features_to_
select=best_n)
fit = rfe.fit(X_train, y_train)
y_pred = fit.predict(X_test)
# Выводим график
plt.figure(figsize=(12, 6))
plt.plot(df_bak.index, y, label='Исходные данные')
plt.plot(df_bak.index[len(X_train):], y_pred, label='Прогноз', color='green')
plt.title('Прогнозирование временного ряда с помощью метода градиентного бустинга')
plt.xlabel('Дата')
plt.ylabel('Продажи')
plt.legend()
plt.show()
432 Прогнозирование временных рядов с конструированием признаков
Вывод:
Размер обучающей выборки: 470
Размер тестовой выборки: 84
Всего предикторов: 18
Лучший выбор n: {'n': 14, 'mse_gb': 57459.00469017896}
Рис. П2.15 Прогнозирование временного ряда
с помощью метода градиентного бустинга
Как видите, простая смена метода позволила снизить MSE с 69 844 до
57 459.
В заключение опробуем продвинутую версию метода градиентного бу-
стинга, именуемую экстремальным градиентным бустингом, или XGBoost.
Метод XGBoost
Метод экстремального градиентного бустинга (extreme gradient boosting),
или XGBoost, представляет собой улучшенную версию традиционного гра-
диентного бустинга, нацеленную на повышение скорости и эффективности.
Аспекты, в которых метод XGBoost превосходит градиентный бустинг:
регуляризация. Метод XGBoost располагает встроенными техника-
ми регуляризации L1 (лассо) и L2 (гребневая), что позволяет снизить
склонность к переобучению модели и повысить ее обобщающую спо-
собность. Это бывает особенно важно при работе с временными ряда-
ми, где моделям приходится отслеживать сложные шаблоны зависи-
мостей и при этом не проявлять излишнюю чувствительность к шуму
в исходных данных;
Применение методов из семейства ARIMA и алгоритмов машинного обучения 433
параллельная обработка. В отличие от градиентного бустинга, метод
XGBoost может быть запущен в распределенной вычислительной среде.
Это позволяет гораздо быстрее обучать модели, что бывает полезно
при необходимости часто обновлять модели или прогнозировать со-
бытия в реальном времени;
обрезка ветвей деревьев. Метод XGBoost использует улучшенный алго-
ритм обрезки ветвей деревьев, позволяющий обнаруживать и исклю-
чать проблемные узлы. Это приводит к образованию более компактных
и эффективных моделей, что важно при работе с временными рядами,
содержащими большое количество сконструированных признаков;
обработка пропущенных значений. В метод XGBoost встроен алгоритм
обработки пропущенных значений, который можно эффективно ис-
пользовать при работе с временными рядами, часто содержащими
пропуски;
значимость признаков. Метод XGBoost предоставляет аналитику де-
тальный отчет о значимости признаков, что позволяет принять взве-
шенное решение о том, какие переменные оставить в модели.
Все это делает данный метод исключительно подходящим для прогнози-
рования временных рядов. Давайте этим воспользуемся:
import xgboost as xgb
print(f'Размер обучающей выборки: {len(X_train)}')
print(f'Размер тестовой выборки: {len(X_test)}')
print(f'Всего предикторов: {X.shape[1]+1}')
mses = []
# Подбор параметра n_features_to_select
for n in range(1, X.shape[1]+1):
rfe = RFE(xgb.XGBRegressor(n_estimators=100, random_state=1), n_features_to_select=n)
fit = rfe.fit(X_train, y_train)
y_pred = fit.predict(X_test)
mse_xgb = mean_squared_error(y_test, y_pred)
mses.append(dict(n=n, mse_xgb=mse_xgb))
best_choice = sorted(mses, key=lambda d: d['mse_xgb'])[0]
best_n = best_choice['n']
print(f'Лучший выбор n: {best_choice}')
# Строим оптимальную модель
rfe = RFE(GradientBoostingRegressor(n_estimators=100, random_state=1), n_features_to_
select=best_n)
fit = rfe.fit(X_train, y_train)
y_pred = fit.predict(X_test)
# Выводим график
plt.figure(figsize=(12, 6))
plt.plot(df_bak.index, y, label='Исходные данные')
plt.plot(df_bak.index[len(X_train):], y_pred, label='Прогноз', color='green')
plt.title('Прогнозирование временного ряда с помощью метода экстремального градиентного
бустинга')
434 Прогнозирование временных рядов с конструированием признаков
plt.xlabel('Дата')
plt.ylabel('Продажи')
plt.legend()
plt.show()
Вывод:
Размер обучающей выборки: 470
Размер тестовой выборки: 84
Всего предикторов: 18
Лучший выбор n: {'n': 9, 'mse_xgb': 49531.89791193042}
Рис. П2.16 Прогнозирование временного ряда
с помощью метода экстремального градиентного бустинга
Последнее, что мы здесь отметим, − это то, что методу случайного леса для
достижения оптимального значения MSE потребовалось 16 из 18 исходных
предикторов, метод градиентного бустинга ограничился 14 признаками, а ме-
тоду экстремального градиентного бустинга хватило и девяти переменных.
Подбор гиперпараметров для методов
машинного обучения
В предыдущем разделе мы воспользовались некоторыми методами из семей-
ства ARIMA, а также тремя ансамблевыми алгоритмами машинного обучения
с целью прогнозирования временных рядов. Подбор параметров для методов
Подбор гиперпараметров для методов машинного обучения 435
ARIMA и SARIMA мы выполняли при помощи простого перебора моделей, а при
использовании случайного леса и градиентного бустинга ограничились только
эмпирическим выбором количества переменных в итоговой модели, восполь-
зовавшись классом RFE. Но сами эти модели также содержат немало средств для
анализа гиперпараметров, подбор которых может помочь найти компромисс
между сложностью итоговой модели и ее способностью к обобщению.
В этом разделе мы покажем, как можно осуществлять подбор гиперпарамет-
ров (hyperparameter tuning), и воспользуемся для этого двумя самыми рас-
пространенными способами, а именно методом подбора по сетке, с которым
уже познакомились при создании первого проекта, и случайным поиском.
Что такое гиперпараметры?
Гиперпараметры представляют собой важные настройки модели машинного
обучения, значения которых не могут быть определены в процессе обуче-
ния, а задаются заранее. По сути, они определяют структуру модели в целом
и формируют ее подход к обучению.
Степень влияния гиперпараметров на качество итоговой модели невоз-
можно переоценить. К примеру, в процессе тщательного подбора параметра,
отвечающего за предельную глубину деревьев решений, мы можем прийти
к компромиссу между недообучением модели и ее переобучением, а нахож-
дение оптимальной скорости обучения поможет сократить время, необхо-
димое для схождения алгоритма.
Каждая модель обладает своим набором гиперпараметров. Мы остановим-
ся на тех моделях, которые использовали для прогнозирования временного
ряда с продажей выпечки, и попробуем их оптимизировать еще больше.
Какие у них есть гиперпараметры?
Модель случайного леса:
n_estimators: количество деревьев в модели;
max_depth: максимальная глубина используемых в модели деревьев;
min_samples_split: минимальное количество наблюдений во внутрен-
нем узле для возможности его расщепления;
max_features: максимальное количество оцениваемых переменных для
нахождения наилучшего разделения узла.
Градиентный бустинг:
learning_rate: скорость обучения;
n_estimators: количество деревьев в модели;
max_depth: максимальная глубина используемых в модели деревьев;
subsample: доля наблюдений, используемых при обучении.
Экстремальный градиентный бустинг:
learning_rate: скорость обучения;
max_depth: максимальная глубина используемых в модели деревьев;
n_estimators: количество деревьев в модели;
436 Прогнозирование временных рядов с конструированием признаков
reg_alpha и reg_lambda: параметры, отвечающие за регуляризацию L1
и L2;
min_child_weight: минимальная сумма весов в дочернем узле.
Использование поиска по сетке для подбора
гиперпараметров
Поиск по сетке (grid search) является одним из основных методов подбора
гиперпараметров в машинном обучении. Он обеспечивает проверку всех
возможных комбинаций значений параметров и выбор оптимального ва-
рианта. Особенно полезным этот способ можно считать при относительно
небольшом количестве перебираемых значений, поскольку он гарантирует
отсутствие пропусков при проверке.
Первым делом вы определяете диапазоны значений подбираемых гипер-
параметров, после чего алгоритм перебирает все возможные комбинации,
строит модели и оценивает их качество, обычно используя при этом кросс-
валидацию для получения надежных результатов.
Метод поиска по сетке славится своей простотой и воспроизводимостью
результатов, но при наличии большого количества гиперпараметров или при
работе с большими наборами данных его использование может оказаться
неэффективным. В этих случаях можно воспользоваться другими методами
подбора параметров, такими как метод случайного поиска или байесовская
оптимизация.
Давайте воспользуемся поиском по сетке для подбора значений гипер-
параметров в нашем примере с применением метода случайного леса для
прогнозирования временного ряда. При этом мы откажемся от применения
класса RFE и будем использовать все имеющиеся переменные в модели.
Сначала реализуем модель случайного леса без подбора параметров, а за-
тем с подбором, и сравним MSE:
from sklearn.ensemble import RandomForestRegressor
model = RandomForestRegressor(n_estimators=100, random_state=1)
fit = model.fit(X_train, y_train)
y_pred = fit.predict(X_test)
mse_rf = mean_squared_error(y_test, y_pred)
print(f'MSE для случайного леса без подбора по сетке: {mse_rf}')
Вывод:
MSE для случайного леса без подбора по сетке: 73842.3334489526
А теперь выполним подбор параметров и посмотрим, как изменится MSE:
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import GridSearchCV
Подбор гиперпараметров для методов машинного обучения 437
# Определяем сетку гиперпараметров для подбора
param_grid_rf = {
'n_estimators': [50, 100, 200],
'max_depth': [5, 10, 20],
'min_samples_split': [2, 5, 10]
}
# Строим модель
model = RandomForestRegressor(random_state=1)
# Инициализируем поиск по сетке с кросс-валидацией (cv=3)
grid_search_rf = GridSearchCV(model, param_grid_rf, cv=3, scoring='neg_mean _squared_error')
# Обучаем модели на обучающей выборке
grid_search_rf.fit(X_train, y_train)
# Выводим оптимальные параметры
print(f"Оптимальные значения параметров: {grid_search_rf.best_params_}")
# Оцениваем качество модели с оптимальным набором параметров
best_rf = grid_search_rf.best_estimator_
y_pred_rf_best = best_rf.predict(X_test)
mse_rf_best = mean_squared_error(y_test, y_pred_rf_best)
print(f'MSE для случайного леса с подбором по сетке: {mse_rf_best}')
Вывод:
Оптимальные значения параметров: {'max_depth': 10, 'min_samples_split': 2, 'n_estimators':
200}
MSE для случайного леса с подбором по сетке: 70663.40532711032
Как видите, нам удалось снизить MSE с 73 842 до 70 663 за счет подбора
оптимальных значений параметров.
При создании экземпляра класса GridSearchCV ему на вход подаются, соб-
ственно, модель, созданная сетка с параметрами, параметр cv, отвечающий
за количество блоков в кросс-валидации, и используемая метрика для мак-
симизации. Поскольку MSE нам нужно минимизировать, мы берем обратную
ей метрику neg_mean_squared_error .
Давайте аналогичным образом подберем оптимальные значения гипер-
параметров для метода градиентного бустинга. Это позволит нам закрепить
полученные знания.
Сначала посмотрим MSE метода без оптимизации:
from sklearn.ensemble import GradientBoostingRegressor
model = GradientBoostingRegressor(n_estimators=100, random_state=1)
fit = model.fit(X_train, y_train)
y_pred = fit.predict(X_test)
mse_gb = mean_squared_error(y_test, y_pred)
print(f'MSE для градиентного бустинга без подбора по сетке: {mse_gb}')
438 Прогнозирование временных рядов с конструированием признаков
Вывод:
MSE для градиентного бустинга без подбора по сетке: 59400.24477688363
А теперь применим метод подбора по сетке:
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.model_selection import GridSearchCV
# Определяем сетку гиперпараметров для подбора
param_grid_gb = {
'n_estimators': [50, 100, 200],
'max_depth': [3, 6, 9],
'learning_rate': [0.01, 0.1, 0.2]
}
# Строим модель
model = GradientBoostingRegressor(random_state=1)
# Инициализируем поиск по сетке с кросс-валидацией (cv=3)
grid_search_gb = GridSearchCV(model, param_grid_gb, cv=3, scoring='neg_mean _squared_error')
# Обучаем модели на обучающей выборке
grid_search_gb.fit(X_train, y_train)
# Выводим оптимальные параметры
print(f"Оптимальные значения параметров: {grid_search_gb.best_params_}")
# Оцениваем качество модели с оптимальным набором параметров
best_gb = grid_search_gb.best_estimator_
y_pred_gb_best = best_gb.predict(X_test)
mse_gb_best = mean_squared_error(y_test, y_pred_gb_best)
print(f'MSE для градиентного бустинга с подбором по сетке: {mse_gb_best}')
Вывод:
Оптимальные значения параметров: {'learning_rate': 0.1, 'max_depth': 3, 'n_estimators': 50}
MSE для градиентного бустинга с подбором по сетке: 64829.3557758754
На этот раз нам не удалось улучшить результат. Напомним, что в классе
GridSearchCV оценка MSE выполняется на обучающем наборе с применением
кросс-валидации.
Использование случайного поиска
для подбора гиперпараметров
В отличие от метода подбора по сетке, метод случайного поиска (random
search) оценивает не все комбинации параметров, а заранее заданное ко-
личество, выбранное из всего многообразия вариантов случайным образом.
Этот подход обладает весомым преимуществом в сравнении с поиском по
Подбор гиперпараметров для методов машинного обучения 439
сетке, заключающимся в эффективности. Метод случайного поиска может
работать с гораздо большей скоростью, особенно при наличии объемной
сетки гиперпараметров. Это позволяет быстрее выполнить оптимизацию
модели без необходимости перебирать все возможные комбинации.
Давайте опробуем этот метод на примере нашей модели экстремального
градиентного бустинга. Сначала запустим модель без подбора параметров:
import xgboost as xgb
model = xgb.XGBRegressor(n_estimators=100, random_state=1)
fit = model.fit(X_train, y_train)
y_pred = fit.predict(X_test)
mse_xgb = mean_squared_error(y_test, y_pred)
print(f'MSE для XGBoost без подбора по сетке: {mse_xgb}')
Вывод:
MSE для XGBoost без подбора по сетке: 60959.15937144712
А теперь запустим случайный поиск по параметрам при помощи класса
RandomizedSearchCV:
import xgboost as xgb
from sklearn.model_selection import RandomizedSearchCV
# Определяем сетку гиперпараметров для подбора
param_dist_xgb = {
'n_estimators': [50, 100, 200],
'max_depth': [3, 6, 9],
'learning_rate': [0.01, 0.1, 0.2],
'subsample': [0.6, 0.8, 1.0]
}
# Строим модель
model = xgb.XGBRegressor(random_state=1)
# Инициализируем случайный поиск с кросс-валидацией (cv=3)
random_search_xgb = RandomizedSearchCV(model, param_dist_xgb, n_iter=20, cv=3,
scoring='neg_mean _squared_error', random_state=1)
# Обучаем модели на обучающей выборке
random_search_xgb.fit(X_train, y_train)
# Выводим оптимальные параметры
print(f"Оптимальные значения параметров: {random_search_xgb.best_params_}")
# Оцениваем качество модели с оптимальным набором параметров
best_xgb = random_search_xgb.best_estimator_
y_pred_xgb_best = best_xgb.predict(X_test)
mse_xgb_best = mean_squared_error(y_test, y_pred_xgb_best)
print(f'MSE для XGBoost со случайным подбором: {mse_xgb_best}')
440 Прогнозирование временных рядов с конструированием признаков
Вывод:
Оптимальные значения параметров: {'subsample': 0.6, 'n_estimators': 100, 'max_depth': 6,
'learning_rate': 0.1}
MSE для XGBoost со случайным подбором: 53254.03783865766
Здесь нам удалось снизить MSE на тестовой выборке с 60 959 до 53 254,
перебрав 20 (n_iter=20) случайных комбинаций значений параметров.
Итоги проекта
В этом проекте мы научились прогнозировать временные ряды с использо-
ванием множества разных техник и методов. Условно процесс прогнозиро-
вания временных рядов можно разбить на следующие стадии.
1. Погружение в данные. На этой стадии вы должны хорошо разобраться
в имеющемся у вас наборе данных, проанализировать возможные за-
висимости и временную составляющую.
2. Конструирование признаков. Далее вам следует подумать о том, ка-
кими признаками можно обогатить ваш набор данных. К особым при-
знакам, связанным с временными рядами, относятся следующие:
• признаки на основе временного лага: это признаки со смещени-
ем в предыдущие периоды, помогающие прогнозировать будущие
значения;
• признаки на основе скользящего окна: с помощью таких призна-
ков можно нивелировать краткосрочные колебания в данных и об-
наружить долгосрочные тренды;
• признаки детрендирования: позволяют избавиться от трендов
и сделать временной ряд более стационарным и легким для пред-
сказаний;
• признаки сезонности: признаки, позволяющие обнаружить повто-
ряющиеся шаблоны в данных.
3. Применение методов из семейства ARIMA и алгоритмов машин-
ного обучения для прогнозирования временных рядов. В этом
проекте мы воспользовались сразу несколькими методами прогно-
зирования, среди которых авторегрессионная модель (AR), модель
скользящего среднего (MA), авторегрессионная модель скользящего
среднего (ARMA), интегрированная авторегрессионная модель скольз-
ящего среднего (ARIMA), интегрированная авторегрессионная модель
скользящего среднего с учетом сезонности (SARIMA), модель случайно-
го леса, градиентный бустинг и его экстремальный сосед XGBoost.
4. Подбор гиперпараметров моделей. В этом проекте мы воспользова-
лись двумя наиболее распространенными техниками подбора гиперпа-
раметров, а именно методом подбора по сетке и методом случайного
поиска.
Особенности развертывания моделей прогнозирования временных рядов 441
5. Оценка качества модели. Для оценки качества моделей мы исполь-
зовали показатель MSE.
Полученные результаты (MSE) применения разных моделей для прогно-
зирования временного ряда в разных вариантах:
авторегрессионная модель (AR):
• 155 219, 156 886;
модель скользящего среднего (MA):
• 156 397;
авторегрессионная модель скользящего среднего (ARMA):
• 121 122;
интегрированная авторегрессионная модель скользящего средне-
го (ARIMA):
• 110 398;
интегрированная авторегрессионная модель скользящего средне-
го с учетом сезонности (SARIMA):
• 107 193;
модель случайного леса:
• 69 844, 73 842, 70 663;
градиентный бустинг:
• 57 459, 59 400, 64 829;
экстремальный градиентный бустинг:
• 49 531, 60 959, 53 254.
Особенности развертывания моделей
прогнозирования временных рядов
Заключительным шагом разработки проекта прогнозирования временных
рядов является его развертывание в рабочей среде. В процессе развертыва-
ния возможны следующие процессы и задачи.
1. Пакетное прогнозирование. Этот вид прогнозирования применим
для подавляющего большинства приложений. Обученная модель мо-
жет использоваться для предсказания будущих значений на следующие
несколько дней, недель или месяцев. Обычно такая технология ис-
пользуется в сфере продаж, при организации снабжения, а также при
прогнозировании финансовых рынков. Вы можете настроить задачи
прогнозирования на ежедневный, еженедельный или ежемесячный
запуск в зависимости от требований и автоматически обновлять про-
гнозы при поступлении новых данных.
2. Прогнозирование в реальном времени. Иногда возникает необходи-
мость реализации системы прогнозов в реальном времени. В основном
это может касаться обработки высокочастотных данных, касающих-
442 Прогнозирование временных рядов с конструированием признаков
ся, например, цен на акции или ответов от датчиков. В таких случаях
обученная модель может быть развернута в среде предсказаний в ре-
альном времени, и новые данные могут поступать в модель прямо
в процессе работы.
3. Поддержка модели. Модели прогнозирования временных рядов тре-
буют регулярного обновления при поступлении новых данных. Перио-
дическое дообучение моделей позволит поддерживать их в актуальном
состоянии в плане выявления шаблонов, трендов и сезонных колеба-
ний. С этой целью может быть поднят отдельный конвейер, который
будет отвечать за периодическую актуализацию модели.
4. Мониторинг и оценка. После развертывания проекта важно на по-
стоянной основе отслеживать качество модели, что позволит обес-
печить поддержку точности ее предсказаний. Если качество модели
со временем падает, возможно, пришло время переобучить или до-
обучить ее.
Практические упражнения
Давайте выполним несколько практических упражнений на создание при-
знаков для временных рядов.
Упражнение 1. Извлечение признаков на основе временных
компонентов
Есть набор данных по продажам с двумя столбцами:
import pandas as pd
# Простой набор данных с датами
data = {'Date': ['2022-01-15', '2022-02-10', '2022-03 -20', '2022-04-15', '2022-05-25'],
'Sales': [200, 220, 250, 210, 230]}
df = pd.DataFrame(data)
Ваша задача состоит в том, чтобы привести столбец с датами к типу date-
time и извлечь из него компоненты, соответствующие году, месяцу, номеру
квартала и дню недели.
Решение
# Приводим столбец Date к формату datetime и извлекаем компоненты дат
df['Date'] = pd.to_datetime(df['Date'])
df['Year'] = df['Date'].dt.year
df['Month'] = df['Date'].dt.month
df['DayOfWeek'] = df['Date'].dt.dayofweek
df['Quarter'] = df['Date'].dt.quarter
print("Набор данных с дополнительными признаками:")
print(df)
Практические упражнения 443
Вывод:
Набор данных с дополнительными признаками:
Date Sales Year Month DayOfWeek Quarter
0 2022-01-15 200 2022
1
5
1
1 2022-02-10 220 2022
2
3
1
2 2022-03 -20 250 2022
3
6
1
3 2022-04-15 210 2022
4
4
2
4 2022-05-25 230 2022
5
2
2
Упражнение 2. Создание признаков на основе временного лага
Используя тот же набор данных, что и в предыдущем упражнении, создайте
признаки, отражающие сумму продажи за вчерашний и позавчерашний дни.
Решение
# Создаем признаки на основе временного лага
df['Sales_Lag1'] = df['Sales'].shift(1)
df['Sales_Lag2'] = df['Sales'].shift(2)
print("Набор данных с дополнительными признаками на основе временного лага:")
print(df)
Вывод:
Набор данных с дополнительными признаками на основе временного лага:
Date Sales Year Month DayOfWeek Quarter Sales_Lag1 Sales_Lag2
0 2022-01-15 200 2022
1
5
1
NaN
NaN
1 2022-02-10 220 2022
2
3
1
200.0
NaN
2 2022-03 -20 250 2022
3
6
1
220.0
200.0
3 2022-04-15 210 2022
4
4
2
250.0
220.0
4 2022-05-25 230 2022
5
2
2
210.0
250.0
Упражнение 3. Создание признаков на основе скользящих
показателей
Используя тот же набор данных, что и в предыдущем упражнении, создайте
признаки, отражающие скользящее среднее и скользящее стандартное от-
клонение за трехдневный период. При этом минимальное количество наблю-
дений в окне, достаточное для расчета значений, должно быть равно двум.
Решение
# Создаем признаки на основе скользящих показателей
df['RollingMean_3'] = df['Sales'].rolling(window=3, min_periods=2).mean()
df['RollingStd_3'] = df['Sales'].rolling(window=3, min_periods=2).std()
print("Набор данных с дополнительными признаками на основе скользящих показателей:")
print(df)
Вывод:
Набор данных с дополнительными признаками на основе скользящих показателей:
Date Sales Year Month ... Sales_Lag1 Sales_Lag2 RollingMean_3 RollingStd_3
0 2022-01-15 200 2022
1 ...
NaN
NaN
NaN
NaN
444 Прогнозирование временных рядов с конструированием признаков
1 2022-02-10 220 2022
2 ...
200.0
NaN
210.000000
14.142136
2 2022-03 -20 250 2022
3 ...
220.0
200.0
223.333333
25.166115
3 2022-04-15 210 2022
4 ...
250.0
220.0
226.666667
20.816660
4 2022-05-25 230 2022
5 ...
210.0
250.0
230.000000
20.000000
[5 rows x 10 columns]
Упражнение 4. Циклическое кодирование признаков
Используя тот же набор данных, примените синусное и косинусное коди-
рование к столбцу DayOfWeek. Это поможет модели отслеживать недельную
цикличность наших данных.
Решение
import numpy as np
# Создаем признаки на основе циклического кодирования
df['DayOfWeek_sin'] = np.sin(2 * np.pi * df['DayOfWeek'] / 7)
df['DayOfWeek_cos'] = np.cos(2 * np.pi * df['DayOfWeek'] / 7)
print("Набор данных с дополнительными признаками на основе циклического кодирования:")
print(df[['DayOfWeek', 'DayOfWeek_sin', 'DayOfWeek_cos']])
Вывод:
Набор данных с дополнительными признаками на основе циклического кодирования:
DayOfWeek DayOfWeek_sin DayOfWeek_cos
0
5
- 0 .974928
-0 .222521
1
3
0.433884
-0 .900969
2
6
- 0 .781831
0.623490
3
4
- 0 .433884
-0 .900969
4
2
0.974928
-0 .222521
Возможные проблемы
В этом разделе мы посмотрим, какие угрозы могут подстерегать вас при соз-
дании дополнительных признаков во время работы с временными рядами.
Утечка информации в результате неправильно
созданных признаков
Признаки на основе временного лага представляют собой очень мощный
инструмент анализа временных рядов. Но если такие признаки создать не-
правильно, может возникнуть утечка информации, заключающаяся в ис-
пользовании моделью данных будущих периодов при обучении.
Возможные проблемы 445
Что может пойти не так:
модели, обученные с использованием данных будущих периодов, мо-
гут хорошо показывать себя на тестовой выборке, но утрачивать ка-
чество в реальных сценариях, где будущие значения недоступны;
при утечке информации модель может некорректно интерпретировать
исторические шаблоны, что может негативно сказываться на ее спо-
собности к обобщению.
Решение:
убедитесь в том, что при создании признаков на основе временного
лага вы используете только значения предыдущих периодов. Для под-
держки хронологического порядка вы можете воспользоваться кросс-
валидацией с применением скользящих или расширяющихся окон.
Неправильно выбранный размер окна при создании
скользящих признаков
При создании признаков на основе скользящих показателей критически важ-
но правильно выбирать размер окна скольжения.
Что может пойти не так:
слишком маленький размер окна может приводить к отслеживанию
только мелкодисперсного шума;
слишком большой размер окна может стать причиной чрезмерно-
го сглаживания данных и трудностей отслеживания краткосрочных
трендов.
Решение:
поэкспериментируйте с размером окна и выберите оптимальное зна-
чение. При выборе значения обращайте внимание на сезонные шабло-
ны в данных, например недельные или месячные.
Пропуски, возникающие в результате создания
новых признаков
При создании признаков на основе временного лага или скользящих пока-
зателей в наборе данных могут появляться пропущенные значения. Игнори-
рование возникших пропусков может приводить к неполноценности набора
или негативно сказываться на качестве обучения модели.
Что может пойти не так:
пропущенные значения могут скомпрометировать процесс обучения
модели или привести к сбою алгоритмов машинного обучения;
446 Прогнозирование временных рядов с конструированием признаков
удаление или неправильное заполнение пропусков может привести
к искажению исходных данных.
Решение:
при необходимости используйте методы подстановки с прямым или
обратным проходом. Удалить наблюдения с пропусками вы можете
только в случае, если они не представляют интереса для модели.
Неправильная интерпретация циклических
переменных
Циклическое кодирование является очень эффективным способом преобра-
зования переменных, для которых характерны повторяющиеся шаблоны. Но
при неправильном применении этот тип преобразования может приводить
к образованию шумов в данных.
Что может пойти не так:
применение циклического кодирования к нециклическим данным мо-
жет привести к тому, что модель начнет улавливать повторяющиеся
зависимости там, где их нет;
неправильная интерпретация циклических переменных может при-
водить к ошибочным выводам, особенно когда речь идет об анализе
сезонности.
Решение:
применяйте циклическое кодирование только к переменным, облада-
ющим явными повторяющимися шаблонами, таким как день недели
или время в часах.
Разреженность данных при создании скользящих
признаков
При работе с высокочастотными данными, в которых новые наблюдения
появляются каждую минуту или час, создание скользящих признаков с боль-
шим размером окна может приводить к появлению разреженных данных,
когда многие наблюдения не содержат реальных значений. Это может ослож-
нять процесс создания новых признаков и сводить на нет ценность создания
скользящих признаков.
Что может пойти не так:
разреженность данных может мешать модели обнаруживать значимые
шаблоны и негативно сказываться на накладных расходах, связанных
с вычислениями;
Возможные проблемы 447
разреженные скользящие признаки могут не справляться с обнару-
жением трендов в реальном сремени, особенно при работе с быстро
меняющимися наборами данных.
Решение:
при работе с высокочастотными данными используйте окна скольже-
ния меньшего размера для поддержания плотности данных и сохране-
ния значимости признаков. Воспользуйтесь при создании скользящих
признаков знаниями о предметной области. К примеру, вы можете
добавить в почасовой набор данных признак с 24-часовым размером
окна для отслеживания дневных трендов.
Неправильный учет часовых поясов в данных
При работе с географическими данными важно уделять особое внимание
обработке часовых поясов. Неправильный учет временных зон может при-
водить к ошибкам при отслеживании шаблонов, связанных со временем.
Что может пойти не так:
временные расхождения могут сказываться на хронологии наблюде-
ний и тем самым влиять на интерпретацию дневных, недельных и се-
зонных шаблонов;
неправильный учет часовых поясов может свести на нет все усилия по
анализу данных в реальном времени, где критическое значение имеет
время наступления события.
Решение:
всегда применяйте стандартизацию временных признаков при не-
обходимости учета часовых поясов. В библиотеке Pandas вы можете
воспользоваться для этого удобными функциями tz_convert() и tz_lo-
calize().
Глава 8
Корректировка
аномалий в данных
при помощи Pandas
В этой главе мы рассмотрим несколько примеров восстановления конси-
стентности данных, которые могут вам пригодиться в процессе обработки
больших наборов. Аномалии в данных могут появляться по самым разным
причинам и могут иметь разную форму. Наиболее распространенные формы
аномалий в данных:
некорректный формат даты: когда похожая информация представ-
лена в данных по-разному. Например, одни даты могут быть записаны
в формате MM/DD/YYYY, а другие – в формате YYYY-MM -DD;
дублирующиеся записи: когда одинаковые или почти одинаковые
сущности представлены в данных множество раз. Обычно это приво-
дит к искажению результатов анализа и увеличению объема данных
без необходимости;
значения, выходящие за допустимые границы: когда значения
в переменной не попадают в диапазон, определенный логикой или
бизнес-требованиями. Часто подобные коллизии объясняются ошиб-
ками измерений или ввода;
опечатки в категориальных переменных: когда в данных присут-
ствуют неправильно написанные названия категорий. Это может при-
водить к неправильным результатам классификации и ошибкам при
группировке.
В этом разделе мы воспользуемся библиотекой Pandas для обнаружения
и устранения аномалий в табличных данных.
Обработка некорректных форматов данных 449
8.1. Обработка некорректных форматов
данных
Нарушение консистентности данных чаще всего возникает по причине объ-
единения данных в одном хранилище из разных источников. Эти ошибки
могут быть связаны как с использованием разных форматов даты, так и с до-
бавлением каких-то специальных символов вроде лишних точек или сим-
волов валюты, нарушающих целостность информации. Подобные ошибки
могут негативно влиять на процесс анализа и обработки данных и приводить
к некорректным заключениям, полученным на основе моделей.
Кроме того, ошибки, связанные с неконсистентностью хранения данных,
могут приводить к вычислительным неточностям, вносить погрешность
в модели и затруднять процесс визуализации данных. К примеру, наличие
разных форматов дат в данных может приводить к неправильной сортиров-
ке наблюдений и их отнесению к некорректным годам и месяцам, а ошибки
числового представления могут выливаться в неправильную интерпретацию
финансовых результатов.
Решение проблем с отсутствием консистентности хранения данных по-
могает добиться следующего:
полной совместимости и интеграции информации в имеющихся дан-
ных;
облегчения процесса обработки и анализа данных за счет исключе-
ния необходимости постоянно проводить проверку типов и форматов
значений;
повышения качества моделей машинного обучения за счет достижения
единообразия входных данных;
повышения согласованности данных с целью облегчения интеграции
с другими инструментами и платформами.
Процедура корректировки подобных проблем, часто называемая стандар-
тизацией, или нормализацией, данных, включает в себя применение едино-
образных правил форматирования ко всему набору данных. Эти правила мо-
гут состоять в приведении всех дат к одному формату (например, ISO 8601),
удалении символов валюты и разделителей разрядов для числовых значений
или приведении текстовых данных к определенному регистру.
Пример приведения дат к единому формату
Предположим, в нашем наборе данных информация о датах представлена
в самых разных форматах, таких как MM/DD/YYYY и YYYY-MM -DD. Что мож-
но сделать?
import pandas as pd
import matplotlib.pyplot as plt
450 Корректировка аномалий в данных при помощи Pandas
# Простой набор данных с неконсистентными форматами дат
data={
'OrderDate': ['2022-01-15', '01/20/2022', 'February 5, 2022', '2022/02/10', '03-15-
2022', '2022.04.01'],
'Amount': [100, 150, 200, 250, 300, 350]
}
df = pd.DataFrame(data)
print("Исходный датафрейм:")
print(df)
print("\nТипы данных:")
print(df.dtypes)
# Приводим все даты к консистентному формату
df['OrderDate'] = pd.to_datetime(df['OrderDate'], errors='coerce', format='mixed')
print("\nДатафрейм после преобразования:")
print(df)
print("\nТипы данных после преобразования:")
print(df.dtypes)
# Проверяем наличие ошибок преобразования (значения NaT)
nat_count = df['OrderDate'].isna().sum()
print(f"\nКоличество ошибок преобразования (значения NaT): {nat_count}")
# Сортируем датафрейм по дате
df_sorted = df.sort_values('OrderDate')
print("\nОтсортированный датафрейм:")
print(df_sorted)
# Создаем столбец с временными интервалами
df_sorted['TimeDelta'] = df_sorted['OrderDate'].diff()
print("\nДатафрейм с временными интервалами:")
print(df_sorted)
# Визуализируем данные
plt.figure(figsize=(10, 6))
plt.scatter(df_sorted['OrderDate'], df_sorted['Amount'], s=60)
plt.title('Заказы с хронологией')
plt.xlabel('Дата заказа')
plt.ylabel('Количество')
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
# Пример арифметических вычислений с датами
latest_date = df['OrderDate'].max()
one_month_ago = latest_date - pd.Timedelta(days=30)
recent_orders = df[df['OrderDate'] > one_month_ago]
print("\nЗаказы за последние 30 дней:")
print(recent_orders)
Обработка некорректных форматов данных 451
Вывод:
Исходный датафрейм:
OrderDate Amount
0
2022-01-15
100
1
01/20/2022
150
2 February 5, 2022
200
3
2022/02/10
250
4
03-15-2022
300
5
2022.04.01
350
Типы данных:
OrderDate
object
Amount
int64
dtype: object
Датафрейм после преобразования:
OrderDate Amount
0 2022-01-15
100
1 2022-01-20
150
2 2022-02-05
200
3 2022-02-10
250
4 2022-03 -15
300
5 2022-04-01
350
Типы данных после преобразования:
OrderDate datetime64[ns]
Amount
int64
dtype: object
Количество ошибок преобразования (значения NaT): 0
Отсортированный датафрейм:
OrderDate Amount
0 2022-01-15
100
1 2022-01-20
150
2 2022-02-05
200
3 2022-02-10
250
4 2022-03 -15
300
5 2022-04-01
350
Датафрейм с временными интервалами:
OrderDate Amount TimeDelta
0 2022-01-15
100
NaT
1 2022-01-20
150 5 days
2 2022-02-05
200 16 days
3 2022-02-10
250 5 days
4 2022-03 -15
300 33 days
5 2022-04-01
350 17 days
Заказы за последние 30 дней:
OrderDate Amount
4 2022-03 -15
300
5 2022-04-01
350
452 Корректировка аномалий в данных при помощи Pandas
Рис. 8.1 Заказы с хронологией
Здесь мы создали датафрейм с разнородными данными в столбце Order-
Date, после чего воспользовались функцией pd.to_datetime() с параметрами
errors='coerce' и format='mixed', чтобы привести эти разрозненные значе-
ния к единому формату даты. Параметр errors отвечает за действие при не-
возможности выполнить преобразование, а его значение 'coerce' говорит
о необходимости в этом случае заменить значение на NaT (Not a Time, не
время). Параметр format='mixed' указывает на то, что в столбце могут при-
сутствовать даты в самых разных форматах и подходить к каждому значе-
нию нужно индивидуально. Будьте осторожны при использовании этого
значения параметра, поскольку результаты могут вас удивить. Лучше при
его использовании в целях безопасности также включать в вызов функции
параметр dayfirst.
Выполнив преобразование столбца с датами, мы отсортировали дата-
фрейм по этому столбцу и вывели информацию о заказах на графике. В за-
ключение мы продемонстрировали пользу от преобразования текстовых
дат в формат datetime, показав возможность применения арифметических
действий к датам. В частности, мы вычли 30 дней из даты последнего заказа
и вывели только те заказы, которые были созданы после получившейся даты.
С помощью этого примера мы подчеркнули важность приведения дат к еди-
ному формату с целью облегчения дальнейшего анализа, моделирования
и визуализации данных.
Обработка некорректных форматов данных 453
Пример удаления символов валюты
Проблемы форматирования могут преследовать вас не только при работе
с датами и временем. В числовых полях нередко также могут присутствовать
скрытые угрозы в виде дополнительных символов или знаков валют, которые
не позволят вам в дальнейшем анализировать данные. Подобные проблемы
могут возникать при загрузке данных из разных источников или обработке
ручного ввода. В результате одни значения в столбце могут содержать сим-
волы валюты, другие могут быть разделены на разряды запятыми и т. д.
Все это может приводить к невозможности Pandas преобразовать данные
в числовой формат, а это чревато серьезными проблемами при дальнейшем
анализе данных.
Для борьбы с паразитными символами и неверными типами данных в Pan-
das предусмотрены специальные инструменты манипулирования строками
и преобразования типов. Рассмотрим пример:
import pandas as pd
import matplotlib.pyplot as plt
# Простой набор данных с символами валют, запятыми в качестве разделителей разрядов
и смешанными форматами
data = {'Sales': ['$1,200', '950', '$2,500.50', '1,100', '€3,000', '¥5000']}
df = pd.DataFrame(data)
print("Исходный датафрейм:")
print(df)
print("\nТип данных столбца Sales:", df['Sales'].dtype)
# Функция для преобразования различных форматов валютных сумм в числа с плавающей запятой
def currency_to_float(value):
# Удаляем символы валют и запятые
value = value.replace('$', '').replace('€', '').replace('¥', '').replace(',', '')
return float(value)
# Применяем функцию преобразования
df['Sales'] = df['Sales'].apply(currency_to_float)
print("\nОчищенный датафрейм:")
print(df)
print("\nТип данных столбца Sales после очистки:", df['Sales'].dtype)
# Базовая статистика
print("\nБазовая статистика столбца Sales:")
print(df['Sales'].describe())
# Визуализация
plt.figure(figsize=(10, 6))
df['Sales'].plot(kind='bar')
plt.title('Значения в столбце Sales')
plt.xlabel('Индекс')
plt.ylabel('Сумма')
454 Корректировка аномалий в данных при помощи Pandas
plt.xticks(rotation=0)
plt.tight_layout()
plt.show()
Вывод:
Исходный датафрейм:
Sales
0
$1,200
1
950
2 $2,500.50
3
1,100
4
€3,000
5
¥5000
Тип данных столбца Sales: object
Очищенный датафрейм:
Sales
0 1200.0
1 950.0
2 2500.5
3 1100.0
4 3000.0
5 5000.0
Рис. 8.2 Значения в столбце Sales
Поиск и удаление дубликатов 455
Тип данных столбца Sales после очистки: float64
Базовая статистика столбца Sales:
count
6.000000
mean
2291.750000
std
1567.947185
min
950.000000
25%
1125.000000
50%
1850.250000
75%
2875.125000
max
5000.000000
Name: Sales, dtype: float64
Здесь мы создали простой набор данных с разнородными суммами в раз-
ных валютах. Затем написали отдельную функцию для удаления из значения
символов валют и запятых и применили ее к столбцу Sales.
Такой подход может эффективно использоваться для выполнения преоб-
разований столбцов любой степени сложности с выносом логики преобра-
зования в специальные функции.
8.2. Поиск и удаление дубликатов
Дублирующие друг друга строки могут появиться в наборе данных в резуль-
тате ошибок при ручном вводе, множественной загрузки одних и тех же
операций из источника или объединения данных из разных источников. Хотя
бывают случаи, когда дублирующиеся записи в наборе данных создаются
намеренно, в большинстве ситуаций их наличие говорит о том, что что-то
в процессе сбора информации пошло не так.
Поиск и удаление дубликатов в наборе данных является важным шагом в про-
цессе предварительной обработки данных, и на то есть множество причин:
сохранение целостности данных: удаление повторов в наборе дан-
ных ведет к обеспечению его целостности. В результате этой операции
каждая сущность или наблюдение будут представлены в наборе лишь
один раз, что позволит избежать проблем с интерпретированием дан-
ных. К примеру, при наличии дубликатов в клиентской базе мы легко
можем переоценить количество уникальных покупателей или по не-
скольку раз рассылать рекламные материалы одним и тем же людям;
точность аналитики: присутствие в наборе данных повторов может
негативно сказаться на статистическом анализе и результатах работы
методов машинного обучения. Например, при анализе тональности от-
зывов о товарах или услугах наличие дубликатов может привести к ис-
кусственному завышению или занижению количества положительных
или отрицательных отзывов;
эффективность хранения: помимо аналитических соображений, ис-
ключение дублирующихся записей может положительно сказаться на
456 Корректировка аномалий в данных при помощи Pandas
управлении данными и их хранении, что особенно важно при работе
с большими наборами. Кроме того, это может повысить эффективность
выполнения запросов к базе данных и ускорить загрузку информации;
консистентность: в дублирующихся наблюдениях зачастую содер-
жатся небольшие различия – например, они могут отличаться датой
или временем. Исключение таких различий может способствовать еди-
нообразному представлению каждой уникальной сущности в наборе
данных. Это бывает важно для повышения точности анализа трендов,
прогнозирования и процесса принятия решений;
повышение качества данных: процедура удаления дубликатов не-
редко является катализатором общего повышения качества данных.
Часто это позволяет обнаружить другие проблемы, связанные с фор-
матированием и вводом данных, или систематические ошибки при их
обработке;
улучшение интеграции данных: при объединении данных из разных
источников удаление дубликатов может играть критически важную
роль, поскольку позволяет поддерживать консистентность и надеж-
ность набора данных.
В то же время избавляться от повторов в данных необходимо с большой
осторожностью, поскольку иногда дубли добавляются в наборы намеренно.
Таким образом, вы должны хорошо понимать структуру данных, с которыми
работаете, и разбираться в предметной области.
Пример удаления дубликатов в данных
Представим, что в нашем наборе данных с клиентами присутствуют дубли-
рующиеся строки:
import pandas as pd
import matplotlib.pyplot as plt
# Простой набор данных с дублями и разным форматированием данных
data={
'CustomerID': [101, 102, 103, 101, 104, 102],
'Name': ['Alice', 'Bob', 'Charlie', 'Alice', 'David', 'Bob'],
'PurchaseAmount': ['$150', '200', '$300.50', '$150', '250', '200'],
'PurchaseDate': ['2023-01-15', '2023-01-16', '2023-01-17', '2023-01-15', '2023-01-18',
'2023-01-16']
}
df = pd.DataFrame(data)
print("Исходный датафрейм:")
print(df)
print("\nТипы данных:")
print(df.dtypes)
# Функция для преобразования формата валютных сумм в числа с плавающей запятой
def currency_to_float(value):
Поиск и удаление дубликатов 457
return float(str(value).replace('$', ''))
# Применяем функцию к столбцу PurchaseAmount
df['PurchaseAmount'] = df['PurchaseAmount'].apply(currency_to_float)
# Приводим поле PurchaseDate к типу datetime
df['PurchaseDate'] = pd.to_datetime(df['PurchaseDate'])
# Ищем дубликаты
duplicates = df[df.duplicated()]
print("\nДублирующиеся строки:")
print(duplicates)
# Удаляем дубликаты
df_cleaned = df.drop_duplicates()
print("\nДатафрейм после удаления дубликатов:")
print(df_cleaned)
print("\nТипы данных после очистки:")
print(df_cleaned.dtypes)
# Базовая статистика очищенных данных
print("\nБазовая статистика столбца PurchaseAmount:")
print(df_cleaned['PurchaseAmount'].describe())
# Визуализация
plt.figure(figsize=(10, 6))
df_cleaned['PurchaseAmount'].plot(kind='bar')
plt.title('Суммы покупок по клиентам')
plt.xlabel('Индекс покупателя')
plt.ylabel('Сумма покупки ($)')
plt.xticks(rotation=0)
plt.tight_layout()
plt.show()
# Группируем по клиентам и считаем суммы покупок
customer_totals = df_cleaned.groupby('Name')['PurchaseAmount'].sum().sort_
values(ascending=False)
print("\nСуммарные покупки по клиентам:")
print(customer_totals)
# Пример использования очищенных данных
total_sales = df_cleaned['PurchaseAmount'].sum()
average_sale = df_cleaned['PurchaseAmount'].mean()
print(f"\nСумма продаж: ${total_sales:.2f}")
print(f"Средняя продажа: ${average_sale:.2f}")
Вывод:
Исходный датафрейм:
CustomerID
Name PurchaseAmount PurchaseDate
0
101 Alice
$150 2023-01-15
1
102
Bob
200 2023-01-16
458 Корректировка аномалий в данных при помощи Pandas
2
103 Charlie
$300.50 2023-01-17
3
101 Alice
$150 2023-01-15
4
104 David
250 2023-01-18
5
102
Bob
200 2023-01-16
Типы данных:
CustomerID
int64
Name
object
PurchaseAmount
object
PurchaseDate
object
dtype: object
Дублирующиеся строки:
CustomerID Name PurchaseAmount PurchaseDate
3
101 Alice
150.0 2023-01-15
5
102 Bob
200.0 2023-01-16
Датафрейм после удаления дубликатов:
CustomerID
Name PurchaseAmount PurchaseDate
0
101 Alice
150.0 2023-01-15
1
102
Bob
200.0 2023-01-16
2
103 Charlie
300.5 2023-01-17
4
104 David
250.0 2023-01-18
Типы данных после очистки:
CustomerID
int64
Name
object
PurchaseAmount
float64
PurchaseDate
datetime64[ns]
dtype: object
Базовая статистика столбца PurchaseAmount:
count
4.000000
mean
225.125000
std
64.743565
min
150.000000
25%
187.500000
50%
225.000000
75%
262.625000
max
300.500000
Name: PurchaseAmount, dtype: float64
Суммарные покупки по клиентам:
Name
Charlie 300.5
David
250.0
Bob
200.0
Alice
150.0
Name: PurchaseAmount, dtype: float64
Сумма продаж: $900.50
Средняя продажа: $225.12
Исправление неконсистентных категориальных данных 459
Рис. 8 .3 Суммы покупок по клиентам
Здесь мы создали набор данных, содержащий дубли и разное форматиро-
вание данных. Затем мы написали функцию для удаления символа доллара
из значений и применили ее к столбцу с суммами. После этого выполнили
поиск дубликатов при помощи метода duplicated(), после чего воспользова-
лись методом drop_duplicates() для удаления повторов из данных. В заключе-
ние мы сгруппировали данные по клиентам и рассчитали суммы покупок для
них. Также мы продемонстрировали пример применения математических
агрегаций к очищенным данным.
8.3. Исправление неконсистентных
категориальных данных
Несоответствия в написании значений в категориальных переменных пред-
ставляют собой большую проблему при анализе данных и проектировании
моделей машинного обучения. Эти несоответствия могут присутствовать
в разной форме, включая банальные опечатки, особенности и различия в на-
писании или использование разных регистров. К примеру, в столбце с катего-
риями товаров вы можете запросто встретить такие написания, как Electron-
ics, electronics и ELECTRONICS. Все они будут относиться к одной категории, но
восприниматься будут как разные.
460 Корректировка аномалий в данных при помощи Pandas
При обучении моделей подобные различия в написании категорий могут
приводить к неожиданным и ошибочным результатам. Допустим, если при
анализе тональности отзывов категории positive и Positive будут восприни-
маться как разные реакции, это может привести к серьезным нарушениям
в работе модели.
Такие проблемы могут возникать вследствие объединения наблюдений из
разных источников, в каждом из которых приняты свои правила. Давайте
посмотрим, что с этим можно сделать.
Пример стандартизации текста в категориальных данных
Рассмотрим набор данных с разными вариантами написания категорий то-
варов:
import pandas as pd
import matplotlib.pyplot as plt
# Простой набор данных с разными вариантами написания категорий
data={
'Category': ['Electronics', 'electronics', 'ELECTronics', 'Furniture', 'furniture',
'FURNITURE', 'Appliances', 'appliances'],
'Price': [100, 200, 150, 300, 250, 400, 175, 225]
}
df = pd.DataFrame(data)
print("Исходный датафрейм:")
print(df)
# Стандартизация текста с приведением к нижнему регистру
df['Category'] = df['Category'].str.lower()
print("\nДатафрейм после приведения к нижнему регистру:")
print(df)
# Считаем вхождения каждой категории
category_counts = df['Category'].value_counts()
print("\nВхождения категорий:")
print(category_counts)
# Визуализируем распределение категорий
plt.figure(figsize=(10, 6))
category_counts.plot(kind='bar')
plt.title('Распределение по категориям')
plt.xlabel('Категория')
plt.ylabel('Количество')
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
# Считаем средние цены по категориям
avg_price_per_category = df.groupby('Category')['Price'].mean().sort_
values(ascending=False)
Исправление неконсистентных категориальных данных 461
print("\nСредние цены по категориям:")
print(avg_price_per_category)
# Визуализируем средние цены по категориям
plt.figure(figsize=(10, 6))
avg_price_per_category.plot(kind='bar')
plt.title('Средние цены по категориям')
plt.xlabel('Категория')
plt.ylabel('Средняя цена')
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
Вывод:
Исходный датафрейм:
Category Price
0 Electronics 100
1 electronics 200
2 ELECTronics 150
3 Furniture
300
4 furniture
250
5 FURNITURE 400
6 Appliances 175
7 appliances 225
Датафрейм после приведения к нижнему регистру:
Category Price
0 electronics 100
1 electronics 200
2 electronics 150
3 furniture
300
4 furniture
250
5 furniture
400
6 appliances 175
7 appliances 225
Вхождения категорий:
Category
electronics 3
furniture
3
appliances
2
Name: count, dtype: int64
Средние цены по категориям:
Category
furniture
316.666667
appliances
200.000000
electronics 150.000000
Name: Price, dtype: float64
462 Корректировка аномалий в данных при помощи Pandas
Рис. 8.4 Распределение по категориям
Рис. 8 .5 Средние цены по категориям
Обработка значений, выходящих за допустимые границы 463
Здесь мы создали набор данных с различиями в написании значений ка-
тегориальной переменной, после чего устранили эту неконсистентность при
помощи атрибута доступа str и метода lower(). В результате мы смогли сгруп-
пировать данные по обновленной переменной и не получили задвоения
результатов из-за разницы в написании наименований категорий.
8.4. Обработка значений, выходящих
за допустимые границы
Под значениями, выходящими за допустимые границы, мы подразумеваем
те из них, которые не укладываются в диапазоны, продиктованные логи-
кой или бизнес-требованиями. Такие значения могут появляться в данных
по самым разным причинам, включая ошибки ввода и неточности измере-
ний. Но в то же время некоторые из этих значений могут быть корректными
и представлять собой выбросы. Поиск и обработка значений, выходящих за
означенные пределы, очень важны и позволяют добиться следующего:
целостность данных: значения, выходящие за допустимые границы,
могут вносить существенные погрешности в статистические методы
и модели машинного обучения, что может приводить к ошибкам пред-
сказаний. К примеру, в столбце с ростом человека ошибочное значение
300 см может сильно исказить результаты расчета среднего роста лю-
дей и стандартного отклонения, что повлечет за собой неправильные
выводы и результаты работы модели;
корректное представление о предметной области: тщательно сле-
дя за соблюдением допустимых границ, мы можем обеспечить точное
соответствие нашего набора данных описываемой им предметной об-
ласти. Это бывает очень важно в таких областях, как медицина и фи-
нансы, где точность данных напрямую влияет на результаты и процесс
принятия решений;
поиск ошибок: наличие подобных значений часто указывает на систе-
матические ошибки при получении или обработке данных, требующие
проведения дополнительных исследований в этой области;
качество модели: исключение значений, выходящих за допустимые
границы, может позволить повысить качество предсказаний модели за
счет устранения шума в данных;
налаженный процесс принятия решений: наличие выходящих за
границы значений может приводить к принятию неправильных реше-
ний, которые могут повлечь финансовые убытки. К примеру, ошибоч-
ные данные о ценах могут помешать инвестору сделать правильные
вложения;
464 Корректировка аномалий в данных при помощи Pandas
качество данных: обработка значений, выходящих за допустимые
границы, является неотъемлемой частью обеспечения качества данных
и, как следствие, построения отчетов на основе надежных сведений.
Эффективные стратегии отслеживания и исправления значений, выходя-
щих за допустимые границы, включают в себя установку границ на основа-
нии знания о предметной области, использование статистических методов
для поиска выбросов и принятие решения об удалении, замене или пометке
таких значений для будущего анализа. Выбор конкретного метода зависит
от задачи и целей анализа.
Пример удаления значений, выходящих за допустимые
границы
Предположим, у нас в наборе данных есть колонка с возрастом и мы знаем,
что возраст человека не может выходить за пределы диапазона от 0 до 120:
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
# Простой набор данных со значениями, выходящими за допустимые границы
data={
'Name': ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve', 'Frank', 'Grace', 'Henry'],
'Age': [25, 132, 30, -5, 45, 200, 0, 80],
'Salary': [50000, 75000, 60000, 55000, 90000, 80000, 70000, 65000]
}
df = pd.DataFrame(data)
print("Исходный датафрейм:")
print(df)
# Поиск значений, выходящих за допустимые границы
age_out_of_range = df[(df['Age'] < 0) | (df['Age'] > 120)]
print("\nЗначения, выходящие за допустимые границы:")
print(age_out_of_range)
# Удаление значений, выходящих за допустимые границы
df_cleaned = df[(df['Age'] >= 0) & (df['Age'] <= 120)]
print("\nДатафрейм после удаления некорректных значений:")
print(df_cleaned)
# Базовая статистика до и после удаления значений, выходящих за допустимые границы
print("\nСтатистика до удаления значений:")
print(df['Age'].describe())
print("\nСтатистика после удаления значений:")
print(df_cleaned['Age'].describe())
# Визуализация данных до и после удаления значений, выходящих за допустимые границы
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
sns.histplot(df['Age'], kde=True, ax=ax1)
ax1.set_title('Распределение возрастов (до очистки)')
Обработка значений, выходящих за допустимые границы 465
ax1.set_xlabel('Возраст')
ax1.set_ylabel('Количество')
sns.histplot(df_cleaned['Age'], kde=True, ax=ax2)
ax2.set_title('Распределение возрастов (после очистки)')
ax2.set_xlabel('Возраст')
ax2.set_ylabel('Количество')
plt.tight_layout()
plt.show()
# Анализ влияния на другие переменные
print("\nСредняя зарплата (до очистки):", df['Salary'].mean())
print("Средняя зарплата (после очистки):", df_cleaned['Salary'].mean())
# Корреляционный анализ
correlation_before = df[['Age', 'Salary']].corr()
correlation_after = df_cleaned[['Age', 'Salary']].corr()
print("\nКорреляция между возрастом и зарплатой (до очистки):")
print(correlation_before)
print("\nКорреляция между возрастом и зарплатой (после очистки):")
print(correlation_after)
Вывод:
Исходный датафрейм:
Name Age Salary
0 Alice 25 50000
1
Bob 132 75000
2 Charlie 30 60000
3 Diana -5 55000
4
Eve 45 90000
5 Frank 200 80000
6 Grace 0 70000
7 Henry 80 65000
Значения, выходящие за допустимые границы:
Name Age Salary
1 Bob 132 75000
3 Diana -5 55000
5 Frank 200 80000
Датафрейм после удаления некорректных значений:
Name Age Salary
0 Alice 25 50000
2 Charlie 30 60000
4
Eve 45 90000
6 Grace 0 70000
7 Henry 80 65000
Статистика до удаления значений:
count
8.000000
mean
63.375000
std
70.980757
min
- 5 .000000
25%
18.750000
466 Корректировка аномалий в данных при помощи Pandas
50%
37.500000
75%
93.000000
max
200.000000
Name: Age, dtype: float64
Статистика после удаления значений:
count
5.000000
mean
36.000000
std
29.453353
min
0.000000
25%
25.000000
50%
30.000000
75%
45.000000
max
80.000000
Name: Age, dtype: float64
Средняя зарплата (до очистки): 68125.0
Средняя зарплата (после очистки): 67000.0
Корреляция между возрастом и зарплатой (до очистки):
Age Salary
Age
1.000000 0 .510549
Salary 0.510549 1.000000
Корреляция между возрастом и зарплатой (после очистки):
Age Salary
Age
1.000000 0 .137343
Salary 0.137343 1.000000
Рис. 8.6 Распределение возрастов до и после очистки
Обработка пропущенных значений, образовавшихся в результате коррекции 467
Здесь мы создали набор данных с явно ошибочными значениями возрас-
та для некоторых сотрудников. Затем мы оставили в датафрейме только тех
сотрудников, возраст которых укладывается в заданный диапазон, и вывели
всю сопутствующую статистику и графики распределения значений. Но при
использовании этого способа нужно не забыть об удаленных из выборки
сотрудниках и произвести необходимые действия по корректировке их воз-
раста в исходном наборе данных.
8.5. Обработка пропущенных значений,
образовавшихся в результате
коррекции аномалий
При корректировке аномалий в данных нередко появляются пропущенные
значения, которые необходимо должным образом обработать. Это могут
быть как пропуски в переменных, касающихся даты и времени (NaT – Not
a Time), так и пропущенные значения в столбцах других типов.
Вам нужно внимательно следить за образованием подобных пропусков
в данных в результате выполнения шагов по обработке исходного набора.
Часто бывает, что именно действия аналитика приводят к появлению про-
пущенных значений в данных, и именно аналитик должен исправлять то,
что наделал.
Ниже показан пример заполнения пропусков, которые мы намеренно
вставили в сгенерированные данные:
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
# Простой набор данных с пропущенными значениями
data={
'OrderDate': ['2022-01-15', pd.NaT, '2022-02-05', pd.NaT, '2022-03 -10', pd.NaT, '2022-
04-20'],
'ProductID': ['A001', 'B002', 'C003', 'D004', 'E005', 'F006', 'G007'],
'Quantity': [5, 3, pd.NA, 7, 2, 4, 6]
}
df = pd.DataFrame(data)
print("Исходный датафрейм:")
print(df)
print("\nКоличество пропущенных значений:")
print(df.isnull().sum())
# Подставляем пропуски в датах с помощью метода прямого заполнения
df['OrderDate'] = df['OrderDate'].ffill()
468 Корректировка аномалий в данных при помощи Pandas
# Заполняем пропуски в столбце с количеством с помощью медианы
df['Quantity'] = df['Quantity'].fillna(df['Quantity'].median())
print("\nДатафрейм после заполнения пропусков:")
print(df)
print("\nКоличество пропущенных значений после заполнения:")
print(df.isnull().sum())
# Визуализируем распределение значений в столбце OrderDate
plt.figure(figsize=(10, 6))
sns.histplot(pd.to_datetime(df['OrderDate']), kde=True)
plt.title('Распределение значений в столбце OrderDate')
plt.xlabel('Дата заказа')
plt.ylabel('Количество')
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
# Анализируем статистику
print("\nОбщая статистика:")
print(df.describe())
print("\nКорреляция между столбцами Quantity и OrderDate:")
df['OrderDate'] = pd.to_datetime(df['OrderDate'])
correlation = df['Quantity'].corr(df['OrderDate'].astype(int) / 10**9)
print(correlation)
Вывод:
Исходный датафрейм:
OrderDate ProductID Quantity
0 2022-01-15
A001
5
1
NaT
B002
3
2 2022-02-05
C003
<NA>
3
NaT
D004
7
4 2022-03-10
E005
2
5
NaT
F006
4
6 2022-04-20
G007
6
Количество пропущенных значений:
OrderDate 3
ProductID 0
Quantity
1
dtype: int64
Датафрейм после заполнения пропусков:
OrderDate ProductID Quantity
0 2022-01-15
A001
5.0
1 2022-01-15
B002
3.0
2 2022-02-05
C003
4.5
3 2022-02-05
D004
7.0
4 2022-03-10
E005
2.0
5 2022-03-10
F006
4.0
6 2022-04-20
G007
6.0
Практические упражнения 469
Количество пропущенных значений после заполнения:
OrderDate 0
ProductID 0
Quantity
0
dtype: int64
Общая статистика:
Quantity
count 7.000000
mean 4.500000
std 1.707825
min 2.000000
25% 3.500000
50% 4.500000
75% 5.500000
max
7.000000
Корреляция между столбцами Quantity и OrderDate:
0.09320026065299429
Здесь мы сгенерировали набор данных с пропущенными значениями,
заполнили пропуски методом прямой подстановки и с помощью медианы
и вывели всю необходимую статистику.
8.6. Практические упражнения
Теперь по традиции приступим к выполнению заданий, с помощью которых
вы сможете закрепить полученные знания на практике.
Упражнение 1. Стандартизация форматов даты
Есть набор данных, в котором столбец с датами заполнен значениями в раз-
ных форматах:
import pandas as pd
# Простой набор данных с разными форматами даты
data = {'OrderDate': ['2022-01-15', '01/20/2022', 'February 5, 2022', '2022/02/10', '2022-
31-12']}
df = pd.DataFrame(data)
Ваша задача состоит в том, чтобы преобразовать все даты в формат YYYY-
MM-DD и вывести количество дат, которые не удалось преобразовать.
Решение
# Приводим все даты к единому формату
df['OrderDate'] = pd.to_datetime(df['OrderDate'], errors='coerce', format='mixed')
# Ищем даты, не прошедшие преобразование
invalid_dates = df[df['OrderDate'].isna()]
470 Корректировка аномалий в данных при помощи Pandas
print("Датафрейм с преобразованными датами:")
print(df)
print("\nКоличество некорректных дат:")
print(len(invalid_dates))
Вывод:
Датафрейм с преобразованными датами:
OrderDate
0 2022-01-15
1 2022-01-20
2 2022-02-05
3 2022-02-10
4
NaT
Количество некорректных дат:
1
Упражнение 2. Удаление дубликатов
Есть набор данных с дублирующимися строками:
import pandas as pd
# Простой набор данных с дубликатами строк
data = {'CustomerID': [101, 102, 103, 101],
'Name': ['Alice', 'Bob', 'Charlie', 'Alice'],
'PurchaseAmount': [150, 200, 300, 150]}
df = pd.DataFrame(data)
print("\nИсходный датафрейм:")
print(df
Ваша задача состоит в удалении дубликатов и сохранении только уникаль-
ных строк в датафрейме.
Решение
# Ищем дублирующиеся строки
duplicates = df[df.duplicated()]
print("\nДублирующиеся строки:")
print(duplicates)
# Ищем дубликаты
df = df.drop_duplicates()
print("\nДатафрейм после удаления дубликатов:")
print(df)
Вывод:
Исходный датафрейм:
CustomerID
Name PurchaseAmount
0
101 Alice
150
1
102
Bob
200
2
103 Charlie
300
3
101 Alice
150
Практические упражнения 471
Дублирующиеся строки:
CustomerID Name PurchaseAmount
3
101 Alice
150
Датафрейм после удаления дубликатов:
CustomerID
Name PurchaseAmount
0
101 Alice
150
1
102
Bob
200
2
103 Charlie
300
Упражнение 3. Стандартизация текста в категориальных
данных
Есть набор данных с разным написанием наименований категорий:
import pandas as pd
# Простой набор данных с неконсистентным вводом категорий
data = {'Category': ['Electronics', 'electronics', 'ELECTronics', 'Furniture',
'furniture'],
'Sales': [100, 200, 300, 400, 500]
}
df = pd.DataFrame(data)
print("\nИсходный датафрейм:")
print(df)
Ваша задача состоит в приведении всех наименований категорий к ниж-
нему регистру, группировке данных по категории и выводе средней суммы
по каждой категории.
Решение
# Приводим категории к нижнему регистру
df['Category'] = df['Category'].str.lower()
# Считаем средние цены по категориям
avg_sum_per_category = df.groupby('Category')['Sales'].mean().sort_values(ascending=False)
print("\nСредние суммы по категориям:")
print(avg_sum_per_category)
Вывод:
Исходный датафрейм:
Category Sales
0 Electronics 100
1 electronics 200
2 ELECTronics 300
3 Furniture
400
4 furniture
500
Средние суммы по категориям:
Category
furniture
450.0
electronics 200.0
Name: Sales, dtype: float64
472 Корректировка аномалий в данных при помощи Pandas
Упражнение 4. Удаление значений, выходящих
за обозначенные границы
Есть набор данных со столбцом с ростом сотрудников в см. Допустимым
считается диапазон значений от 120 до 240:
import pandas as pd
# Простой набор данных со значениями, выходящими за допустимые пределы
data = {'Name': ['Alice', 'Bob', 'Charlie', 'Diana'],
'Height': [44, 132, 197, -5]}
df = pd.DataFrame(data)
print("\nИсходный датафрейм:")
print(df)
Ваша задача состоит в удалении из набора данных строк, в которых зна-
чения роста не входят в указанный диапазон.
Решение
# Определяем значения, выходящие за границы диапазона
out_of_range = df[(df['Height'] > 240) | (df['Height'] < 120)]
print("\nЗначения, выходящие за границы диапазона:")
print(out_of_range)
# Удаляем значения, выходящие за границы диапазона
df = df[(df['Height'] >= 120) & (df['Height'] <= 240)]
print("\nДатафрейм после удаления значений, выходящих за границы диапазона:")
print(df)
Вывод:
Исходный датафрейм:
Name Height
0 Alice
44
1
Bob
132
2 Charlie
197
3 Diana
-5
Значения, выходящие за границы диапазона:
Name Height
0 Alice
44
3 Diana
-5
Датафрейм после удаления значений, выходящих за границы диапазона:
Name Height
1
Bob
132
2 Charlie
197
Упражнение 5. Замена пропущенных значений
после коррекции аномалий в данных
Есть набор данных со столбцом OrderDate, в котором появились пропущенные
значения после корректировки аномалий:
Возможные проблемы 473
import pandas as pd
# Простой набор данных с пропущенными значениями
data = {'OrderDate': ['2022-01-15', pd.NaT, '2022-02-05', pd.NaT]}
df = pd.DataFrame(data)
print("\nИсходный датафрейм:")
print(df)
Ваша задача состоит в подстановке пропусков в столбце методом прямого
заполнения.
Решение
# Заполняем пропуски прямым проходом
df['OrderDate'] = df['OrderDate'].ffill()
print("\nДатафрейм после замены пропущенных значений:")
print(df)
Вывод:
Исходный датафрейм:
OrderDate
0 2022-01-15
1
NaT
2 2022-02-05
3
NaT
Датафрейм после замены пропущенных значений:
OrderDate
0 2022-01-15
1 2022-01-15
2 2022-02-05
3 2022-02-05
8.7. Возможные проблемы
В этом разделе мы поговорим о скрытых угрозах, которые могут поджидать
вас при работе с выбросами, аномалиями и неконсистентностью в данных.
8.7.1. Удаление важных наблюдений вместе
с выбросами
При идентификации и удалении выбросов бывает легко принять выбива-
ющиеся из общего ряда, но при этом важные наблюдения, за ошибочные
данные. К примеру, в наборе данных с информацией о пациентах клиники
выбивающееся из общей картины наблюдение может говорить о наличии
редкого заболевания, а не об ошибочном замере показателей.
474 Корректировка аномалий в данных при помощи Pandas
Что может пойти не так:
удаление значимых выбросов может приводить к погрешностям моде-
ли, особенно в областях вроде медицины или финансов, где выбросы
могут нести ценную информацию;
удаление выбросов может обеднять обучающий набор данных, что чре-
вато ухудшением качества предсказаний.
Решение:
всегда внимательно оценивайте отклоняющиеся от общей канвы на-
блюдения на предмет их значимости для данных перед удалением.
В этом вам может помочь знание предметной области;
используйте технику винсоризации, которую мы описывали ранее,
вместо простого удаления, когда считаете, что наблюдения с экстре-
мальными значениями необходимо сохранить.
8.7.2. Чрезмерная стандартизация категориальных
данных
При выполнении операции стандартизации категориальных данных (напри-
мер, при приведении наименований категорий товаров к нижнему регистру)
есть риск потери значимых различий между категориями. К примеру, наиме-
нования Electronics и electronic parts могут относиться к разным категориям
в розничной базе данных.
Что может пойти не так:
сращивание категорий может негативно сказываться на способности
модели выявлять нюансы в данных, что чревато ухудшением точности
предсказаний;
чрезмерная стандартизация категориальных данных может приводить
к нейтрализации важных шаблонов в иерархических категориях (на-
пример, при работе с ролями Junior и Senior).
Решение:
всегда тщательно исследуйте имеющиеся категории в данных перед вы-
полнением стандартизации. Применяйте преобразования только к на-
именованиям, действительно обозначающим одни и те же категории;
рассмотрите возможность использования таблиц соответствия для по-
хожих категорий вместо применения обычной стандартизации или
воспользуйтесь иерархией категорий.
8.7.3. Ошибочное интерпретирование дубликатов
Дублирование записей не всегда можно воспринимать как ошибку. Иногда
повторы могут говорить о возвращении покупателя или повторной транзак-
ции, и в таких случаях их удаление может приводить к потере данных.
Возможные проблемы 475
Что может пойти не так:
удаление дубликатов может приводить к обеднению данных, особенно
при анализе потребительского поведения или шаблонов транзакций;
удаление значимых записей может нарушать поведение критических
метрик, отвечающих за общие продажи или возвращение покупателей.
Решение:
всегда внимательно исследуйте дубликаты на предмет избыточности,
сравнивая значения дополнительных переменных (например, даты
и времени транзакции).
8.7.4. Ошибочное удаление значений, выходящих
за границы диапазона
Иногда удаление значений, выходящих за обозначенные границы диапазо-
на, может приводить к появлению погрешностей в модели, особенно если
эти значения характеризуют важные наблюдения, выбивающиеся из общего
ряда. К примеру, экстремальные значения возраста пациентов могут стать
поводом для отдельного исследования.
Что может пойти не так:
удаление значений, выходящих за границы диапазона, может приво-
дить к снижению обобщающей способности модели;
удаление важных наблюдений может негативно сказываться на разно-
образии данных и, как следствие, надежности производимого анализа.
Решение:
воспользуйтесь другими граничными значениями для указания диа-
пазона. Иногда бывает полезнее помечать выбросы с помощью отдель-
ного флага, а не удалять их;
вы можете сохранить наблюдения, которые считаете выбросами, и про-
анализировать их отдельно.
8.7.5. Ошибки, появляющиеся в результате
автоматической стандартизации
Иногда стандартизация форматов данных, например в отношении дат или
валют, может приводить к непреднамеренным модификациям, особенно при
использовании некорректных предположений. Допустим, если приводить
все даты к формату MM/DD/YYYY, то даты, поступившие в формате DD/MM/
YYYY, могут быть преобразованы неправильно.
Что может пойти не так:
некорректное преобразование дат может приводить к нарушению хро-
нологии записей и появлению ошибок в анализе;
476 Корректировка аномалий в данных при помощи Pandas
неправильное интерпретирование числовых данных (например, при-
равнивание выражений €1,000 и $1,000) может приводить к ошибкам
в калькуляциях.
Решение:
всегда тщательно исследуйте форматы исходных данных перед при-
менением автоматизированной стандартизации;
определяйте и следите за использованием правил, касающихся фор-
матов данных, при выполнении загрузки.
8.7.6. Ошибки в результате подстановки
пропущенных значений
После выполнения подстановки пропущенных значений, особенно тех, кото-
рые были получены в результате очистки данных, могут появиться погреш-
ности в модели. К примеру, метод прямого заполнения при обработке про-
пущенных дат может серьезно нарушить хронологию событий и негативно
сказаться на анализе сезонных составляющих.
Что может пойти не так:
применение прямого или обратного заполнения при подстановке про-
пусков может приводить к созданию искусственных трендов и корре-
ляций в данных;
замена пропусков без учета трендов и сезонности может приводить
к снижению качества предсказаний временных рядов.
Решение:
всегда используйте методы подстановки, учитывающие природу дан-
ных. Например, вы можете применить интерполяцию на основе вре-
мени или сезонные средние значения для временных рядов;
вы можете оставить значения пропущенными, если ни один метод под-
становки для них не подходит, и позволить модели самой обработать
их, например при помощи техник на основе деревьев.
Заключение
В этой главе мы рассмотрели несколько техник корректировки аномалий
в данных, доступных в библиотеке Pandas. В частности, мы поговорили
о способах обработки форматов данных, научились обнаруживать и удалять
дубликаты, исправлять неконсистентные категориальные данные, должным
образом обрабатывать значения, выходящие за допустимый диапазон, и вы-
полнять подстановку пустых значений, образовавшихся в результате коррек-
тировки аномалий. Все эти техники вы будете использовать на постоянной
основе в процессе очистки и предварительной подготовки данных.
В следующей главе мы подробно поговорим о методах снижения размер-
ности.
Глава 9
Методы снижения
размерности
В условиях постоянно меняющихся требований в области науки о данных
нам приходится иметь дело со все более сложными и многомерными набо-
рами, часто включающими несметное количество признаков. Такая прорва
информации, пусть и невероятно ценной, создает существенные сложности
для анализа данных и разработки моделей. Подобные сложности могут про-
являться по-разному и выливаться в повышенные требования к вычисли-
тельным системам, риск возникновения переобучения моделей и преграды
на пути эффективной визуализации многомерных данных. Для решения та-
ких проблем специалисты по работе с данными и исследователи разработали
целый набор средств и методологий, направленных на снижение размерности
(dimensionality reduction).
По сути, процедура снижения размерности призвана привести объемные
многомерные данные к более или менее контролируемому виду за счет сни-
жения количества признаков в наборе при условии сохранения большей ча-
сти ценной информации о данных. Стратегически осмысленное снижение
размерности позволит упростить итоговую модель, повысить ее произво-
дительность и более интуитивно визуализировать результаты исследований.
В этой главе мы поговорим о наиболее распространенных и эффективных
техниках снижения размерности, таких как анализ главных компонент, ли-
нейный дискриминантный анализ и метод нелинейного снижения размер-
ности и визуализации многомерных переменных.
Для каждой из перечисленных техник мы приведем полноценный пример
использования, а также углубимся в их математические обоснования и по-
говорим об их преимуществах и недостатках.
9.1. Анализ главных компонент (PCA)
Анализ главных компонент (Principal Component Analysis – PCA) представляет
собой одну из основных техник снижения размерности, которая широко при-
478 Методы снижения размерности
меняется в самых разных областях науки о данных и алгоритмах машинного
обучения. В своей основе анализ главных компонент сводится к матема-
тической процедуре, преобразующей полный набор переменных, которые
могут коррелировать друг с другом, к уменьшенному набору линейно не-
коррелирующих признаков, которые и называются главными компонентами
(principal component).
Прелесть анализа главных компонент состоит в способности обнаружи-
вать шаблоны в данных. Для этого осуществляется проекция данных в новой
системе координат, где оси, представляющие собой главные компоненты,
упорядочиваются в порядке объема объясняемой ими дисперсии в исходных
данных. И этот порядок чрезвычайно важен! Первая главная компонента
способна объяснить большую часть дисперсии в данных, тогда как все по-
следующие объясняют остаток дисперсии при условии их ортогональности
к уже выявленным компонентам.
Объяснение дисперсии позволяет методу главных компонент обнаружить
большую часть важных аспектов в данных. Первые несколько компонент за-
частую содержат большую часть информации, присутствующей в исходных
данных. Это позволяет аналитикам существенно снижать размерность своих
данных с сохранением большинства важных характеристик.
На практике анализ главных компонент применяется в самых разных об-
ластях:
в сфере обработки изображений использование этого метода позволя-
ет сжимать изображения, представляя их с меньшим количеством из-
мерений. Это обеспечивает серьезную экономию места при хранении,
притом что качество исходных изображений сильно не ухудшается;
в области финансов метод главных компонент применяется при ана-
лизе биржевых сводок для выявления основных факторов влияния на
движения рынка;
в биоинформатике этот метод позволяет исследователям визуализи-
ровать сложные генетические данные, что облегчает процесс распо-
знавания шаблонов и зависимостей между разными генами и наблю-
дениями.
Осознание того, когда стоит применять метод главных компонент, не усту-
пает по важности пониманию того, как именно он работает. Будучи доста-
точно мощным, анализ главных компонент все же предполагает наличие
в исходных данных линейных зависимостей, и он абсолютно бесполезен
в деле обнаружения сложных нелинейных шаблонов. В таких случаях лучше
бывает воспользоваться нелинейными техниками снижения размерности,
такими как t-SNE или UMAP.
Читая эту главу, вы узнаете, как применять анализ главных компонент
на практике, интерпретировать получаемые результаты и обходиться с его
ограничениями. Эти знания станут для вас трамплином к освоению более
сложных техник снижения размерности и способов их применения в реаль-
ных сценариях.
Анализ главных компонент (PCA) 479
9.1.1. Суть анализа главных компонент
Основной целью применения анализа главных компонент является осу-
ществление проекции данных в пространстве меньшей размерности с со-
хранением максимально возможного объема исходной информации. При
использовании этой техники снижение размерности достигается за счет вы-
явления направлений, называемых главными компонентами, вдоль которых
наблюдается наибольшая дисперсия. Эти главные компоненты формируют
новую систему координат, содержащую самую суть исходных данных. Давай-
те пройдемся по процедуре применения анализа главных компонент.
1. Центрирование данных
На первом шаге нам необходимо выполнить операцию центрирования дан-
ных путем вычитания среднего из значений каждой переменной. Это по-
зволит центрировать наблюдения в исходной системе координат. Таким
образом, мы исключаем любые смещения, которые могут присутствовать
в исходных данных. Центрирование данных предполагает несколько важных
следствий:
обеспечивает тот факт, что первая главная компонента в действитель-
ности будет соответствовать направлению, вдоль которого наблюдает-
ся наивысшая дисперсия в наборе данных. Без центрирования первая
главная компонента могла бы быть построена на основании общего
расположения облака данных, а не их внутренней структуры;
облегчает расчет ковариационной матрицы на следующих шагах;
позволяет делать более осмысленные сравнения признаков. Вычитая
среднее значение, мы смещаем фокус в сторону того, насколько кон-
кретные значения отличаются от общей нормы, а не концентрируемся
на их абсолютных значениях;
помогает легче интерпретировать полученные в результате главные
компоненты. После центрирования главные компоненты будут про-
ходить через начало системы координат, что сделает их направления
более интуитивно понятными. Математически центрирование дости-
гается путем вычитания среднего по каждому признаку из каждого на-
блюдения. Если обозначить исходную матрицу данных как X с m пере-
менными и n наблюдениями, то центрированную матрицу X_centered
можно получить следующим образом:
X_centered = X – μ,
где μ – матрица такого же размера, как X, в которой столбцы заполнены
средними значениями с повторами в количестве n раз. Этот на пер-
вый взгляд простой шаг позволяет заложить основы для последующих
вычислений и существенно повысить качество и интерпретируемость
итоговых результатов.
480 Методы снижения размерности
2. Вычисление ковариационной матрицы
Следующим шагом после центрирования данных нам нужно вычислить ко-
вариационную матрицу. Это квадратная симметричная матрица, элементы
которой представляют ковариацию между двумя признаками. Ковариацион-
ная матрица очень важна по причине того, что:
позволяет количественно выразить зависимости между переменными,
отражая их взаимную изменчивость;
помогает определить наличие корреляции и связи между перемен-
ными;
формирует фундамент для нахождения собственных векторов и соб-
ственных значений на следующих шагах.
Ковариационная матрица рассчитывается на основе центрированных зна-
чений, полученных на предыдущем шаге. Для набора данных, состоящего из
m признаков, ковариационная матрица будет обладать размером m×m. Каж-
дый элемент матрицы (i, j) этой матрицы будет характеризовать ковариацию
между i-м и j-м признаками, а элементы на диагонали будут соответствовать
дисперсии признаков.
С точки зрения математики ковариационная матрица C вычисляется по
следующей формуле:
C = (1/(n – 1)) * X_centered.T * X_centered,
где X_centered – это центрированная матрица данных, n – количество наблю-
дений, а X_centered.T – транспонированная матрица X_centered.
Ковариационная матрица является симметричной, поскольку ковариация
между признаками A и B – это то же самое, что и ковариация между призна-
ками B и A. Это свойство важно для следующего шага с разложением матрицы
по собственным значениям.
3. Вычисление собственных значений
и собственных векторов
После расчета ковариационной матрицы она используется для вычисления
собственных значений и собственных векторов. Этот шаг очень важен при
выполнении анализа главных компонент, поскольку именно он закладывает
основы для определения главных компонент. Что же такое собственные зна-
чения и собственные векторы?
Собственные значения (eigenvalue) – это скалярные величины, количест-
венно выражающие объем дисперсии, описываемый каждым собственным
вектором. Большие собственные значения указывают направления наиболь-
шей изменчивости данных.
Собственные векторы (eigenvector) представляют направления наиболь-
шей изменчивости данных. Каждому собственному вектору ставится в со-
ответствие собственное значение, и он указывает направление главной
Анализ главных компонент (PCA) 481
компоненты. В процессе разложения по собственным значениям (eigende-
composition) ковариационной матрицы определяются собственные значения
и собственные векторы. С точки зрения математики мы решаем для кова-
риационной матрицы C следующее уравнение:
CV=λV,
где V – это собственный вектор, а λ – соответствующее собственное значение.
Собственные векторы с наибольшими собственными значениями и становят-
ся наиболее значимыми главными компонентами. Причина этого в том, что
они позволяют определить направления наибольшей изменчивости данных.
Ранжируя собственные векторы на основе их собственных значений, мы опре-
деляем, какие компоненты оставим в наборе данных в процессе снижения
размерности. Стоит отметить, что количество собственных значений и соб-
ственных векторов будет соответствовать количеству измерений в исходном
наборе данных. При этом многие из полученных векторов могут оказаться не
столь значимыми, т. е. будут обладать низкими собственными значениями,
что позволит исключить их без риска потерять много важной информации.
Этот шаг может оказаться очень затратным в плане вычислений, особенно
для наборов данных с большим количеством измерений. В связи с этим для
определения главных компонент часто используются оптимизированные ал-
горитмы, такие как метод степенных итераций (power iteration method) или
сингулярное разложение (singular value decomposition – SVD).
4. Отбор главных компонент
После вычисления собственных значений и собственных векторов нам необ-
ходимо отобрать несколько наиболее значимых векторов в качестве главных
компонент. Процесс отбора очень важен и должен учитывать сразу несколько
аспектов:
порог дисперсии: обычно рекомендуется выбирать компоненты, сово-
купно объясняющие большую часть дисперсии – около 80–95 %;
анализ графика каменистой осыпи: выводя на график собственные
значения в порядке убывания, мы зачастую легко можем определить
точку изгиба, или локтя, в которой кривая резко меняет направление.
Эта точка символизирует определенную границу, после которой ком-
поненты перестают вносить ощутимый вклад в объяснение дисперсии;
практические предпосылки: количество компонент для выбора также
может определяться требованиями к вычислительным ресурсам, ин-
терпретируемости результатов или знаниями о предметной области.
Выбранные главные компоненты формируют ортогональный базис, охва-
тывающий подпространство, описывающее большую часть дисперсии в дан-
ных. Проекция наших исходных наблюдений в этом подпространстве по-
зволит эффективно снизить размерность, сохранив при этом большинство
важных шаблонов и связей в наборе данных.
482 Методы снижения размерности
Важно отметить, что, несмотря на всю свою мощь, иногда применение
анализа главных компонент может приводить к исключению небольших, но
важных признаков, если они не вносят ощутимый вклад в общую дисперсию.
В связи с этим важно проявлять осторожность и быть особенно вниматель-
ным при использовании этого метода.
5. Проекция данных
На заключительном этапе анализа главных компонент нам необходимо
преобразовать исходные данные путем проецирования их на выбранные
компоненты. Это важная операция, позволяющая отобразить наблюдения
с большим количеством измерений в пространстве пониженной размерно-
сти, определенном отобранными на предыдущем шаге главными компонен-
тами. Ниже приведено подробное объяснение этого шага:
математическое преобразование: проекция осуществляется путем
матричного перемножения. Если исходную матрицу обозначить как
X, а матрицу из выбранных главных компонент – как P, то результиру-
ющую матрицу X_transformed можно получить следующим образом:
X_transformed = X * P.
Фактически эта операция позволяет развернуть и масштабировать
данные в соответствии с новой системой координат, определенной
выбранными главными компонентами;
снижение размерности: использование меньшего количества глав-
ных компонент в сравнении с исходным количеством признаков по-
зволяет добиться снижения размерности. Иначе говоря, в результиру-
ющей матрице X_transformed будет меньше столбцов, чем в исходной
матрице X, и каждый из них будет соответствовать одной из выбран-
ных главных компонент;
сохранение важной информации: несмотря на уменьшение коли-
чества признаков, результирующая матрица будет содержать большую
часть информации, присутствовавшей в исходном наборе данных. Это
обеспечивается отбором компонент, соответствующих направлениям
наибольшей изменчивости данных;
снижение шума: дополнительным бонусом от операции проецирова-
ния данных может быть снижение уровня шума. Исключая компоненты
с низкой изменчивостью данных, которые зачастую описывают шум,
мы потенциально повышаем качество представления присутствующих
в данных шаблонов;
повышение интерпретируемости: проецированные данные часто
могут обладать лучшей интерпретируемостью в сравнении с исходным
набором. Каждое измерение в новом пространстве представляет собой
комбинацию исходных признаков, описывающую большую часть дис-
персии в данных;
Анализ главных компонент (PCA) 483
визуализация: если мы в результате выполненного анализа придем
к двум или трем главным компонентам, то сможем наглядно визуали-
зировать наши изначально многомерные данные с помощью обычных
двух- и трехмерных графиков, что поможет определить наличие в них
кластеров, выбросов и трендов, которые были незаметны в исходных
многомерных данных.
Следуя приведенной выше последовательности действий, вы сможете эф-
фективно снизить размерность сложных данных без значительной потери
ценной информации. Эта техника позволит не только упростить анализ дан-
ных, но и визуализировать многомерные данные, выявить в них шаблоны
и снизить шум. Понимание всех шагов данного процесса лежит в основе
эффективного применения анализа главных компонент в разных областях
науки о данных и алгоритмах машинного обучения.
9.1.2. Реализация анализа главных компонент
при помощи Scikit-learn
Давайте применим анализ главных компонент к простому набору данных
с целью демонстрации возможности снижения размерности с сохранением
большей части важной информации. Мы воспользуемся классом PCA, при-
сутствующим в библиотеке Scikit-learn, который предлагает простой подход
к сложным математическим вычислениям, скрывающимся за ширмой ана-
лиза главных компонент. Используя этот класс, мы сможем абстрагироваться
от подробностей вычисления ковариационной матрицы и нахождения соб-
ственных векторов и собственных значений, что позволит нам сосредото-
читься на ключевых аспектах концепции снижения размерности.
Класс PCA предлагает простой интерфейс, при помощи которого можно
напрямую задать количество главных компонент, которое вы хотели бы оста-
вить. Такая гибкость бывает особенно важна при работе с многомерными
наборами данных, т. к. она позволяет экспериментировать с разными под-
ходами к снижению размерности и оценивать их влияние на анализ или
модели машинного обучения.
Пример применения анализа главных компонент
В этом примере мы воспользуемся популярным набором данных с инфор-
мацией о цветках ириса, содержащим четыре переменные. В процессе мы
сократим размерность до двух измерений, чтобы можно было удобно визуа-
лизировать наши данные.
import numpy as np
from sklearn.datasets import load_iris
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
484 Методы снижения размерности
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
# Загружаем набор данных Iris
iris = load_iris()
X = iris.data
y = iris.target
# Стандартизируем признаки
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# Инициализируем класс PCA для снижения размерности до двух
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X_scaled)
# Преобразуем вывод в датафрейм
df_pca = pd.DataFrame(data=X_pca, columns=['PC1', 'PC2'])
df_pca['target'] = y
df_pca['species'] = [iris.target_names[i] for i in y]
# Визуализируем данные по двум главным компонентам
plt.figure(figsize=(12, 8))
sns.scatterplot(data=df_pca, x='PC1', y='PC2', hue='species', style='species', s=70)
plt.title('PCA на наборе данных Iris', fontsize=16)
plt.xlabel('Главная компонента 1', fontsize=12)
plt.ylabel('Главная компонента 2', fontsize=12)
plt.legend(title='Вид цветка', title_fontsize='12', fontsize='10')
# Краткое описание кластеров
for species in iris.target_names:
subset = df_pca[df_pca['species'] == species]
centroid = subset[['PC1', 'PC2']].mean()
plt.annotate(species, centroid, fontsize=16, fontweight='bold')
plt.tight_layout()
plt.show()
# Рассчитываем и выводим на графике доли объясненной дисперсии
explained_variance_ratio = pca.explained_variance_ratio_
cumulative_variance_ratio = np.cumsum(explained_variance_ratio)
plt.figure(figsize=(10, 6))
plt.bar(range(1, len(explained_variance_ratio) + 1), explained_variance_ratio, alpha=0.5,
align='center', label='Индивидуальная доля')
plt.step(range(1, len(cumulative_variance_ratio) + 1), cumulative_variance_ratio,
where='mid', label='Накопительная доля')
plt.ylabel('Доли объясненной дисперсии')
plt.xlabel('Главные компоненты')
plt.title('Доли объясненной дисперсии по главным компонентам')
plt.legend(loc='best')
Анализ главных компонент (PCA) 485
plt.tight_layout()
plt.show()
# Выводим сопутствующую информацию
print("Доли объясненной дисперсии:", explained_variance_ratio)
print("Накопительные доли объясненной дисперсии:", cumulative_variance_ratio)
print("\nНагрузки признаков (корреляция между признаками и главными компонентами):")
feature_loadings = pd.DataFrame(
pca.components_ . T,
columns=['PC1', 'PC2'],
index=iris.feature_names
)
print(feature_loadings)
Вывод:
Доли объясненной дисперсии: [0.72962445 0.22850762]
Накопительные доли объясненной дисперсии: [0.72962445 0.95813207]
Нагрузки признаков (корреляция между признаками и главными компонентами):
PC1
PC2
sepal length (cm) 0.521066 0.377418
sepal width (cm) -0 .269347 0.923296
petal length (cm) 0.580413 0.024492
petal width (cm) 0.564857 0.066942
Рис. 9.1 PCA на наборе данных Iris
486 Методы снижения размерности
Рис. 9 .2 Доли объясненной дисперсии по главным компонентам
Давайте посмотрим, что тут происходит.
1. Подготовка данных:
• загружаем набор данных Iris с помощью функции load_iris() из со-
става библиотеки Scikit-learn;
• признаки стандартизируем при помощи класса StandardScaler. Этот
шаг очень важен, поскольку метод главных компонент очень чув-
ствителен к масштабу входных переменных.
2. Использование класса PCA:
• создаем экземпляр класса PCA с целью сокращения размерности до
двух;
• метод fit_transform() позволяет подогнать модель PCA к нашим дан-
ным и преобразовать сами данные за один проход.
3. Визуализация данных:
• строим диаграмму рассеяния по данным с уменьшенной размер-
ностью с помощью пакета Seaborn, который зачастую рисует более
красивые графики в сравнении с традиционным Matplotlib;
• каждый вид цветка ириса представлен на диаграмме своим цветом
и стилем маркера;
• для центроидов классов добавляем аннотации, чтобы лучше было
видно, как распределяются наши данные в обновленном простран-
стве меньшей размерности.
Анализ главных компонент (PCA) 487
4. Анализ долей объясненной дисперсии:
• рассчитываем и выводим на графике доли объясненной дисперсии
в разрезе главных компонент;
• столбиками показан индивидуальный вклад каждой компоненты
в долю объясненной дисперсии;
• ступенчатый линейный график показывает накопительную долю
объясненной дисперсии, что бывает полезно при определении того,
сколько компонент оставить.
5. Нагрузки исходных признаков:
• здесь мы выводим информацию о нагрузках, или вкладе, исходных
признаков, которые выражаются в виде корреляции между призна-
ками и главными компонентами;
• эта информация помогает интерпретировать значение созданных
главных компонент в терминах исходных переменных.
Здесь мы показали не только как выполнить операцию снижения размер-
ности, но и как проинтерпретировать полученные результаты.
9.1.3. Объясненная дисперсия и анализ главных
компонент
Одним из главных достоинств метода главных компонент является его
способность количественно выражать объем исходной информации, со-
храненной в процессе снижения размерности. Основной метрикой в этом
отношении является доля объясненной дисперсии (explained variance ratio), по-
казывающая, какая часть исходной информации содержится в каждой глав-
ной компоненте. Эта метрика позволяет оценить значимость компонент
в деле представления информации из набора данных.
Оценивая накопительную долю объясненной дисперсии, мы можем по-
нять, какой процент важной информации нам удалось сохранить. К примеру,
мы можем остановить свой выбор на таком количестве компонент, которое
позволит сохранить 95 % исходной дисперсии.
Также оценка доли объясненной дисперсии помогает интерпретировать
значимость каждой компоненты в отдельности. Так, компоненты с более
высокой долей способны нести в себе сведения о большей части шаблонов
и трендов, присутствующих в исходных данных. Эта информация бывает
полезна при выборе главных компонент и получении дополнительных све-
дений о структуре исходного набора данных и набора с сокращенным числом
измерений.
Стоит также отметить, что само распределение долей объясненной дис-
персии по главным компонентам может дать массу полезной информации
о характере исходных данных. К примеру, резкие перепады на графике до-
488 Методы снижения размерности
лей объясненной компонентами дисперсии могут означать, что на самом
деле наши данные обладают маломерной структурой, тогда как постепенное
снижение доли на графике может говорить о сложной многомерной при-
роде данных, где каждая исходная переменная обеспечивает существенный
вклад в изменчивость. Подобный анализ может помочь с выбором модели
и позволить понять, насколько сложной структурой обладает исходный на-
бор данных.
Пример анализа долей объясненной дисперсии
с помощью метода PCA
Давайте рассчитаем доли всех возможных главных компонент на примере
того же набора данных, посвященного видам цветка ириса.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
# Загружаем набор данных Iris
iris = load_iris()
X = iris.data
y = iris.target
feature_names = iris.feature_names
# Стандартизируем признаки
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# Инициализируем класс PCA для снижения размерности до всех возможных компонент
pca_full = PCA()
X_pca = pca_full.fit_transform(X_scaled)
# Рассчитываем доли объясненной дисперсии и накопительные доли
explained_variance_ratio = pca_full.explained_variance_ratio_
cumulative_variance = np.cumsum(explained_variance_ratio)
# Выводим доли объясненной дисперсии и накопительные доли
print("Доли объясненной дисперсии по главным компонентам:", explained_variance_ratio)
print("Накопительные доли объясненной дисперсии:", cumulative_variance)
# Выводим на графике накопительные доли объясненной дисперсии
plt.figure(figsize=(10, 6))
plt.plot(range(1, len(cumulative_variance) + 1), cumulative_variance, 'bo-')
plt.xlabel('Количество главных компонент')
Анализ главных компонент (PCA) 489
plt.ylabel('Накопительная объясненная дисперсия')
plt.title('Накопительная объясненная дисперсия для набора данных Iris')
plt.xticks(range(1, X.shape[1]+1))
plt.grid(True)
plt.tight_layout()
plt.show()
# Выводим на графике индивидуальные доли объясненной дисперсии
plt.figure(figsize=(10, 6))
plt.bar(range(1, len(explained_variance_ratio) + 1), explained_variance_ratio)
plt.xlabel('Главная компонента')
plt.ylabel('Доля объясненной дисперсии')
plt.title('Доли объясненной дисперсии по главным компонентам')
plt.xticks(range(1, X.shape[1]+1))
plt.tight_layout()
plt.show()
# Рассчитываем и выводим нагрузки признаков
feature_loadings = pd.DataFrame(
pca_full.components_ . T,
columns=[f'PC{i+1}' for i in range(len(feature_names))],
index=feature_names
)
print("\nНагрузки признаков:")
print(feature_loadings)
# Визуализируем первые две главные компоненты
plt.figure(figsize=(10, 8))
scatter = plt.scatter(X_pca[:, 0], X_pca[:, 1], c=y, cmap='viridis')
plt.xlabel('Первая главная компонента')
plt.ylabel('Вторая главная компонента')
plt.title('Набор данных Iris в пространстве главных компонент')
plt.colorbar(scatter, label='Виды цветка')
plt.tight_layout()
plt.show()
Вывод:
Доли объясненной дисперсии по главным компонентам: [0.72962445 0.22850762 0.03668922
0.00517871]
Накопительные доли объясненной дисперсии: [0.72962445 0.95813207 0.99482129 1.
]
Нагрузки признаков:
PC1
PC2
PC3
PC4
sepal length (cm) 0.521066 0.377418 0.719566 -0.261286
sepal width (cm) -0 .269347 0.923296 -0 .244382 0.123510
petal length (cm) 0.580413 0.024492 -0 .142126 0.801449
petal width (cm) 0.564857 0.066942 -0 .634273 -0.523597
490 Методы снижения размерности
Рис. 9 .3 Накопительная объясненная дисперсия для набора данных Iris
Рис. 9 .4 Доли объясненной дисперсии по главным компонентам
Анализ главных компонент (PCA) 491
Рис. 9 .5 Набор данных Iris в пространстве главных компонент
В отличие от предыдущего кода, здесь мы создали экземпляр класса PCA
без передачи ему параметра n_components для извлечения всех возможных
главных компонент.
На рис. 9 .3 мы показали, как увеличивается накопительная доля объяснен-
ной дисперсии при добавлении новых компонент. Как видите, при исполь-
зовании всех четырех главных компонент мы получаем 100 % объясненной
дисперсии, что вполне логично. На рис. 9 .4 мы видим, какую долю диспер-
сии объясняют каждая из четырех главных компонент по отдельности, а на
рис. 9 .5 отобразили наш набор данных в пространстве двух первых главных
компонент. Кроме того, в выводе скрипта мы видим, какой нагрузкой обла-
дает каждый из исходных признаков в нашем наборе данных.
9.1.4. Когда стоит применять анализ
главных компонент
Анализ главных компонент можно применять в самых разных сценариях,
а главной его целью является приведение сложных наборов данных к более
простому виду. Вот несколько условий, в которых этот вид анализа может
оказаться полезным:
чрезмерно сложные наборы данных с большим количеством из-
мерений: при работе с такими массивами данных почти всегда мож-
492 Методы снижения размерности
но эффективно использовать анализ главных компонент. Это позво-
лит не только сократить время на вычисления, но и облегчит процесс
визуализации данных. К примеру, в геномике, где зачастую одно-
временно анализируются тысячи генов, анализ главных компонент
может помочь свести сложный набор данных к более управляемому
подмножеству компонент с сохранением большей части важной ин-
формации;
наличие корреляции между признаками: метод главных компонент
прекрасно справляется с наборами данных, переменные в которых
сильно коррелируют друг с другом. Путем определения направлений,
характеризующихся наибольшей изменчивостью данных, этот метод
эффективно объединяет коррелирующие признаки вместе. Это бывает,
например, полезно в области финансов, где множество экономических
показателей изменяются по одному шаблону;
снижение шума в данных: в большинстве реальных наборов данных
присутствует шум, мешающий правильно идентифицировать шабло-
ны. Анализ главных компонент решает эту проблему за счет того, что
в основном оперирует переменными, создающими большие колеба-
ния, а шумы часто относятся к незначительным проявлениям измен-
чивости. Это свойство делает данный метод полезным при обработке
различных сигналов, включая изображения и речь;
предварительная обработка для моделей машинного обучения:
анализ главных компонент может быть одним из важнейших шагов на
этапе предварительной обработки исходных данных. Снижая количест-
во признаков, вы тем самым уменьшаете риск переобучения модели
и повышаете ее качество, особенно если количество признаков в ней
значительно превышает число наблюдений.
В качестве ложки дегтя можно заметить, что, будучи линейной техникой,
анализ главных компонент предполагает, что зависимости в данных могут
быть представлены в линейном виде. Таким образом, для наборов данных
со сложной нелинейной структурой лучше могут подойти методы вроде
t-SNE или UMAP. Эти нелинейные техники способны обнаруживать сложные
шаблоны в данных, жертвуя при этом интерпретируемостью в сравнении
с анализом главных компонент.
9.1.5. Ключевые выводы об анализе главных
компонент
Кратко перечислим все, что мы узнали об анализе главных компонент:
анализ главных компонент представляет собой мощную технику сни-
жения размерности данных путем преобразования исходных при-
знаков в направления, называемые главными компонентами. Эти
компоненты упорядочиваются по доле объясняемой ими дисперсии
Техники отбора признаков 493
в данных – тем самым мы как бы концентрируем большую часть цен-
ной информации о данных в небольшом количестве новых измерений;
доля объясненной дисперсии – это основная метрика анализа главных
компонент, количественно выражающая количество информации, за-
ключенной в каждой компоненте. С помощью этой меры аналитик
может определить оптимальное количество компонент, которые не-
обходимо оставить в наборе данных, исходя из баланса между новой
размерностью и объемом сохраненной информации;
области применения анализа главных компонент:
• снижение уровня шума: с помощью этого метода можно попробо-
вать выделить значимый сигнал из шума, что позволит повысить
качество данных;
• визуализация: приводя исходные многомерные данные к двум или
трем измерениям, мы получаем возможность удобно визуализиро-
вать их с помощью традиционных графиков;
• сжатие данных: анализ главных компонент позволяет значительно
снизить размер набора данных при сохранении в нем важной ин-
формации;
• выделение признаков: анализ главных компонент может помочь
в извлечении новых важных признаков, которые будут содержать
самую суть исходных данных;
ограничения анализа главных компонент:
• предположение о линейности: метод главных компонент предпо-
лагает, что признаки в наборе данных характеризуются линейными
зависимостями, что не всегда соответствует действительности;
• сложности с интерпретацией результатов: созданные главные ком-
поненты могут вносить сложности в плане интерпретации в связи
с неопределенностью их связи с исходными признаками;
• чувствительность к выбросам: наблюдения, выбивающиеся из общей
картины, могут существенно влиять на результаты анализа главных
компонент;
прочие техники, такие как t-SNE и UMAP, могут использоваться со-
вместно с анализом главных компонент для более полноценного сни-
жения размерности, особенно при работе с данными, характеризую-
щимися нелинейными зависимостями.
9.2. Техники отбора признаков
Зачастую реальные наборы данных могут состоять из великого множества
исходных признаков. При этом важно понимать, что далеко не все из них
вносят одинаковый вклад в качество будущих моделей. Некоторые признаки
могут оказаться частично или полностью бесполезными для моделей, в том
числе по причине дублирования данных в других признаках. Еще более важ-
494 Методы снижения размерности
но то, что некоторые признаки могут вносить паразитный шум в данные,
что может приводить к переобучению моделей и снижению их обобщающей
способности.
Описанные выше сложности могут существенно затруднить работу с объ-
емными наборами данных. Кроме того, включение в модель избыточных или
шумовых признаков может негативно сказаться на вычислительных затратах
на обучение и развертывание модели.
Для решения подобных проблем специалисты по работе с данными при-
меняют разнообразные методы, называемые техниками отбора признаков
(feature selection technique). Эти техники призваны выполнять следующие
задачи:
помощь в определении и сохранении наиболее важных признаков,
описывающих самую суть данных;
повышение интерпретируемости модели за счет снижения количества
используемых признаков;
снижение вычислительных расходов и ускорение процесса обучения
и развертывания модели;
повышение точности предсказаний модели за счет концентрирования
на наиболее важных аспектах данных.
Все техники отбора признаков можно условно разделить на три категории:
методы фильтрации: эти методы оценивают важность признаков ис-
ходя из их статистических свойств безотносительно конкретных мо-
делей;
оберточные методы: методы из этой категории базируются на про-
верке разных подмножеств признаков применительно к конкретной
модели;
встроенные методы: эти методы включают технику отбора признаков
непосредственно в процесс обучения модели.
Каждая из этих категорий методов обладает своими преимуществами
и областями применения. В следующих разделах мы подробно разберем
все перечисленные категории и приведем примеры их использования на
практике.
9.2.1. Методы фильтрации
Методы фильтрации (filter method) представляют собой основу техник от-
бора признаков и оперируют исключительно с наборами данных, безотноси-
тельно конкретных моделей машинного обучения. При использовании этих
методов признаки оцениваются на основе их статистических свойств, таких
как коэффициенты корреляции, дисперсия и информационно-теоретиче-
ские критерии, включая количество взаимной информации.
Основным преимуществом методов фильтрации является их вычисли-
тельная эффективность и масштабируемость, что делает их особенно полез-
Техники отбора признаков 495
ными при работе с большими многомерными данными. Эти методы могут
служить в качестве хорошей отправной точки в процессе отбора признаков,
позволяя аналитику быстро проранжировать переменные в исходном наборе
данных по их значимости.
Некоторые популярные методы из этой категории:
корреляция Пирсона: измеряет линейные зависимости между призна-
ками и целевой переменной;
критерий хи-квадрат: оценивает зависимости между категориальны-
ми признаками и целевой переменной;
взаимная информация: количественно выражает зависимости между
признаками и целевой переменной, учитывая как линейные, так и не-
линейные взаимосвязи.
Хотя методы фильтрации считаются довольно мощными и в то же вре-
мя простыми, они обладают некоторыми ограничениями. Дело в том, что
эти методы оценивают значимость переменных по отдельности, что может
приводить к упущению важных взаимодействий между признаками. Кроме
того, они могут не отвечать требованиям моделей, которые вы в дальнейшем
планируете применять к этим данным.
Несмотря на все ограничения, методы фильтрации играют существенную
роль в конвейере техник по отбору признаков. С их помощью можно вы-
полнить первичное прореживание исходного набора данных и тем самым
облегчить задачу отбора признаков более продвинутым методам, которые
можно применить после фильтрации. Такой многоэтапный подход к отбору
признаков зачастую приводит к получению надежных и эффективных мо-
делей, балансирующих между качеством предсказаний и вычислительной
эффективностью.
Распространенные методы фильтрации
Давайте разберем три наиболее часто используемых метода фильтрации,
а также обсудим их преимущества и области применения.
1. Метод установки порога дисперсии (variance thresholding). Этот метод
обращает внимание на меру изменчивости переменных. В результате
он исключает переменные с низкой дисперсией, опираясь на пред-
положение о том, что такие признаки обладают низкой различающей
способностью:
• реализация: устанавливается пороговое значение для показателя
дисперсии, и из набора данных исключаются переменные, характе-
ризующиеся дисперсией ниже заданного порога;
• применение: особенно эффективен при работе с наборами данных
с большим количеством бинарных или низковариабельных призна-
ков, таких как экспрессия генов, где определенные гены могут об-
ладать очень низкой изменчивостью;
• преимущества: быстро удаляет признаки с малой информатив-
ностью, снижая шум в наборе данных.
496 Методы снижения размерности
2. Метод установки порога корреляции (correlation thresholding). Этот ме-
тод решает проблему мультиколлинеарности в наборах данных. С по-
мощью него можно идентифицировать и исключить из набора сильно
коррелирующие друг с другом признаки, что позволит избавиться от
избыточных переменных:
• реализация: строится корреляционная матрица по всем признакам,
и устанавливается пороговое значение для коэффициента корре-
ляции. В результате из набора данных исключаются переменные,
характеризующиеся корреляцией выше заданного порога;
• применение: особенно эффективен при работе с наборами данных,
в которых признаки могут описывать одни и те же глубинные фак-
торы. Например, это касается сферы финансов, где экономические
показатели отражают одни и те же феномены;
• преимущества: позволяет создать более компактную модель путем
удаления избыточной информации, что способствует повышению
интерпретируемости модели и снижению риска переобучения.
3. Статистические критерии. В основе этих методов лежат различные
статистические меры для выявления зависимостей между признаками
и целевой переменной. Эти методы позволяют количественно ранжи-
ровать признаки и выбирать их наиболее информативные подмно-
жества для обучения моделей. Вот некоторые критерии, использую-
щиеся в подобных методах:
• критерий хи-квадрат: особенно полезен при работе с категориаль-
ными признаками. Можно использовать для классификации текстов
или анализа потребительской корзины;
• F-статистика ANOVA: применяется при работе с числовыми пере-
менными для определения статистически значимых различий меж-
ду средними значениями двух и более групп в целевой переменной.
Традиционно используется в области биомедицины и при сравнении
товаров;
• взаимная информация: универсальная метрика, способная выявлять
линейные и нелинейные зависимости между предикторами и целе-
вой переменной. Количественно измеряет количество информации,
получаемой о целевой переменной при исследовании заданного
признака. Особенно эффективен при работе со сложными набора-
ми данных, где зависимости между переменными могут быть не
столь очевидными, например при обработке сигналов или анализе
изображений.
Выбор метода фильтрации часто зависит от природы данных и особых тре-
бований конкретной задачи. Например, сначала при работе с многомерным
набором данных можно воспользоваться методом установки порога диспер-
сии, чтобы быстро исключить переменные с низкой изменчивостью. После
этого можно применить метод установки порога корреляции для дальней-
шей очистки набора данных и удаления из него избыточных дублирующих
Техники отбора признаков 497
признаков. Наконец, в завершение можно использовать один из статисти-
ческих критериев для ранжирования оставшихся признаков на основе связи
с целевой переменной.
Пример применения метода установки порога дисперсии
В наборах с больших количеством признаков могут присутствовать пере-
менные с низкой изменчивостью значений, редко несущие много полезной
информации. Избавиться от таких признаков можно следующим образом:
from sklearn.feature_selection import VarianceThreshold
import pandas as pd
# Простой набор данных с переменными с низкой изменчивостью
data = {'Feature1': [1, 1, 1, 1, 1],
'Feature2': [2, 2, 2, 2, 2],
'Feature3': [0, 1, 0, 1, 0],
'Feature4': [10, 15, 10, 20, 15]}
df = pd.DataFrame(data)
# Применяем метод установки порога дисперсии (порог = 0.2)
selector = VarianceThreshold(threshold=0.2)
reduced_data = pd.DataFrame(selector.fit_transform(df), columns = selector.get_feature_
names_out())
print("Признаки после применения метода установки порога дисперсии:")
print(reduced_data)
Вывод:
Признаки после применения метода установки порога дисперсии:
Feature3 Feature4
0
0
10
1
1
15
2
0
10
3
1
20
4
0
15
Здесь мы воспользовались классом VarianceThreshold и его методом fit_
transform() для отсечения признаков, обладающих дисперсией ниже задан-
ного порога. С помощью метода get_feature_names_out() мы восстановили
имена столбцов в датафрейме.
Пример применения метода установки порога корреляции
Сильно коррелирующие переменные зачастую несут в себе схожую инфор-
мацию, и во избежание появления мультиколлинеарности рекомендуется от
них избавляться. Сделать это можно так, как показано ниже:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
498 Методы снижения размерности
# Простой набор данных с коррелирующими переменными
np.random.seed(42)
n_samples = 1000
data = {'Feature1': np.random.normal(0, 1, n_samples),
'Feature2': np.random.normal(0, 1, n_samples),
'Feature3': np.random.normal(0, 1, n_samples),
'Feature4': np.random.normal(0, 1, n_samples)
}
data['Feature5'] = data['Feature1'] * 0.8 + np.random.normal(0, 0.2, n_samples) #
Корреляция с Feature1
data['Feature6'] = data['Feature2'] * 0.9 + np.random.normal(0, 0.1, n_samples) #
Корреляция с Feature2
df = pd.DataFrame(data)
# Рассчитываем корреляционную матрицу
correlation_matrix = df.corr()
# Визуализируем корреляционную матрицу
plt.figure(figsize=(10, 8))
sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm', vmin=-1, vmax=1, center=0)
plt.title('Корреляционная матрица до удаления коррелирующих переменных')
plt.show()
# Устанавливаем порог корреляции
threshold = 0.8
# Выбираем пары признаков с коэффициентом корреляции, превышающим порог
corr_features = set()
for i in range(len(correlation_matrix.columns)):
for j in range(i):
if abs(correlation_matrix.iloc[i, j]) > threshold:
colname = correlation_matrix.columns[i]
corr_features.add(colname)
print("Сильно коррелирующие столбцы для удаления:", corr_features)
# Функция для удаления коррелирующих переменных
def remove_correlated_features(df, threshold):
correlation_matrix = df.corr().abs()
upper_tri = correlation_matrix.where(np.triu(np.ones(correlation_matrix.shape), k=1).
astype(bool))
to_drop = [column for column in upper_tri.columns if any(upper_tri[column] >
threshold)]
return df.drop(to_drop, axis=1)
# Применяем функцию для удаления коррелирующих переменных
df_uncorrelated = remove_correlated_features(df, threshold)
print("\nФорма исходного датафрейма:", df.shape)
print("Форма датафрейма после удаления коррелирующих переменных:", df_uncorrelated.shape)
# Визуализируем корреляционную матрицу после удаления коррелирующих переменных
correlation_matrix_after = df_uncorrelated.corr()
plt.figure(figsize=(10, 8))
sns.heatmap(correlation_matrix_after, annot=True, cmap='coolwarm', vmin=-1, vmax=1,
center=0)
Техники отбора признаков 499
plt.title('Корреляционная матрица после удаления коррелирующих переменных')
plt.show()
Вывод:
Сильно коррелирующие столбцы для удаления: {'Feature6', 'Feature5'}
Форма исходного датафрейма: (1000, 6)
Форма датафрейма после удаления коррелирующих переменных: (1000, 4)
Рис. 9.6 Корреляционная матрица
до удаления коррелирующих переменных
Что мы здесь сделали?
1. Генерирование данных:
• создали набор данных, состоящий из 1000 наблюдений и шести пере-
менных;
• первые четыре переменные являются независимыми и обладают
нормальным распределением значений;
• переменную Feature5 мы сгенерировали так, чтобы она сильно кор-
релировала с переменной Feature1, а переменную Feature6 – так, что-
бы коррелировала с переменной Feature2;
• таким образом мы создали набор данных, отдаленно напоминаю-
щий реальный, где часто встречаются избыточные переменные.
500 Методы снижения размерности
Рис. 9.7 Корреляционная матрица
после удаления коррелирующих переменных
2. Вычисление корреляционной матрицы:
• мы воспользовались функцией corr() для создания корреляционной
матрицы по всем признакам в наборе данных;
• ячейки матрицы заполнены коэффициентами корреляции Пирсона
для всех пар переменных.
3. Визуализация корреляционной матрицы:
• здесь мы при помощи тепловой карты визуализировали корреляци-
онную матрицу. Темные цвета ячеек соответствуют более сильной
корреляции.
4. Поиск коррелирующих признаков:
• сначала мы установили порог корреляции на отметке 0.8;
• затем прошли по корреляционной матрице и извлекли пары при-
знаков с корреляцией, превышающей заданный порог;
• добавили эти столбцы в список для потенциального удаления corr_
features.
5. Функция для удаления коррелирующих переменных:
• определили функцию remove_correlated_features(), удаляющую при-
знаки с высоким коэффициентом корреляции;
• во избежание дублирования операций мы обработали только верх-
ний треугольник матрицы;
Техники отбора признаков 501
• в каждой паре коррелирующих признаков мы сохраняем один из них
и удаляем другой.
6. Удаление коррелирующих переменных:
• вызываем функцию remove_correlated_features() для нашего дата-
фрейма;
• выводим форму датафрейма до и после удаления коррелирующих
признаков.
7. Визуализация результатов:
• повторно выводим на тепловой карте корреляционную матрицу,
чтобы убедиться, что избыточные признаки были удалены.
9.2.2. Оберточные методы
Оберточные методы (wrapper method) исключения признаков подразумева-
ют многократное обучение и оценку качества модели машинного обучения
с разными наборами предикторов. Они нацелены на определение оптималь-
ной комбинации признаков с точки зрения максимизации качества предска-
заний модели. В отличие от методов фильтрации, которые функционируют
безотносительно конкретной модели, оберточные методы принимают в рас-
чет характеристики и смещения выбранного алгоритма.
Обычно процесс применения оберточных методов состоит из следующих
этапов:
1) выбор подмножества признаков;
2) обучение выбранной модели с этим набором признаков;
3) оценка качества модели;
4) повторение цикла с другими наборами признаков.
Несмотря на свою ресурсоемкость, оберточные методы обладают следую-
щими преимуществами:
учитывают возможные факторы взаимодействия между переменными,
которые методы фильтрации могут пропускать;
способны оптимизировать процесс отбора признаков для конкретной
выбранной модели;
принимают в расчет сложные зависимости между признаками и целе-
вой переменной.
К числу распространенных оберточных методов можно отнести рекур-
сивное исключение признаков (recursive feature elimination – RFE), а также
методы последовательного добавления и исключения признаков. Эти ме-
тоды бывают особенно полезны, когда качество модели имеет решающее
значение, а вопрос затрачиваемых ресурсов не столь критичен. Обычно они
применяются в сценариях с умеренным количеством признаков и числом
наблюдений, позволяющим безболезненно выполнить несколько этапов
обучения модели.
502 Методы снижения размерности
Описание наиболее распространенных оберточных методов приводим
ниже.
1. Рекурсивное исключение признаков (recursive feature elimination – RFE).
Этот метод позволяет последовательно улучшать набор признаков для
повышения качества модели:
• сначала обучение модели производится с полным набором приз-
наков;
• признаки ранжируются на основе их значимости;
• исключается наименее значимый признак;
• процесс повторяется до достижения желаемого количества призна-
ков в модели;
• этот метод бывает полезен при необходимости найти заданное ко-
личество наиболее значимых признаков;
• обычно используется совместно с линейными моделями (например,
с логистической регрессией) и моделями на основе деревьев.
2. Метод последовательного добавления признаков (forward selection). При
использовании этого подхода признаки последовательно добавляются
в модель:
• сначала обучение модели производится с пустым набором при-
знаков;
• постепенно в модель добавляется по одному признаку, максимально
увеличивающему ее качество;
• процесс повторяется до достижения критерия остановки (например,
плато в отношении увеличения качества модели);
• метод полезен для создания компактных моделей с минимально
возможным количеством признаков;
• бывает эффективен при наличии большого количества потенциаль-
ных признаков для модели.
3. Метод последовательного исключения признаков (backward elimination).
При использовании этого подхода признаки последовательно исклю-
чаются из модели:
• сначала обучение модели производится с полным набором при-
знаков;
• постепенно из модели исключается по одному признаку, оказываю-
щему наименьшее влияние на качество модели;
• процесс повторяется до достижения критерия остановки;
• помогает обнаружить и исключить из модели избыточные или наи-
менее важные признаки;
• бывает эффективен при наличии умеренного количества признаков
в наборе данных.
Оберточные методы исключения признаков часто позволяют выполнить
более тщательное прореживание исходных переменных в сравнении с мето-
дами фильтрации, поскольку могут учитывать требования конкретной моде-
ли и потенциальные взаимодействия признаков. В то же время эти методы
могут быть чрезвычайно ресурсозатратными, особенно при работе с больши-
Техники отбора признаков 503
ми наборами данных, что объясняется их итеративной природой. В основе
выбора метода часто лежат размер исходного набора данных, доступные
вычислительные ресурсы и требования конкретной решаемой задачи.
Пример применения метода рекурсивного исключения
признаков
Давайте применим метод отбора признаков RFE к простому набору данных
совместно с моделью логистической регрессии:
import numpy as np
import matplotlib.pyplot as plt
from sklearn.feature_selection import RFE
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
# Загружаем простой набор данных по видам цветков ириса
X, y = load_iris(return_X _y=True)
feature_names = load_iris().feature_names
# Разбиваем набор данных на обучающую и тестовую выборки
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# Инициализируем модель с оберткой в виде метода RFE с разным количеством признаков
n_features_to_select_range = range(1, len(feature_names) + 1)
accuracies = []
for n_features_to_select in n_features_to_select_range:
model = LogisticRegression(max_iter=10)
rfe = RFE(estimator=model, n_features_to_select=n_features_to_select)
# Выполняем обучение обертки RFE
rfe = rfe.fit(X_train, y_train)
# Преобразовываем данные, оставляя нужное количество признаков
X_train_rfe = rfe.transform(X_train)
X_test_rfe = rfe.transform(X_test)
# Обучаем модель
model.fit(X_train_rfe, y_train)
# Делаем предсказания
y_pred = model.predict(X_test_rfe)
# Рассчитываем точность модели и добавляем ее в список
accuracy = accuracy_score(y_test, y_pred)
accuracies.append(accuracy)
print(f"Количество признаков: {n_features_to_select}")
print("Выбранные признаки:", np.array(feature_names)[rfe.support_])
print("Ранжирование признаков:", rfe.ranking_)
print(f"Точность: {accuracy:.4f}\n")
504 Методы снижения размерности
print(accuracies)
# Выводим на графике точность модели в сравнении с количеством признаков
plt.figure(figsize=(10, 6))
plt.plot(n_features_to_select_range, accuracies, marker='o')
plt.xlabel('Количество признаков')
plt.ylabel('Точность')
plt.xticks(n_features_to_select_range)
plt.title('Точность модели в сравнении с количеством признаков')
plt.grid(True)
plt.show()
# Извлекаем оптимальное количество предикторов
best_n _features = n_features_to_select_range[np.argmax(accuracies)]
print(f"Оптимальное количество признаков: {best_n _features}")
# Заново запускаем RFE с оптимальным количеством предикторов
best_model = LogisticRegression(max_iter=1000)
best_rfe = RFE(estimator=best_model, n_features_to_select=best_n _features)
best_rfe = best_rfe.fit(X_train, y_train)
print("\nОптимальное подмножество признаков:")
print("Выбранные признаки:", np.array(feature_names)[best_rfe.support_])
print("Ранжирование признаков:", best_rfe.ranking_)
Вывод:
Количество признаков: 1
Выбранные признаки: ['sepal width (cm)']
Ранжирование признаков: [4 1 2 3]
Точность: 0.5556
Количество признаков: 2
Выбранные признаки: ['sepal width (cm)' 'petal length (cm)']
Ранжирование признаков: [3 1 1 2]
Точность: 0.8889
Количество признаков: 3
Выбранные признаки: ['sepal width (cm)' 'petal length (cm)' 'petal width (cm)']
Ранжирование признаков: [2 1 1 1]
Точность: 0.9778
Количество признаков: 4
Выбранные признаки: ['sepal length (cm)' 'sepal width (cm)' 'petal length (cm)'
'petal width (cm)']
Ранжирование признаков: [1 1 1 1]
Точность: 1.0000
[0.5555555555555556, 0.8888888888888888, 0.9777777777777777, 1.0]
Оптимальное количество признаков: 4
Оптимальное подмножество признаков:
Выбранные признаки: ['sepal length (cm)' 'sepal width (cm)' 'petal length (cm)'
'petal width (cm)']
Ранжирование признаков: [1 1 1 1]
Техники отбора признаков 505
Рис. 9.8 Точность модели в сравнении с количеством признаков
Что здесь происходит?
1. Загрузка и предобработка данных:
• загружаем набор данных по видам цветка ириса с помощью функции
load_iris();
• разбиваем набор на обучающую и тестовую выборки посредством
функции train_test_split().
2. Реализация метода RFE:
• в цикле отбираем разное количество признаков для модели при по-
мощи метода RFE;
• на каждой итерации цикла создаем модель логистической регрессии
и обертку RFE, затем применяем к RFE методы fit() и transform(),
в результате чего остается только нужное количество признаков.
После этого мы обучаем модель логистической регрессии на этом
количестве признаков, предсказываем целевую переменную и вы-
числяем точность модели, сохраняя ее в список.
3. Визуализация результатов:
• выводим на графике точность модели в сравнении с количеством
используемых признаков;
• график может помочь в выборе оптимального количества признаков
для итоговой модели.
4. Выбор оптимального набора признаков:
• выбираем количество признаков, соответствующее лучшему качест-
ву модели, при помощи функции np.argmax();
• перезапускаем модель RFE с оптимальным количеством признаков.
506 Методы снижения размерности
5. Вывод и интерпретация:
• на каждой итерации выводим количество признаков, выбранные
признаки, ранжирование признаков (более низкий ранг означает
более высокую значимость) и точность модели;
• по окончании цикла выводим оптимальное подмножество призна-
ков и их ранжирование.
9.2.3. Встроенные методы
Встроенные методы (embedded method) предлагают более продвинутый под-
ход к отбору признаков, заключающийся в интеграции процесса отбора не-
посредственно в фазу обучения модели. Это позволяет еще больше оптими-
зировать процесс выбора оптимальных признаков за счет учета специфики
конкретной модели. Кроме того, встроенные методы обладают преимущест-
вами в плане вычислительной эффективности, поскольку при их использо-
вании нам не нужно отдельно отбирать признаки и обучать модель.
Эффективность встроенных методов базируется на их способности исполь-
зовать внутренние механизмы модели с целью оценки значимости призна-
ков. К примеру, регрессия лассо, в которой применяется L1-регуляризация,
автоматически сжимает в направлении нуля коэффициенты наименее зна-
чимых признаков. Это позволяет не только отобрать оптимальное количест-
во признаков, но и предотвратить переобучение модели.
Техника определения значимости признаков на основе деревьев, пред-
ставляющая собой еще один распространенный встроенный метод, ис-
пользует структуру деревьев решений для оценки важности предикторов.
В ансамблевых моделях, таких как случайный лес или градиентный бустинг,
признаки, чаще остальных использующиеся для разделения узлов на ветви
или вносящие существенный вклад в снижение неопределенности, считают-
ся наиболее значимыми. Такой подход предлагает естественное ранжирова-
ние признаков на основе их предсказательной способности применительно
к выбранной модели.
Помимо регрессии лассо и техник на основе деревьев, к встроенным ме-
тодам также относятся метод эластичной сети, объединяющий в себе L1-
и L2-регуляризации, и некоторые алгоритмы нейронных сетей, содержащие
механизмы отбора признаков. С помощью этих методов можно добиться
нужного компромисса между точностью модели и ее интерпретируемостью.
Распространенные встроенные методы
Двумя наиболее распространенными встроенными методами являются сле-
дующие.
1. Регрессия лассо (Lasso Regression): этот метод применяет L1-регуля-
ризацию, штрафующую функцию потерь на основании абсолютных
значений коэффициентов признаков. Особенности метода:
Техники отбора признаков 507
• наименее значимые признаки получают коэффициенты, прибли-
женные или равные нулю, что фактически исключает их из списка
предикторов модели;
• добавляет разреженности модели, что делает ее более простой и ин-
терпретируемой;
• особенно полезен при работе с многомерными данными или при
необходимости выделить наиболее значимые признаки.
2. Модели на основе деревьев: эти модели, включающие деревья решений
и ансамблевые методы, такие как случайный лес, осуществляют отбор
признаков в процессе обучения. Особенности таких моделей:
• признаки ранжируются по значимости в процессе разделения узлов
на ветви;
• при использовании модели случайного леса значимость признаков
усредняется по множеству деревьев, что обеспечивает надежность
процессу отбора предикторов;
• способны обнаруживать нелинейные зависимости и эффекты взаи-
модействия между признаками, чего лишены линейные модели;
• полученные оценки значимости могут быть использованы в даль-
нейшем процессе отбора признаков или для конструирования новых
переменных.
Пример применения регрессии лассо
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import Lasso
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_squared_error, r2_score
from sklearn.datasets import fetch_openml
import warnings
warnings.filterwarnings("ignore")
housing = fetch_openml(name="house_prices", as_frame=True)
X = housing['data']
X = X.drop(columns=['Alley', 'LotFrontage', 'FireplaceQu', 'PoolQC', 'Fence',
'MiscFeature', 'MasVnrType', 'BsmtQual', 'BsmtCond', 'BsmtExposure',
'BsmtFinType1', 'BsmtFinType2', 'MasVnrArea', 'Electrical',
'GarageType', 'GarageYrBlt', 'GarageFinish', 'GarageQual',
'GarageCond'])
y = housing['target']
# Разделяем числовые и категориальные столбцы
numeric_cols = X.select_dtypes(include=[np.number]).columns
categorical_cols = X.select_dtypes(exclude=[np.number]).columns
# Кодируем категориальные столбцы с одним активным состоянием
df_cat = pd.get_dummies(X, columns=categorical_cols)
X = pd.concat([X.drop(columns=categorical_cols), df_cat], axis=1)
508 Методы снижения размерности
# Разбиваем набор данных на обучающую и тестовую выборки
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# Стандартизируем признаки
scaler = StandardScaler()
X_train[numeric_cols] = scaler.fit_transform(X_train[numeric_cols])
X_test[numeric_cols] = scaler.transform(X_test[numeric_cols])
feature_names = X_train.columns
# Инициализируем и обучаем модель регрессии лассо с разными значениями альфа
alphas = [0.1, 0.5, 1.0, 3.0, 5.0, 8.0, 10.0, 15.0, 20.0]
results = []
for alpha in alphas:
lasso = Lasso(alpha=alpha, random_state=42)
lasso.fit(X_train, y_train)
# Вычисляем значимость признаков
feature_importance = np.abs(lasso.coef_)
selected_features = np.where(feature_importance > 0)[0]
# Делаем предсказания
y_pred = lasso.predict(X_test)
# Рассчитываем метрики
mse = mean_squared_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)
results.append({
'alpha': alpha,
'selected_features': selected_features,
'mse': mse,
'r2': r2
})
print(f"\nАльфа: {alpha}")
print(f"Количество выбранных признаков: {len(selected_features)}")
print(f"MSE: {mse:.4f}")
print(f"R-квадрат: {r2:.4f}")
# Обучаем оптимальную модель (на основе метрики R-квадрат)
best_model = max(results, key=lambda x: x['r2'])
best_alpha = best_model['alpha']
print(f"\nАльфа оптимальной модели: {best_alpha}")
best_lasso = Lasso(alpha=best_alpha, random_state=42)
best_lasso.fit(X_train, y_train)
# Выводим на графике значимость признаков для оптимальной модели
coefs = np.abs(best_lasso.coef_)
plt.figure(figsize=(12, 6))
plt.bar(feature_names[coefs > 20000], coefs[coefs > 20000])
plt.title(f'Значимость признаков (регрессия лассо, альфа={best_alpha})')
plt.xlabel('Признаки')
plt.ylabel('|Коэффициент|')
plt.xticks(rotation=90)
plt.tight_layout()
plt.show()
Техники отбора признаков 509
# Выводим на графике количество выбранных признаков в сравнении с параметром альфа
num_features = [len(result['selected_features']) for result in results]
plt.figure(figsize=(10, 6))
plt.plot(alphas, num_features, marker='o')
plt.title('Количество выбранных признаков в сравнении с параметром альфа')
plt.xlabel('Альфа')
plt.ylabel('Количество выбранных признаков')
plt.xticks(alphas)
plt.grid(True)
plt.show()
Вывод:
Альфа: 0.1
Количество выбранных признаков: 244
MSE: 829852897.9717
R-квадрат: 0.8918
Альфа: 0.5
Количество выбранных признаков: 229
MSE: 828894029.8512
R-квадрат: 0.8919
Альфа: 1.0
Количество выбранных признаков: 225
MSE: 827019937.9888
R-квадрат: 0.8922
Альфа: 3.0
Количество выбранных признаков: 209
MSE: 820462062.9929
R-квадрат: 0.8930
Альфа: 5.0
Количество выбранных признаков: 208
MSE: 815965068.5279
R-квадрат: 0.8936
Альфа: 8.0
Количество выбранных признаков: 201
MSE: 812971651.1326
R-квадрат: 0.8940
Альфа: 10.0
Количество выбранных признаков: 189
MSE: 812735307.0934
R-квадрат: 0.8940
Альфа: 15.0
Количество выбранных признаков: 179
MSE: 808826567.1131
R-квадрат: 0.8946
Альфа: 20.0
Количество выбранных признаков: 172
MSE: 807095545.0247
R-квадрат: 0.8948
Альфа оптимальной модели: 20.0
510 Методы снижения размерности
Рис. 9.9 Значимость признаков (регрессия лассо)
Рис. 9 .10 Количество выбранных признаков
в зависимости от параметра альфа
Техники отбора признаков 511
Что мы здесь сделали?
1. Загрузка и предобработка данных:
• загрузили набор данных house_prices из модуля sklearn.datasets ,
оставили в нем только нужные столбцы, закодировали категориаль-
ные столбцы с одним активным состоянием, разбили набор данных
на обучающую и тестовую выборки и стандартизировали числовые
признаки.
2. Реализация модели регрессии лассо:
• создали список alphas со всеми значениями параметра альфа для
регрессии лассо;
• для каждого значения альфа создали и обучили модель и вычислили
значимость признаков, оставив только признаки с коэффициентом
больше нуля;
• сделали предсказания;
• рассчитали метрики и добавили их в общий список;
• вывели информацию о полученных метриках.
3. Нахождение оптимальной модели:
• извлекли модель с наибольшим значением метрики R-квадрат;
• обучили модель с оптимальным значением параметра альфа.
4. Визуализация:
• при помощи столбчатой диаграммы вывели наиболее значимые
признаки (с коэффициентом, превышающим 20 000);
• при помощи линейной диаграммы показали изменение количества
предикторов при увеличении значения параметра альфа.
5. Интерпретация:
• оценив результаты и графики, мы можем сделать вывод о наиболее
значимых признаках и о том, как степень регуляризации влияет на
отбор признаков и качество модели, а также сделать выбор в поль-
зу того или иного значения параметра альфа для достижения при-
емлемого компромисса между простотой модели, т. е . количеством
признаков в ней, и качеством.
9.2.4. Ключевые выводы о техниках отбора
признаков
Процедуре отбора признаков отводится важнейшее место в конвейере задач
машинного обучения. На этом этапе вы можете внести существенный вклад
в повышение качества модели и ее интерпретируемости, а также снизить
потенциальный риск переобучения. Давайте финально пройдемся по всем
трем категориям методов отбора признаков и закрепим знания об их пре-
имуществах и недостатках.
1. Методы фильтрации: это наиболее эффективные методы с точки зре-
ния требований к ресурсам:
512 Методы снижения размерности
• преимущества: простота реализации, независимость от конкретной
модели, масштабируемость для работы с большими наборами дан-
ных;
• недостатки: могут не учитывать сложные факторы взаимодействия
между переменными и их связь с целевой переменной;
• примеры: корреляционный анализ, критерий хи-квадрат, взаимная
информация.
2. Оберточные методы: эти методы используют знания о конкретной
модели для оценки значимости наборов предикторов:
• преимущества: учитывают сложные факторы взаимодействия между
переменными и оптимизируются под конкретную модель;
• недостатки: высокая вычислительная сложность, особенно при ра-
боте с большими наборами данных;
• примеры: рекурсивное исключение признаков (RFE), а также методы
последовательного добавления и исключения признаков.
3. Встроенные методы: эти методы включают технику отбора признаков
непосредственно в процесс обучения модели:
• преимущества: позволяют добиться компромисса между вычисли-
тельной сложностью и качеством модели;
• недостатки: зависят от конкретной выбранной модели и не очень
хорошо обобщаются на другие алгоритмы;
• примеры: регрессия лассо, техники на основе деревьев и градиент-
ный бустинг.
Для достижения наилучшего результата вы можете комбинировать разные
техники отбора признаков на разных этапах обработки данных. Это позво-
лит по максимуму воспользоваться их сильными сторонами и нивелировать
недостатки.
Помните, что отбор признаков представляет собой итеративную проце-
дуру, в связи с чем важно использовать кросс-валидацию для оценки значи-
мости признаков и выполнять переоценку при поступлении новых данных.
9.3. Практические упражнения
Теперь приступим к выполнению заданий – последних в этой книге.
Упражнение 1. Метод установки порога дисперсии
Есть набор данных с несколькими переменными:
from sklearn.feature_selection import VarianceThreshold
import pandas as pd
# Простой набор данных с переменными с низкой дисперсией
data = {'Feature1': [1, 1, 1, 1, 1],
Практические упражнения 513
'Feature2': [0.5, 0.5, 0.5, 0.5, 0.5],
'Feature3': [0, 1, 0, 1, 0],
'Feature4': [1, 2, 3, 4, 5]}
df = pd.DataFrame(data)
Ваша задача состоит в том, чтобы с помощью метода установки порога
дисперсии отсечь признаки с дисперсией ниже 0.1 .
Решение
selector = VarianceThreshold(threshold=0.1)
reduced_data = pd.DataFrame(selector.fit_transform(df), columns = selector.get_feature_
names_out())
print("Сокращенный набор данных с признаками с высокой дисперсией:")
print(reduced_data)
Вывод:
Сокращенный набор данных с признаками с высокой дисперсией:
Feature3 Feature4
0
0.0
1.0
1
1.0
2.0
2
0.0
3.0
3
1.0
4.0
4
0.0
5.0
Упражнение 2. Метод установки порога корреляции
Есть набор данных с несколькими переменными:
# Простой набор данных с коррелирующими переменными
data = {'Feature1': [1, 2, 3, 4, 5],
'Feature2': [2, 4, 6, 8, 10], # Высокая корреляция с Feature1
'Feature3': [5, 3, 6, 2, 1],
'Feature4': [100, 12, 15, 20, 25]}
df = pd.DataFrame(data)
Ваша задача состоит в том, чтобы с помощью метода установки порога
корреляции найти все пары переменных с корреляцией выше порога 0.8
и удалить по одному столбцу из этих пар.
Решение
correlation_matrix = df.corr()
threshold = 0.8
corr_features = set()
for i in range(len(correlation_matrix.columns)):
for j in range(i):
if abs(correlation_matrix.iloc[i, j]) > threshold:
colname = correlation_matrix.columns[i]
corr_features.add(colname)
# Удаляем коррелирующие столбцы
df_reduced = df.drop(columns=corr_features)
514 Методы снижения размерности
print("Сокращенный набор данных после удаления коррелирующих признаков:")
print(df_reduced)
Вывод:
Сокращенный набор данных после удаления коррелирующих признаков:
Feature1 Feature3 Feature4
0
1
5
100
1
2
3
12
2
3
6
15
3
4
2
20
4
5
1
25
Упражнение 3. Метод рекурсивного исключения признаков
(RFE)
Загрузите набор данных, посвященный видам цветка ириса:
from sklearn.feature_selection import RFE
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris
# Загружаем набор данных Iris
X, y = load_iris(return_X _y=True, as_frame=True)
Ваша задача состоит в том, чтобы с помощью метода RFE совместно с мо-
делью логистической регрессии обнаружить два наиболее значимых при-
знака.
Решение
model = LogisticRegression(max_iter=200)
rfe = RFE(model, n_features_to_select=2)
X_rfe = rfe.fit_transform(X, y)
print("Выбранные признаки после применения метода RFE:", rfe.get_feature_names _out())
print("Ранжирование признаков:", rfe.ranking_)
Вывод:
Выбранные признаки после применения метода RFE: ['petal length (cm)' 'petal width (cm)']
Ранжирование признаков: [3 2 1 1]
Упражнение 4. Реализация анализа главных компонент
для снижения размерности
Здесь мы снова воспользуемся набором данных, посвященным видам цветка
ириса:
from sklearn.decomposition import PCA
import pandas as pd
import matplotlib.pyplot as plt
# Загружаем набор данных Iris
Практические упражнения 515
iris = load_iris()
X = iris.data
y = iris.target
Ваша задача состоит в том, чтобы с помощью анализа главных компонент
сократить количество измерений в наборе данных до двух и визуализиро-
вать результат с помощью графика.
Решение
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X)
# Преобразуем результаты в датафрейм
df_pca = pd.DataFrame(data=X_pca, columns=['PC1', 'PC2'])
df_pca['target'] = y
# Выводим результаты на графике
plt.figure(figsize=(8, 6))
for label in df_pca['target'].unique():
subset = df_pca[df_pca['target'] == label]
plt.scatter(subset['PC1'], subset['PC2'], label=iris.target_names[label])
plt.xlabel('Главная компонента 1')
plt.ylabel('Главная компонента 2')
plt.title('Анализ главных компонент на наборе данных Iris')
plt.legend()
plt.show()
Решение
Рис. 9.11 Анализ главных компонент на наборе данных Iris
516 Методы снижения размерности
9.4. Возможные проблемы
В этом разделе мы посмотрим, какие скрытые угрозы могут поджидать вас
при использовании техник снижения размерности данных.
9.4.1. Удаление слишком большого количества
признаков
Процесс отбора признаков способен существенно улучшить качество модели,
но стоит знать меру, поскольку чрезмерно усердное исключение предикто-
ров может привести к недообучению модели и снижению ее способности
обнаруживать шаблоны в данных.
Что может пойти не так:
модель может утратить способность к обобщению на новых данных,
в результате чего снизится качество предсказаний;
если при исключении признаков ориентироваться только на диспер-
сию или корреляцию столбцов без применения знаний о предметной
области, можно случайно удалить очень важные для модели признаки.
Решение:
оценивайте качество модели после каждого шага снижения размер-
ности и не забывайте пользоваться кросс-валидацией для получения
достоверных результатов;
совмещайте использование автоматизированных средств снижения
размерности с применением знаний о конкретной предметной об-
ласти, чтобы иметь возможность сохранить значимые признаки, даже
если статистические показатели говорят об их избыточности.
9.4.2. Опасности использования только методов
фильтрации
Методы фильтрации полагаются исключительно на статистические метрики,
такие как дисперсия и корреляция, и не зависят от конкретной модели, что
может приводить к пропуску важных взаимодействий между переменны-
ми. К примеру, значимые признаки могут обладать небольшой собственной
дисперсией, но вносить ощутимый вклад в модель в комбинации с другими
переменными.
Что может пойти не так:
модель может игнорировать важные зависимости между предиктора-
ми, что может приводить к снижению качества предсказаний;
Возможные проблемы 517
при использовании методов фильтрации в модели могут оставаться
избыточные признаки, которые могут обладать статистической зна-
чимостью, но при этом не нести полезной информации для модели.
Решение:
используйте методы фильтрации в совокупности с оберточными
и встроенными методами для учета всех важных зависимостей;
тщательно анализируйте оставленные признаки на предмет их вклада
в качество модели.
9.4.3. Утечка информации при использовании
оберточных методов
Оберточные методы оценивают значимость признаков на основе качества
конкретной выбранной модели, что может приводить к непреднамеренной
утечке информации в случаях, когда данные будущих периодов используют-
ся в процессе отбора переменных. В результате вы можете получить хорошее
качество модели в процессе обучения, но при этом она может оказаться ли-
шена способности должным образом обобщать новые данные.
Что может пойти не так:
модель может показывать приемлемое качество на тестовых данных
с использованием кросс-валидации, но плохо приспосабливаться
к боевым условиям в отсутствие доступа к будущим данным;
оберточные методы могут улавливать шум и принимать его за значи-
мые эффекты, особенно при работе с небольшими наборами данных,
что может негативно сказываться на обобщающей способности мо-
дели.
Решение:
при работе с временными рядами убедитесь в том, что кросс-валидация
и обучение модели выполняются без риска утечки информации;
с осторожностью используйте оберточные методы на маленьких на-
борах данных и применяйте методы последовательного добавления
и исключения признаков для оценки влияния на качество модели каж-
дого предиктора.
9.4.4. Чрезмерные штрафы при использовании
встроенных методов
Встроенные методы, такие как регрессия лассо, осуществляют снижение
размерности путем наложения штрафа на менее значимые признаки, но
перебор с величиной штрафа может приводить к исключению из модели
518 Методы снижения размерности
важных предикторов. В наборах данных с ограниченной информацией ре-
гуляризация может приводить к чрезмерному упрощению модели с риском
ее недообучения.
Что может пойти не так:
регрессия лассо и подобные ей методы могут исключать из модели
признаки, вносящие существенный вклад в модель, особенно при рабо-
те с зашумленными данными или при наличии сильно коррелирующих
переменных;
важным признакам могут быть выданы нулевые коэффициенты, что
говорит о возможности пропуска незначительных, но ценных шабло-
нов в данных.
Решение:
управляйте мощностью регуляризации при помощи плавной настрой-
ки параметров (в регрессии лассо это параметр альфа) и используйте
кросс-валидацию для проверки качества модели на каждом шаге;
рассмотрите вариант использования метода эластичной сети, пред-
ставляющего собой комбинацию L1- и L2-регуляризаций, в случае по-
явления больших штрафов.
9.4.5. Ошибки при интерпретации главных
компонент
Анализ главных компонент призван приводить набор данных к меньшей
размерности, но интерпретация новых размерностей может оказаться не-
простым делом. Компоненты представляют собой лишь комбинации исход-
ных признаков, что затрудняет их понимание в контексте конкретной пред-
метной области.
Что может пойти не так:
без понимания того, как итоговые компоненты соотносятся с исход-
ными признаками, невозможно адекватно проинтерпретировать полу-
ченные данные меньшей размерности;
модели могут утрачивать интерпретируемость, особенно в приложени-
ях, требующих четких пояснений в отношении предсказаний (напри-
мер, в области здравоохранения и финансов).
Решение:
изучите долю объясненной дисперсии для каждой новой компоненты
для понимания того, сколько ценной информации они в себе несут.
Это может помочь при определении значимости главных компонент;
используйте анализ главных компонент только на этапе исследова-
ния данных или их подготовки, а если в процессе моделирования вам
важна интерпретируемость результатов, применяйте дополнительные
методики.
Заключение 519
9.4.6. Избыточность данных при использовании
техник отбора признаков
При комбинировании различных методов отбора признаков может возник-
нуть избыточность в отношении переменных, если похожие по важности
признаки оказываются в приоритете разных методов. К примеру, методы
фильтрации и оберточные методы могут указывать на сохранение предикто-
ров с высокой дисперсией, что может приводить к дублированию информа-
ции без существенной прибавки в качестве предсказаний.
Что может пойти не так:
сохранение избыточных переменных может вести к повышению вре-
мени вычисления без улучшения модели и потенциальному появле-
нию мультиколлинеарности;
чрезмерная избыточность данных может стать причиной появления
очень раздутой модели со сложностями в интерпретации и поддержке.
Решение:
оценивайте набор переменных после применения каждого метода на
предмет наличия избыточных или сильно коррелирующих друг с дру-
гом предикторов;
воспользуйтесь иерархическим подходом к отбору признаков (напри-
мер, примените сначала метод фильтрации, а затем оберточный ме-
тод) для создания оптимального набора предикторов.
Заключение
В этой главе мы познакомились с разнообразными техниками, направлен-
ными на снижение размерности данных и отбор признаков, занимающими
важное место в процедуре обработки больших наборов данных. Эти техники
ставят целью упрощение итоговой модели и повышение ее качества с сохра-
нением большей части исходной информации.
Начали мы с обсуждения анализа главных компонент – широко распро-
страненного метода снижения размерности, позволяющего преобразовать
исходные признаки в новые измерения, называемые главными компонен-
тами, совокупно объясняющие большую часть дисперсии в данных.
Далее мы рассмотрели три основные категории техник отбора признаков:
методы фильтрации, оберточные и встроенные методы. Методы фильтрации
направлены на оценку важности признаков исходя из их статистических
свойств безотносительно конкретных моделей. Оберточные методы бази-
руются на проверке разных подмножеств признаков применительно к кон-
кретной модели, а встроенные методы включают технику отбора признаков
непосредственно в процесс обучения модели.
Контрольный опрос.
Часть III. Очистка
и предобработка данных
Приведенные ниже вопросы помогут вам вспомнить все самое важное, что
было пройдено в третьей части книги. Ответьте на вопросы, чтобы убедиться,
что вы хорошо усвоили пройденный материал.
Вопрос 1. Какой из перечисленных методов позволит эффективно уда-
лить признаки с очень низкой дисперсией?
a) рекурсивное исключение признаков (RFE);
b) метод установки порога дисперсии;
c) анализ главных компонент;
d) регрессия лассо.
Вопрос 2. Какая техника может оказаться наиболее приемлемой для
подстановки пропущенных значений при работе с временными ряда-
ми?
a) удаление всех строк с пропущенными датами;
b) заполнение пропущенных дат средними значениями;
c) переиндексация данных к обычной частоте и использование метода
прямого или обратного заполнения;
d) замена пропущенных дат на константу.
Вопрос 3. Что представляет собой доля объясненной дисперсии в ана-
лизе главных компонент?
a) дисперсия набора данных, объясняемая каждой исходной переменной;
b) количество компонент, выбранных для достижения 100 % объясненной
дисперсии;
c) доля дисперсии в наборе данных, объясненная каждой главной компо-
нентой;
d) общая дисперсия преобразованных данных.
Вопрос 4. В каком из предложенных сценариев может пригодиться цик-
лическое кодирование (синусное или косинусное преобразование)?
a) кодирование данных о дневных продажах;
b) кодирование категориальных переменных, таких как категория това-
ров;
Контрольный опрос. Часть III. Очистка и предобработка данных 521
c) кодирование переменных с циклическими составляющими, такими
как день недели;
d) кодирование идентификаторов пользователей.
Вопрос 5. Какая из перечисленных ниже характеристик НЕ соответству-
ет методам фильтрации?
a) ранжируют признаки независимо от конкретной модели;
b) являются эффективными с точки зрения вычислений;
c) полагаются на процесс обучения модели при определении значимости
признаков;
d) используют такие метрики, как дисперсия и корреляция, для отбора
признаков.
Вопрос 6. Какая из перечисленных техник снижения размерности ис-
пользует линейные преобразования для создания новых осей, объяс-
няющих дисперсию в данных?
a) линейный дискриминантный анализ;
b) анализ главных компонент;
c) рекурсивное исключение признаков (RFE);
d) регрессия лассо.
Вопрос 7. Если две переменные в наборе данных обладают корреляци-
ей, близкой к единице, какая техника позволит снизить избыточность
данных без потери критически важной информации?
a) рекурсивное исключение признаков (RFE);
b) метод установки порога дисперсии;
c) метод установки порога корреляции;
d) регрессия лассо.
Вопрос 8. В каком случае оберточные методы отбора признаков могут
оказаться более эффективными в сравнении с методами фильтрации?
a) когда во главе угла стоит вычислительная эффективность алгоритма;
b) при работе с набором данных, содержащим сильно коррелирующие
переменные;
c) при необходимости учесть факторы взаимодействия между перемен-
ными;
d) когда ранжирование признаков полагается только на статистические
свойства.
Вопрос 9. Каков главный недостаток регрессии лассо при отборе пере-
менных?
a) в результате его использования ни один признак не может покинуть
модель;
b) его нельзя комбинировать с другими методами отбора признаков;
c) он может чрезмерно штрафовать признаки, что может приводить к пе-
реобучению модели;
d) он не способен приводить к нулю коэффициенты незначимых при-
знаков.
522 Контрольный опрос. Часть III. Очистка и предобработка данных
Вопрос 10. Почему рекомендуется использовать кросс-валидацию при
применении оберточных методов отбора признаков, таких как RFE,
работая с небольшими наборами данных?
a) для максимизации фактора взаимодействия между переменными;
b) чтобы убедиться, что выбранные признаки хорошо обобщаются на
новых данных;
c) во избежание утечки информации;
d) для повышения вычислительной эффективности.
Ответы
Вопрос 1
Правильный ответ – b: метод установки порога дисперсии.
Вопрос 2
Правильный ответ – c: переиндексация данных к обычной частоте и исполь-
зование метода прямого или обратного заполнения.
Вопрос 3
Правильный ответ – c: доля дисперсии в наборе данных, объясненная каждой
главной компонентой.
Вопрос 4
Правильный ответ – c: кодирование переменных с циклическими составля-
ющими, такими как день недели.
Вопрос 5
Правильный ответ – c: полагаются на процесс обучения модели при опреде-
лении значимости признаков.
Вопрос 6
Правильный ответ – b: анализ главных компонент.
Вопрос 7
Правильный ответ – c: метод установки порога корреляции.
Вопрос 8
Правильный ответ – c: при необходимости учесть факторы взаимодействия
между переменными.
Вопрос 9
Правильный ответ – c: он может чрезмерно штрафовать признаки, что может
приводить к переобучению модели.
Вопрос 10
Правильный ответ – b: чтобы убедиться, что выбранные признаки хорошо
обобщаются на новых данных.
Заключение
Вот и подошло к концу наше увлекательное путешествие в мир инженерии
данных. Если вы прочитали эту книгу полностью, то, безусловно, обрели все
необходимые знания для осуществления очистки данных, их структуриро-
вания, преобразования и подготовки для дальнейшего анализа при помощи
методов машинного обучения. Полученные навыки пригодятся вам при ра-
боте с любыми наборами данных – от самых маленьких до огромных.
Наука о данных подразумевает постоянное обучение, ведь каждый набор
данных характеризуется собственными уникальными сложностями, решать
которые вы можете самыми разными методами, большинство из которых
описаны в этой книге.
Если кратко возвращаться к содержанию книги, то сначала мы познако-
мились с инструментарием, который необходимо знать каждому аналитику
данных для решения задач, связанных с подготовкой данных. В частности,
мы рассмотрели множество примеров применения библиотек Pandas, NumPy
и Scikit-learn. Далее мы перешли к вопросам конструирования признаков
при помощи кодирования и преобразования существующих данных. После
этого мы подробно поговорили о всех существующих техниках подстановки
пропущенных значений. Затем остановились на вопросах масштабирования
и нормализации признаков, а также подробно поговорили о кодировании
категориальных переменных. В большом проекте мы скрупулезно разобрали
все аспекты прогнозирования временных рядов. А завершили книгу важной
темой, касающейся снижения размерности.
Надеемся, что в этой книге вы нашли ответы на все интересующие вас
вопросы.
Предметный указатель
A
ACF, 413
adfuller(), 402
agg(), 49, 83
AIC, 404
Apache Spark, 234
apply(), 101, 298
ARIMA, 372, 406, 414
ARMA, 411
asfreq(), 385
astype(), 43
AutoReg, 403
axis, 120
B
best_estimator_, 161
bfill(), 380
C
category, 43
clip(), 264
ColumnTransformer, 53
compute(), 234
corr(), 151
cumsum(), 42
D
Dask, 230
datetime, 179
describe(), 35, 93
diff(), 390
drop(), 385
drop_duplicates(), 459
dt, 338
dtype, 116
duplicated(), 459
F
ffill(), 35
fillna(), 35, 321
fit_transform(), 53
from_pandas(), 234
G
get_feature_names_out(), 294
GridSearchCV, 160, 437
groupby(), 83
H
head(), 35
I
info(), 108
inverse_transform(), 308
isin(), 59
IterativeImputer, 210
K
KNNImputer, 204
L
LabelEncoder, 154
linalg, 28
lower(), 463
M
MAE, 158
map(), 321
map_partitions(), 234
MAR, 225
Matplotlib, 36
MCAR, 225
Предметный указатель 525
mean_absolute_error(), 159
melt(), 84, 96
memory_usage(), 36
MICE, 208
MinMaxScaler, 155, 256
MLlib, 234
MNAR, 225
multiprocessing, 417
N
NaN, 55
NaT, 452
np.argmax(), 120
np.cbrt(), 276
np.corrcoef(), 121
np.cos, 179
np.exp(), 114, 334
np.log(), 61, 114, 155, 269
np.mean(), 114
np.median(), 114
np.nansum(), 55
np.sin, 179
np.sqrt(), 114, 273
np.std(), 114
NumPy, 18, 27
O
OneHotEncoder, 53, 238
OrdinalEncoder, 321
P
PACF, 413
Pandas, 18, 26
PCA, 302, 483
pct_change(), 42
pd.cut(), 188, 338
pd.DataFrame(), 40
pd.date_range(), 105, 400
pd.get_dummies(), 153, 294
pd.to_datetime(), 179
pd.to_numeric(), 108
Pipeline, 238
pivot(), 96
pivot_table(), 96
plot_acf(), 414
plot_pacf(), 414
PolynomialFeatures, 343
Pool, 417
PowerTransformer, 279
R
r2_score(), 159
R-квадрат, 158
RandomForestClassifier, 68
RandomForestRegressor, 158
RandomizedSearchCV, 439
reindex(), 400
resample(), 59
reset_index(), 50, 386
RFE, 162, 427
RobustScaler, 264
rolling(), 36, 379
S
SARIMA, 419
SARIMAX, 421
Scikit-learn, 18, 66
scipy.stats.ttest_ind(), 89
select_dtypes(), 228
set_index(), 385
SHAP-значение, 363
shift(), 375
SimpleImputer, 52
SparkSession, 237
StandardScaler, 52, 258
stats.zscore(), 63
str, 463
StringIndexer, 238
T
time.time(), 112
toarray(), 308
to_numeric(), 36
to_numpy(), 55
train_test_split(), 68, 157
transform(), 318
t-SNE, 302
tz_convert(), 383
tz_localize(), 383
U
unstack(), 105
V
VarianceThreshold, 497
526 Предметный указатель
W
withColumn(), 237
X
XGBoost, 432
Z
Z-нормализация, 256
Z-оценка, 63
А
Абсолютно случайно пропущенные
значения, 225
Автокорреляция, 374
Авторегрессионная модель, 403
Авторегрессионное интегрированное
скользящее среднее, 372
Анализ главных компонент, 302, 477
Б
Блок данных, 229
В
Векторизованная операция, 113
Винсоризация, 264
Временная зависимость, 374
Встроенный метод, 506
Выделение сезонности, 395
Вычисление важности признаков, 363
Г
Генетический алгоритм, 29
Гиперпараметр, 159
Главная компонента, 302, 478
Глубокий синтез признаков, 29
Градиентный спуск, 251
Д
Детрендирование, 386
на основе разностей, 387
на основе регрессии, 387
на основе скользящего среднего, 387
Доля объясненной дисперсии, 487
Дробно-степенное преобразование, 335
И
Интерполяция, 36
Информационный критерий
Акаике, 404
К
Кардинальность, 296
Квадратическое преобразование, 334
Кодирование, 152
на основе целевой переменной, 189,
300
на основе частоты, 298
по меткам, 152
по среднему, 300
с одним активным состоянием, 152,
291
Конвейер, 50
данных, 30
Конструирование признаков, 17
Косинусное кодирование, 382
Кубическое преобразование, 334
Кусочная функция, 342
Л
Логарифмическое преобразование, 268
Локально-чувствительное
хеширование, 218
М
Масштабирование, 250
Межквартильный размах, 261
Метод
градиентного бустинга, 431
множественного восстановления
пропущенных данных, 37
множественной подстановки
с помощью цепных уравнений, 208
нелинейного снижения размерности
и визуализации многомерных
переменных, 302
последовательного добавления
признаков, 502
последовательного исключения
признаков, 502
случайного поиска, 438
степенных итераций, 481
установки
порога дисперсии, 495
порога корреляции, 496
фильтрации, 494
Предметный указатель 527
экстремального градиентного
бустинга, 432
k-ближайших соседей, 203
Минимаксное масштабирование, 253
Модель
авторегрессии и скользящего
среднего, 411
скользящего среднего, 409
Мультиколлинеарность, 198
Н
Неслучайно пропущенные
значения, 225
Нормализация, 155, 250
О
Оберточный метод, 501
Обратное заполнение, 36
П
Партиция, 229
Перекрестный признак, 345
Переменная взаимодействия, 156, 342
Переобучение, 78
Подбор гиперпараметров, 435
Поиск по сетке, 436
Порядковое кодирование, 318
Преобразование
Бокса–Кокса, 276
Йео–Джонсона, 276
квадратного корня, 270
кубического корня, 273
Признак, 17
на основе временного лага, 373
на основе временных рядов, 176
на основе скользящего окна, 377
Прогнозирование временных
рядов, 372
Проклятие размерности, 153
Прямое заполнение, 36
Р
Разбиение числовых переменных
на интервалы, 185
Разложение по собственным
значениям, 481
Разреженная матрица, 305
Регрессия лассо, 506
Рекурсивное исключение
признаков, 427, 502
Робастное масштабирование, 261
С
Сглаживание, 312
Сезонность, 394
Сезонный признак, 396
Сингулярное разложение, 481
Синусное кодирование, 382
Скользящее среднее, 40
Случайно пропущенные значения, 225
Снижение размерности, 477
Собственное значение, 480
Собственный вектор, 480
Средняя абсолютная ошибка, 158
Стандартизация, 256
Т
Техники отбора признаков, 494
Транслирование, 27, 114
У
Удаление тренда, 386
Универсальная функция, 27
Утечка информации, 78, 198
Ф
Фактор инфляции дисперсии, 198
Функция автокорреляции, 413
Ц
Целевероятностное кодирование, 300
Циклическое кодирование, 396
Ч
Частичная функция
автокорреляции, 413
Э
Экспоненциальное сглаживание, 372
Инженерия данных в Python
Главный редактор Мовчан Д. А.
Зам. главного редактора Яценков В. С.
editor@dmkpress.com
Перевод Гинько А. Ю .
Корректор Синяева Г. И .
Верстка Чаннова А. А.
Дизайн обложки Мовчан А. Г .
Гарнитура PT Serif. Печать цифровая.
Усл. печ. л. 42,9. Тираж 200 экз.
Веб-сайт издательства: www.dmkpress.com
Книги издательства «ДМК Пресс»
можно купить оптом и в розницу на складе издательства по адресу:
Москва, ул. Электродная, д. 2, стр. 12, офис 7, тел. +7 (499) 322 -19-38,
а также заказать на сайте www.dmkpress.com
с доставкой в любой регион РФ