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 с доставкой в любой регион РФ