7 полезных операций в Pandas при работе с DataFrame
Абстракция датафрейма является одной из наиболее полезных концепций в современной экосистеме управления данными. Вращается она главным образом вокруг табличных структур, которые имеют повышенную производительность при обновлении и запросе данных различными способами. Сериализация/десериализация этих структур из/в различные форматы файлов упрощает работу с данными. Более того, возможность производить различные SQL-подобные операции, такие как объединение, наряду с выполнением математических вычислений в самом датафрейме существенно расширяет возможности программиста.
Эта статья подчеркивает некоторые наиболее полезные операции, которые можно выполнять с помощью абстракции датафрейма. Реализовывать мы их будем через библиотеку Pandas. Постараюсь представить материал в интуитивно понятной форме, чтобы в дальнейшем вы могли применить эти знания в других случаях или при работе с другими фреймворками.
1. Конкатенация DataFrame
Есть два способа конкатенировать датафрейм A и B . Представьте их как проиндексированные таблицы. Эти две таблицы можно объединить либо по оси y, либо по оси x.
Если требуется конкатенировать их вдоль x, то вызов API будет таким:
Если же вдоль y, то таким:
Применение
Предположим, у вас есть большое количество CSV-файлов или XLSX-данных, которые нужно присоединить друг к другу. Одним из способов сделать это будет считать данные файлов в датафреймы и использовать инструкцию pd.concat([file_1, file_2]) . Программно можно перебрать имена файлов (используя модуль glob для чтения набора имен файлов с помощью техники сопоставления шаблонов, например regex), считать их в датафрейм, соединить в памяти и сериализовать конкатенированные датафреймы в нужный формат файлов.
Вариант с axis = 0 используется нечасто, но его можно применять в сценариях, когда нужно обработать массивы данных, собранных с упорядочиванием. То есть, когда последовательность данных соответствует последовательности других массивов данных. В таком случае эти массивы можно объединить вдоль оси x, получив более объемное и значительное представление в табличном формате. Затем к полученной структуре можно применять операции, использующие все типы данных в ее столбцах.
2. Разделение DataFrame
Датафрейм можно разделить множеством способов, и выбор техники полностью зависит от цели этого разделения. Рассмотрим ряд случаев.
Просмотр сведений
В некоторых сценариях, особенно при написании кода с помощью блокнотов (например Jupyter), мы заглядываем в датафрейм, только чтобы понять, как он выглядит. В таких случаях можно использовать метод head() .
Исключение столбцов
Этот метод разделяет датафрейм вдоль оси y, то есть просто выбрасывает из него часть столбцов. Используется данный метод в типичном сценарии, когда нам не нужно, чтобы конечный DataFrame содержал эти столбцы, или когда мы предполагаем, что при дальнейшем обновлении структура станет занимать слишком много памяти.
Удаление датафреймов друг из друга
Представим, что у нас есть датафрейм X , состоящий из столбцов [A, B, C, D] , и датафрейм Y , состоящий из подмножества столбцов X . Нам нужно удалить Y из X . Это можно сделать так:
Конкатенация этих датафреймов приведет к дублированию общих записей, которые в итоге будут удалены выражением keep = false функции drop_duplicates() .
Применение
Предположим, что столбец A — это определенный вид ID сведений о работнике. К примеру, датафрейм X состоит из всех данных о работниках, а датафрейм Y содержит данные (с той же структурой) о работниках, не разбирающихся в Python. Нам нужно отфильтровать сведения о сотрудниках, которые не знакомы с Python.
Определение дельты записей на основе столбца
Представим, что у нас есть датафрейм X , состоящий из столбцов [A, B, C, D] , а также датафрейм Y , состоящий из тех же столбцов. При этом некоторые элементы столбцов A этих датафреймов являются общими. Нам нужно получить из датафрейма X строки, которые не содержат значения из столбца A , находящиеся в столбце A датафрейма Y .
Применение
Взгляните на эту таблицу:
Эти пары могли быть сгенерированы, например, из двух журналов: старого и нового. Нам нужно найти пары ID, принадлежащие одному и тому же человеку. Предположим, что ваш отдел кадров неожиданно заявляет, что определенный список ( hr_list ) сотрудников с ID_1 больше в компании не работает. Как удалить их из этого датафрейма?
Разделение на основе значений столбцов
Датафрейм можно фильтровать на основе значений столбца. В этом случае критерий отбора может включать несколько выражений при условии, что они будут возвращать логические значения.
Это простейший пример.
3. Подсчет записей в столбце
Это эффективный способ определения количества различных элементов в столбце.
Ответ на приведенный выше запрос можно получить следующим подходом:
4. Чтение фрагментов DataFrame
В некоторых случаях будет более эффективно считывать только части датафрейма, особенно при его больших размерах. Обратите внимание, что каждый датафрейм является индексированной табличной структурой, находящейся в памяти, а значит потребляющей пространство, потенциально нужное другим структурам данных. В связи с этим при работе с большими массивами информации всегда лучше считывать только ее нужную часть.
Более того, можно считывать большие файлы в отдельные фрагменты и маршалировать их в датафреймы.
Таким образом одновременно в памяти удерживается только фрагмент размером chunksize .
5. Применение функций к строкам
Бывают случаи, в которых требуется внести изменения в конкретные столбцы детафрейма. К примеру, в датафрейме X , содержащем столбцы A , B и C , мы можем применить функцию f() к значениям столбца B , чтобы сохранить их в столбце D .
Эта операция окажется намного быстрее, чем перебор всего датафрейма с помощью iterrows() .
Есть и альтернативный метод. Его можно использовать, когда функцию f() требуется применить только к одному столбцу.
6. Объединение двух датафреймов
По аналогии с реляционными базами данных датафреймы можно объединять merge , используя разрешающий столбец.
Однако стоит заметить, что операция merge является дорогостоящей, в связи с чем перед слиянием больших датасетов стоит проявлять особое внимание. В случаях, когда датасеты слишком велики, рекомендуется использовать методы группировки (англ.), чтобы избежать перегрузки памяти и связанных с этим проблем производительности.
7. Переименование столбцов
Переименовывать столбцы особенно полезно перед сериализацией файла или перед внедрением стороннего хранилища данных.
Оперирование таблицами pandas #
Считаем таблицу с планетами и выведем типы данных в её столбце.
| Количество спутников | Масса | Группа | Кольца | |
|---|---|---|---|---|
| Название | ||||
| Меркурий | 0 | 0.0055 | земная группа | Нет |
| Венера | 0 | 0.8150 | земная группа | Нет |
| Земля | 1 | 1.0000 | земная группа | Нет |
| Марс | 2 | 0.1070 | земная группа | Нет |
| Юпитер | 62 | 317.8000 | газовый гигант | Да |
| Сатурн | 34 | 95.2000 | газовый гигант | Да |
| Уран | 27 | 14.3700 | ледяной гигант | Да |
| Нептун | 13 | 17.1500 | ледяной гигант | Да |
Видим, что типы первых столбцов были корректно выведены из таблицы, а вот последние два столбца можно хранить более эффективно.
Во-первых, значение столбца “ Кольца ” булевого характера. Чтобы сразу считать их сразу как булевые, в методе read_csv можно указать какие значения следует интерпретировать в качестве True , а какие в качестве False , параметрами true_values и false_values .
По аналогии с true_values и false_values ещё есть параметр na_values , отвечающий за пропущенные значения.
| Количество спутников | Масса | Группа | Кольца | |
|---|---|---|---|---|
| Название | ||||
| Меркурий | 0 | 0.0055 | земная группа | False |
| Венера | 0 | 0.8150 | земная группа | False |
| Земля | 1 | 1.0000 | земная группа | False |
| Марс | 2 | 0.1070 | земная группа | False |
| Юпитер | 62 | 317.8000 | газовый гигант | True |
| Сатурн | 34 | 95.2000 | газовый гигант | True |
| Уран | 27 | 14.3700 | ледяной гигант | True |
| Нептун | 13 | 17.1500 | ледяной гигант | True |
Видим, что значения столбца “ Кольца ” автоматически приняли булевый тип.
Во-вторых, значения столбца “ Группа ” принимают одно из трех значений, а хранятся в столбце типа object . Переменные такого характера называют категориальными или номинальными. Для их хранения в pandas эффективнее будет воспользоваться специальным категориальным типом данных.
Для начала продемонстрируем работу метода pandas.Series.astype, который позволяет преобразовать тип данных столбца (или таблицы)
Видим, что метод преобразовал все к типу "category" . Эффективнее, конечно, загрузить таблицу сразу с необходимыми типами данных.
| Количество спутников | Масса | Группа | Кольца | |
|---|---|---|---|---|
| Название | ||||
| Меркурий | 0 | 0.005500 | земная группа | False |
| Венера | 0 | 0.815000 | земная группа | False |
| Земля | 1 | 1.000000 | земная группа | False |
| Марс | 2 | 0.107000 | земная группа | False |
| Юпитер | 62 | 317.799988 | газовый гигант | True |
| Сатурн | 34 | 95.199997 | газовый гигант | True |
| Уран | 27 | 14.370000 | ледяной гигант | True |
| Нептун | 13 | 17.150000 | ледяной гигант | True |
Названия столбцов#
Иногда может потребоваться переименовать столбцы и метки строк в таблице. Переведем названия столбцов и планет, чтобы продемонстрировать принцип работы метода pd.DataFrame.rename.
| number of moons | mass | group | rings | |
|---|---|---|---|---|
| Название | ||||
| Mercury | 0 | 0.005500 | земная группа | False |
| Venus | 0 | 0.815000 | земная группа | False |
| Earth | 1 | 1.000000 | земная группа | False |
| Mars | 2 | 0.107000 | земная группа | False |
| jupyter | 62 | 317.799988 | газовый гигант | True |
| Saturn | 34 | 95.199997 | газовый гигант | True |
| Uranus | 27 | 14.370000 | ледяной гигант | True |
| Neptune | 13 | 17.150000 | ледяной гигант | True |
Т.е. в большинстве случаев удобнее всего указать новые названия столбцов и/или новые метки индекса в виде словаря.
Обратите внимание, что по умолчанию метод rename не изменяет названия столбцов на месте, а возвращает копию таблицы с переименованными столбцами. Это стандартное поведение для многих методов pandas : в последнее время разработчики pandas стараются избегать мутирующих методов. Однако если талица очень крупная, то её копирование ради совершения простых операций может оказаться накладным. В таком случае полезным может оказаться метод inplace .
Переименовать название индекса таблицы можно методом pandas.Index.rename
Пропущенные значения#
Большинство методов pandas разработаны таким образом, чтобы они работали с пропущенными значениями.
| s1 | s2 | s3 | |
|---|---|---|---|
| a | 1.0 | -1 | NaN |
| b | 2.0 | NaN | NaN |
| c | NaN | NaN | NaN |
Например, метод mean вычисляет среднее каждого столбца с численными значениями.
How can I use the apply() function for a single column?
I have a pandas dataframe with two columns. I need to change the values of the first column without affecting the second one and get back the whole dataframe with just first column values changed. How can I do that using apply() in pandas?
8 Answers 8
Given a sample dataframe df as:
what you want is:
![]()
For a single column better to use map() , like this:
![]()
Given the following dataframe df and the function complex_function ,
there are several solutions to use apply() on only one column. In the following I will explain them in detail.
I. Simple solution
The straightforward solution is the one from @Fabio Lamanna:
Only the first column is modified, the second column is unchanged. The solution is beautiful. It is just one line of code and it reads almost like english: "Take ‘col1’ and apply the function complex_function to it."
However, if you need data from another column, e.g. ‘col2’, it won’t work. If you want to pass the values of ‘col2’ to variable y of the complex_function , you need something else.
II. Solution using the whole dataframe
Alternatively, you could use the whole dataframe as described in this SO post or this one:
or if you prefer (like me) a solution without a lambda function:
There is a lot going on in this solution that needs to be explained. The apply() function works on pd.Series and pd.DataFrame . But you cannot use df[‘col1’] = df.apply(complex_function).loc[:, ‘col1’] , because it would throw a ValueError .
Hence, you need to give the information which column to use. To complicate things, the apply() function does only accept callables. To solve this, you need to define a (lambda) function with the column x[‘col1’] as argument; i.e. we wrap the column information in another function.
Unfortunately, the default value of the axis parameter is zero ( axis=0 ), which means it will try executing column-wise and not row-wise. This wasn’t a problem in the first solution, because we gave apply() a pd.Series . But now the input is a dataframe and we must be explicit ( axis=1 ). (I marvel how often I forget this.)
Whether you prefer the version with the lambda function or without is subjective. In my opinion the line of code is complicated enough to read even without a lambda function thrown in. You only need the (lambda) function as a wrapper. It is just boilerplate code. A reader should not be bothered with it.
Now, you can modify this solution easily to take the second column into account:
At index 4 the value has changed from 1 to 2, because the first condition 7 > 5 is true but the second condition 7 > 8 is false.
Note that you only needed to change the first line of code (i.e. the function) and not the second line.
Side note
Never put the column information into your function.
By doing this, you make a general function dependent on a column name! This is a bad idea, because the next time you want to use this function, you cannot. Worse: Maybe you rename a column in a different dataframe just to make it work with your existing function. (Been there, done that. It is a slippery slope!)
III. Alternative solutions without using apply()
Although the OP specifically asked for a solution with apply() , alternative solutions were suggested. For example, the answer of @George Petrov suggested to use map() ; the answer of @Thibaut Dubernet proposed assign() .
I fully agree that apply() is seldom the best solution, because apply() is not vectorized. It is an element-wise operation with expensive function calling and overhead from pd.Series .
One reason to use apply() is that you want to use an existing function and performance is not an issue. Or your function is so complex that no vectorized version exists.
Another reason to use apply() is in combination with groupby(). Please note that DataFrame.apply() and GroupBy.apply() are different functions.
So it does make sense to consider some alternatives:
map() only works on pd.Series , but accepts dict and pd.Series as input. Using map() with a function is almost interchangeable with using apply() . It can be faster than apply() . See this SO post for more details.
applymap() is almost identical for dataframes. It does not support pd.Series and it will always return a dataframe. However, it can be faster. The documentation states: "In the current implementation applymap calls func twice on the first column/row to decide whether it can take a fast or slow code path.". But if performance really counts you should seek an alternative route.
assign() is not a feasible replacement for apply() . It has a similar behaviour in only the most basic use cases. It does not work with the complex_function . You still need apply() as you can see in the example below. The main use case for assign() is method chaining, because it gives back the dataframe without changing the original dataframe.
Annex: How to speed up apply() ?
I only mention it here because it was suggested by other answers, e.g. @durjoy. The list is not exhaustive:
Do not use apply() . This is no joke. For most numeric operations, a vectorized method exists in pandas. If/else blocks can often be refactored with a combination of boolean indexing and .loc . My example complex_function could be refactored in this way.
Refactor to Cython. If you have a complex equation and the parameters of the equation are in your dataframe, this might be a good idea. Check out the official pandas user guide for more information.
Use raw=True parameter. Theoretically, this should improve the performance of apply() if you are just applying a NumPy reduction function, because the overhead of pd.Series is removed. Of course, your function has to accept an ndarray. You have to refactor your function to NumPy. By doing this, you will have a huge performance boost.
Use 3rd party packages. The first thing you should try is Numba. I do not know swifter mentioned by @durjoy; and probably many other packages are worth mentioning here.
Try/Fail/Repeat. As mentioned above, map() and applymap() can be faster — depending on the use case. Just time the different versions and choose the fastest. This approach is the most tedious one with the least performance increase.
Открытый курс машинного обучения. Тема 1. Первичный анализ данных с Pandas

Открытый курс машинного обучения mlcourse.ai сообщества OpenDataScience – это сбалансированный по теории и практике курс, дающий как знания, так и навыки (необходимые, но не достаточные) машинного обучения уровня Junior Data Scientist. Нечасто встретите и подробное описание математики, стоящей за используемыми алгоритмами, и соревнования Kaggle Inclass, и примеры бизнес-применения машинного обучения в одном курсе. С 2017 по 2019 годы Юрий Кашницкий yorko и большая команда ODS проводили живые запуски курса дважды в год – с домашними заданиями, соревнованиями и общим рейтингом учаcтников (имена героев запечатлены тут).
Курс состоит из:
- 10 статей на Хабре (и то же самое на Медиуме на англ.). Впрочем, статьи на Медиуме и Хабре не обновляются, актуальный материал – на английском на сайте mlcourse.ai
- 10 лекций (Youtube-канал на русском + более свежие лекции на англ.), подробное описание каждой темы – в этой статье
- воспроизводимых материалов (Jupyter notebooks) в репозитории mlcourse.ai и в виде Kaggle Dataset (нужен только браузер)
- отличных соревнований Kaggle Inclass (не на «стаканье xgboost-ов», а на построение признаков)
- демо-версий домашних заданий по каждой теме по каждой теме (только на англ.), доступных по подписке на Patreon («Bonus Assignments» tier)
Список статей серии
План этой статьи
1. О курсе
Мы не ставим себе задачу разработать еще один исчерпывающий вводный курс по машинному обучению или анализу данных (т.е. это не замена специализации Яндекса и МФТИ, дополнительному образованию ВШЭ и прочим фундаментальным онлайн- и оффлайн-программам и книжкам). Цель этой серии статей — быстро освежить имеющиеся у вас знания или помочь найти темы для дальнейшего изучения. Подход примерно как у авторов книги Deep Learning, которая начинается с обзора математики и основ машинного обучения — краткого, максимально ёмкого и с обилием ссылок на источники.
Если вы планируете пройти курс, то предупреждаем: при подборе тем и создании материалов мы ориентируемся на то, что наши слушатели знают математику на уровне 2 курса технического вуза и хотя бы немного умеют программировать на Python. Это не жёсткие критерии отбора, а всего лишь рекомендации — можно записаться на курс, не зная математики или Python, и параллельно навёрстывать:
- базовую математику (математический анализ, линейную алгебру, оптимизацию, теорвер и статистику) можно повторить по этим конспектам Yandex & MIPT (делимся с разрешения). Кратко, на русском – то что надо. Если подробно, то матан – Кудрявцев, линал – Кострикин, оптимизация – Boyd (англ.), теорвер и статистика – Кибзун. Плюс отличные онлайн-курсы МФТИ и ВШЭ на Coursera;
- по Python хватит небольшого интерактивного туториала на Datacamp или этого репозитория по Python и базовым алгоритмам и структурам данных. Что-то более продвинутое – это, например, курс питерского Computer Science Center;
- что касается машинного обучения, то есть классический (но слегка устаревший) курс Andrew Ng «Machine Learning»(Stanford, Coursera). На русском языке есть отличная специализация МФТИ и Яндекса «Машинное обучение и анализ данных». А вот и лучшие книги: «Pattern recognition and Machine Learning» (Bishop), «Machine Learning: A Probabilistic Perspective » (Murphy), «The elements of statistical learning» (Hastie, Tibshirani, Friedman), «Deep Learning» (Goodfellow, Bengio, Courville). Книга Goodfellow начинается с обзора математики и понятного и интересного введения в машинное обучение и внутреннее устройство его алгоритмов. Приятно, что теперь про глубокое обучение есть книга и на русском языке – «Глубокое обучение: погружение в мир нейронных сетей» (Николенко С. И., Кадурин А. А., Архангельская Е. О.).
Также про курс рассказано в этом анонсе.
Какое ПО нужно
Для прохождения курса нужен ряд Python-пакетов, большинство из них есть в сборке Anaconda с Python 3.6. Чуть позже понадобятся и другие библиотеки, об этом будет сказано дополнительно. Полный список можно посмотреть в Dockerfile.
Также можно воспользоваться Docker-контейнером, в котором все необходимое ПО уже установлено. Подробности – на странице Wiki репозитория.
2. Домашние задания в курсе
Каждая статья сопровождается домашним заданием в виде Jupyter Notebook, в который надо дописать код, и на основе этого выбрать правильный ответ в Google-форме. Примеры домашних заданий приведены в статьях серии (в конце).
3. Демонстрация основных методов Pandas
Весь код можно воспроизвести в этом Jupyter notebook. Но актуальная обновляемая версия – только на английском.
Pandas — это библиотека Python, предоставляющая широкие возможности для анализа данных. Данные, с которыми работают датасаентисты, часто хранятся в форме табличек — например, в форматах .csv, .tsv или .xlsx. С помощью библиотеки Pandas такие табличные данные очень удобно загружать, обрабатывать и анализировать с помощью SQL-подобных запросов. А в связке с библиотеками Matplotlib и Seaborn Pandas предоставляет широкие возможности визуального анализа табличных данных.
Основными структурами данных в Pandas являются классы Series и DataFrame. Первый из них представляет собой одномерный индексированный массив данных некоторого фиксированного типа. Второй – это двухмерная структура данных, представляющая собой таблицу, каждый столбец которой содержит данные одного типа. Можно представлять её как словарь объектов типа Series. Структура DataFrame отлично подходит для представления реальных данных: строки соответствуют признаковым описаниям отдельных объектов, а столбцы соответствуют признакам.
Будем показывать основные методы в деле, анализируя набор данных по оттоку клиентов телеком-оператора (скачивать не нужно, он есть в репозитории). Прочитаем данные (метод read_csv ) и посмотрим на первые 5 строк с помощью метода head :

В Jupyter-ноутбуках датафреймы Pandas выводятся в виде вот таких красивых табличек, и print(df.head()) выглядит хуже.
По умолчанию Pandas выводит всего 20 столбцов и 60 строк, поэтому если ваш датафрейм больше, воспользуйтесь функцией set_option :
Каждая строка представляет собой одного клиента – это объект исследования.
Столбцы – признаки объекта.
| Название | Описание | Тип |
|---|---|---|
| State | Буквенный код штата | номинальный |
| Account length | Как долго клиент обслуживается компанией | количественный |
| Area code | Префикс номера телефона | количественный |
| International plan | Международный роуминг (подключен/не подключен) | бинарный |
| Voice mail plan | Голосовая почта (подключена/не подключена) | бинарный |
| Number vmail messages | Количество голосовых сообщений | количественный |
| Total day minutes | Общая длительность разговоров днем | количественный |
| Total day calls | Общее количество звонков днем | количественный |
| Total day charge | Общая сумма оплаты за услуги днем | количественный |
| Total eve minutes | Общая длительность разговоров вечером | количественный |
| Total eve calls | Общее количество звонков вечером | количественный |
| Total eve charge | Общая сумма оплаты за услуги вечером | количественный |
| Total night minutes | Общая длительность разговоров ночью | количественный |
| Total night calls | Общее количество звонков ночью | количественный |
| Total night charge | Общая сумма оплаты за услуги ночью | количественный |
| Total intl minutes | Общая длительность международных разговоров | количественный |
| Total intl calls | Общее количество международных разговоров | количественный |
| Total intl charge | Общая сумма оплаты за международные разговоры | количественный |
| Customer service calls | Число обращений в сервисный центр | количественный |
Целевая переменная: Churn – Признак оттока, бинарный признак (1 – потеря клиента, то есть отток). Потом мы будем строить модели, прогнозирующие этот признак по остальным, поэтому мы и назвали его целевым.
Посмотрим на размер данных, названия признаков и их типы.
Видим, что в таблице 3333 строки и 20 столбцов. Выведем названия столбцов:
Чтобы посмотреть общую информацию по датафрейму и всем признакам, воспользуемся методом info :
bool , int64 , float64 и object — это типы признаков. Видим, что 1 признак — логический (bool), 3 признака имеют тип object и 16 признаков — числовые. Также с помощью метода info удобно быстро посмотреть на пропуски в данных, в нашем случае их нет, в каждом столбце по 3333 наблюдения.
Изменить тип колонки можно с помощью метода astype . Применим этот метод к признаку Churn и переведём его в int64 :
Метод describe показывает основные статистические характеристики данных по каждому числовому признаку (типы int64 и float64 ): число непропущенных значений, среднее, стандартное отклонение, диапазон, медиану, 0.25 и 0.75 квартили.

Чтобы посмотреть статистику по нечисловым признакам, нужно явно указать интересующие нас типы в параметре include .
| State | International plan | Voice mail plan | |
|---|---|---|---|
| count | 3333 | 3333 | 3333 |
| unique | 51 | 2 | 2 |
| top | WV | No | No |
| freq | 106 | 3010 | 2411 |
Для категориальных (тип object ) и булевых (тип bool ) признаков можно воспользоваться методом value_counts . Посмотрим на распределение данных по нашей целевой переменной — Churn :
2850 пользователей из 3333 — лояльные, значение переменной Churn у них — 0 .
Посмотрим на распределение пользователей по переменной Area code . Укажем значение параметра normalize=True , чтобы посмотреть не абсолютные частоты, а относительные.
Сортировка
DataFrame можно отсортировать по значению какого-нибудь из признаков. В нашем случае, например, по Total day charge ( ascending=False для сортировки по убыванию):

Сортировать можно и по группе столбцов:
спасибо за замечание про устаревший sort makkos

Индексация и извлечение данных
DataFrame можно индексировать по-разному. В связи с этим рассмотрим различные способы индексации и извлечения нужных нам данных из датафрейма на примере простых вопросов.
Для извлечения отдельного столбца можно использовать конструкцию вида DataFrame[‘Name’] . Воспользуемся этим для ответа на вопрос: какова доля людей нелояльных пользователей в нашем датафрейме?
14,5% — довольно плохой показатель для компании, с таким процентом оттока можно и разориться.
Очень удобной является логическая индексация DataFrame по одному столбцу. Выглядит она следующим образом: df[P(df[‘Name’])] , где P — это некоторое логическое условие, проверяемое для каждого элемента столбца Name . Итогом такой индексации является DataFrame, состоящий только из строк, удовлетворяющих условию P по столбцу Name .
Воспользуемся этим для ответа на вопрос: каковы средние значения числовых признаков среди нелояльных пользователей?
Скомбинировав предыдущие два вида индексации, ответим на вопрос: сколько в среднем в течение дня разговаривают по телефону нелояльные пользователи?
Какова максимальная длина международных звонков среди лояльных пользователей ( Churn == 0 ), не пользующихся услугой международного роуминга ( ‘International plan’ == ‘No’ )?
Датафреймы можно индексировать как по названию столбца или строки, так и по порядковому номеру. Для индексации по названию используется метод loc , по номеру — iloc .
В первом случае мы говорим «передай нам значения для id строк от 0 до 5 и для столбцов от State до Area code», а во втором — «передай нам значения первых пяти строк в первых трёх столбцах».
| State | Account length | Area code | |
|---|---|---|---|
| 0 | KS | 128 | 415 |
| 1 | OH | 107 | 415 |
| 2 | NJ | 137 | 415 |
| 3 | OH | 84 | 408 |
| 4 | OK | 75 | 415 |
| 5 | AL | 118 | 510 |
| State | Account length | Area code | |
|---|---|---|---|
| 0 | KS | 128 | 415 |
| 1 | OH | 107 | 415 |
| 2 | NJ | 137 | 415 |
| 3 | OH | 84 | 408 |
| 4 | OK | 75 | 415 |
Если нам нужна первая или последняя строчка датафрейма, пользуемся конструкцией df[:1] или df[-1:] :

Применение функций к ячейкам, столбцам и строкам
Применение функции к каждому столбцу: apply
Метод apply можно использовать и для того, чтобы применить функцию к каждой строке. Для этого нужно указать axis=1 .
Применение функции к каждой ячейке столбца: map
Например, метод map можно использовать для замены значений в колонке, передав ему в качестве аргумента словарь вида

Аналогичную операцию можно провернуть с помощью метода replace :

Группировка данных
В общем случае группировка данных в Pandas выглядит следующим образом:
- К датафрейму применяется метод groupby , который разделяет данные по grouping_columns – признаку или набору признаков.
- Выбираем нужные нам столбцы ( columns_to_show ).
- К полученным группам применяется функция или несколько функций.
Группирование данных в зависимости от значения признака Churn и вывод статистик по трём столбцам в каждой группе.
Сделаем то же самое, но немного по-другому, передав в agg список функций:

Сводные таблицы
Допустим, мы хотим посмотреть, как наблюдения в нашей выборке распределены в контексте двух признаков — Churn и International plan . Для этого мы можем построить таблицу сопряженности, воспользовавшись методом crosstab :
| International plan | No | Yes |
|---|---|---|
| Churn | ||
| 0 | 2664 | 186 |
| 1 | 346 | 137 |
| Voice mail plan | No | Yes |
|---|---|---|
| Churn | ||
| 0 | 0.602460 | 0.252625 |
| 1 | 0.120912 | 0.024002 |
Мы видим, что большинство пользователей лояльны и при этом пользуются дополнительными услугами (международного роуминга / голосовой почты).
Продвинутые пользователи Excel наверняка вспомнят о такой фиче, как сводные таблицы (pivot tables). В Pandas за сводные таблицы отвечает метод pivot_table , который принимает в качестве параметров:
- values – список переменных, по которым требуется рассчитать нужные статистики,
- index – список переменных, по которым нужно сгруппировать данные,
- aggfunc — то, что нам, собственно, нужно посчитать по группам — сумму, среднее, максимум, минимум или что-то ещё.
Давайте посмотрим среднее число дневных, вечерних и ночных звонков для разных Area code:
| Total day calls | Total eve calls | Total night calls | |
|---|---|---|---|
| Area code | |||
| 408 | 100.496420 | 99.788783 | 99.039379 |
| 415 | 100.576435 | 100.503927 | 100.398187 |
| 510 | 100.097619 | 99.671429 | 100.601190 |
Преобразование датафреймов
Как и многое другое в Pandas, добавление столбцов в DataFrame осуществимо несколькими способами.
Например, мы хотим посчитать общее количество звонков для всех пользователей. Создадим объект total_calls типа Series и вставим его в датафрейм:

Добавить столбец из имеющихся можно и проще, не создавая промежуточных Series:

Чтобы удалить столбцы или строки, воспользуйтесь методом drop , передавая в качестве аргумента нужные индексы и требуемое значение параметра axis ( 1 , если удаляете столбцы, и ничего или 0 , если удаляете строки):

4. Первые попытки прогнозирования оттока
Посмотрим, как отток связан с признаком «Подключение международного роуминга» (International plan). Сделаем это с помощью сводной таблички crosstab, а также путем иллюстрации с Seaborn (как именно строить такие картинки и анализировать с их помощью графики – материал следующей статьи).
| International plan | False | True | All |
|---|---|---|---|
| Churn | |||
| 0 | 2664 | 186 | 2850 |
| 1 | 346 | 137 | 483 |
| All | 3010 | 323 | 3333 |

Видим, что когда роуминг подключен, доля оттока намного выше – интересное наблюдение! Возможно, большие и плохо контролируемые траты в роуминге очень конфликтогенны и приводят к недовольству клиентов телеком-оператора и, соответственно, к их оттоку.
Далее посмотрим на еще один важный признак – «Число обращений в сервисный центр» (Customer service calls). Также построим сводную таблицу и картинку.
| Customer service calls | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | All |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Churn | |||||||||||
| 0 | 605 | 1059 | 672 | 385 | 90 | 26 | 8 | 4 | 1 | 0 | 2850 |
| 1 | 92 | 122 | 87 | 44 | 76 | 40 | 14 | 5 | 1 | 2 | 483 |
| All | 697 | 1181 | 759 | 429 | 166 | 66 | 22 | 9 | 2 | 2 | 3333 |

Может быть, по сводной табличке это не так хорошо видно (или скучно ползать взглядом по строчкам с цифрами), а вот картинка красноречиво свидетельствует о том, что доля оттока сильно возрастает начиная с 4 звонков в сервисный центр.
Добавим теперь в наш DataFrame бинарный признак — результат сравнения Customer service calls > 3 . И еще раз посмотрим, как он связан с оттоком.
| Churn | 0 | 1 | All |
|---|---|---|---|
| Many_service_calls | |||
| 0 | 2721 | 345 | 3066 |
| 1 | 129 | 138 | 267 |
| All | 2850 | 483 | 3333 |

Объединим рассмотренные выше условия и построим сводную табличку для этого объединения и оттока.
| Churn | 0 | 1 |
|---|---|---|
| row_0 | ||
| False | 2841 | 464 |
| True | 9 | 19 |
Значит, прогнозируя отток клиента в случае, когда число звонков в сервисный центр больше 3 и подключен роуминг (и прогнозируя лояльность – в противном случае), можно ожидать около 85.8% правильных попаданий (ошибаемся всего 464 + 9 раз). Эти 85.8%, которые мы получили с помощью очень простых рассуждений – это неплохая отправная точка (baseline) для дальнейших моделей машинного обучения, которые мы будем строить.
В целом до появления машинного обучения процесс анализа данных выглядел примерно так. Прорезюмируем:
- Доля лояльных клиентов в выборке – 85.5%. Самая наивная модель, ответ которой «клиент всегда лоялен» на подобных данных будет угадывать примерно в 85.5% случаев. То есть доли правильных ответов (accuracy) последующих моделей должны быть как минимум не меньше, а лучше, значительно выше этой цифры;
- С помощью простого прогноза, который условно можно выразить такой формулой: «International plan = True & Customer Service calls > 3 => Churn = 1, else Churn = 0», можно ожидать долю угадываний 85.8%, что еще чуть выше 85.5%. Впоследствии мы поговорим о деревьях решений и разберемся, как находить подобные правила автоматически на основе только входных данных;
- Эти два бейзлайна мы получили без всякого машинного обучения, и они служат отправной точной для наших последующих моделей. Если окажется, что мы громадными усилиями увеличиваем долю правильных ответов всего, скажем, на 0.5%, то возможно, мы что-то делаем не так, и достаточно ограничиться простой моделью из двух условий;
- Перед обучением сложных моделей рекомендуется немного покрутить данные и проверить простые предположения. Более того, в бизнес-приложениях машинного обучения чаще всего начинают именно с простых решений, а потом экспериментируют с их усложнениями.
5. Домашнее задание №1
Для разминки/подготовки предлагается поанализировать демографические данные с помощью Pandas. Надо заполнить недостающий код в Jupyter-заготовке и выбрать правильные ответы в веб-форме (там же найдете и решение).
Актуальные и обновляемые версии демо-заданий – на английском на сайте курса, вот первое задание. Также по подписке на Patreon («Bonus Assignments» tier) доступны расширенные домашние задания по каждой теме (только на англ.).