Pandas, Анализ данных

Интересные трюки pandas

Время прочтения: 8 мин.

Для начала, выберем себе «подопытного кролика». Им станет набор данных, расположенный по ссылке.

Не будем утруждать себя переходом на сайт и скачиванием документа. Считаем его прямо в коде программы по url:

dataframe  =  pd.read_csv('https://raw.github.com/pandas-dev/pandas/master/pandas/tests/data/tips.csv')
dataframe.head()

В представленной выше таблице собраны данные о том, какой размер чаевых оставляют люди в зависимости от дня недели, итогового счёта, пола, времени приёма пищи и, даже, размера одежды и пристрастия к курению. В таблице 6 столбцов и 244 строки. При помощи команды

dataframe.dtypes

можно увидеть, какого типа данные собраны в таблице:

А теперь приступим к преобразованиям.

1. MAP

Часто для моделей машинного обучения переменную «пол» необходимо перевести из текстового формата в числовой, закодировав «Male», например, как 1, а «Female» — 0.

Можно заменить значения в столбце «sex» на True/False, сравнивая с «Male», а потом конвертировать True/False к типу данных int:

dataframe['sex'] = (dataframe['sex']=='Male').astype(int)
end = datetime.now()
print('Duration:', end-start)
dataframe.head()

Получим следующий результат (рис 2 ). В первой строке указано время выполнения кода, вычисленное при помощи модуля datetime.

Теперь пришло время применить первый трюк нашей статьи – функции «map». Для неё необходимо создать словарь следующего типа {«старое значение»: «новое значение»}. После применения этой функции, значения в столбце будут заменены на новые в соответствии со словарём:

col_map = {"Male":1, "Female":0} – создали словарь
start = datetime.now()
dataframe['sex'] = dataframe['sex'].map(col_map) – применили метод
end = datetime.now()
print('Duration:', end-start)
dataframe.head()

Результат выполнения программы :

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

 2. IDXMIN/IDXMAX + NE

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

Например, команда dataframe[‘total_bill’].idxmax() вернёт результат 170. Это значит, что запись с максимальным итоговым счётом находится на 170-ой строке нашей таблицы. Получить её можно с помощью команды dataframe.loc[dataframe[‘total_bill’].idxmax()]:

Но это не самое интересное применение данных команд, хоть и полезное.

Рассмотрим их связку с функцией ne(). «ne» — это сокращение от «not equal», и функция делает ровно то, о чем говорит её название: проверяет все значения столбца на равенство заданному значению и возвращает True или False.

Например, команда dataframe.time.ne(«Dinner») покажет, был счёт оплачен во время обеда или нет:

   0 False
   1 False
   2 False
   3 False
   4 False
   5 False

       …

   220 True
   221 True
   222 True
   223 True
   224 True

        …

   240 False
   241 False
   242 False
   243 False

Теперь рассмотрим, как эти две команды работают в связке:

dataframe.time.ne(‘Dinner’).idxmax() – покажет индекс первой записи, для которой время оплаты счёта не «Dinner».  Результатом выполнения этой команды будет 77. То есть, для всех счетов в строках 0 – 76 значение переменной «time» будет равно «Dinner», а строка 77 – первая, для которой это не так. Команда dataframe.loc[dataframe.time.ne(‘Dinner’).idxmax()] покажет нам эту запись:

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

dataframe.loc[dataframe.time.ne('Dinner').idxmax():]

Этот приём может быть особенно полезен для анализа временных рядов с повторяющимися значениями в начале.

3. NSMALLEST/NLARGEST

Для того, чтобы получить топ-5 посетителей с самыми большими оплаченным счетами, обычно применяют два метода: сначала выполняют метод sort_values(), в котором указывают ключ ascending = False, а затем методом head()  то есть отсекают необходимое количество записей. Вот как это выглядит в коде:

start = datetime.now()  - вычисляем время выполнения
sub_df = dataframe.sort_values(by='total_bill', ascending= False).head(5) – применяем два метода
end = datetime.now() – вычисляем время выполнения
print('Duration:', end-start) – выводим время выполнения
sub_df – выводим результат

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

Чтобы получить топ-5 посетителей с наибольшим оплаченным счётом при помощи nlargest(), нужно будет выполнить код:

start = datetime.now() – вычисляем время выполнения
sub_df = dataframe.nlargest(5,'total_bill') – применяем метод
end = datetime.now() – вычисляем время выполнения
print('Duration:', end-start) – выводим время выполнения
sub_df – выводим результат

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

Как несложно догадаться из названия, метод nsmallest() будет делать то же самое, что и nlargest(), но выдавать не топ наибольших, а топ наименьших значений.

4. ISIN ПРОТИВ APPLY

Если есть необходимость применить к одному или нескольким столбцам DataFrame какую-нибудь функцию, то для этого используют метод apply(). Но на больших данных этот метод может работать достаточно долго, поэтому рекомендуется не использовать его там, где функцию можно реализовать без apply().

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

SELECT * 
FROM dataframe
WHERE total_bill IN (10.27, 35.26, 19.82, 24.06, 16.31)

В pandas можно реализовать это кодом с использованием apply.
Для начала, создадим вектор длины 20, в который положим случайные значений переменной «total_bill»:

sample = [dataframe.total_bill[i] for i in sorted(random.sample(range(len(dataframe)), 20))]

А теперь напишем фильтр, в котором применим метод apply() и lambda-функцию, проверяющую, лежит ли значение в векторе sample:

start = datetime.now() – вычисляем время выполнения
sub_df = dataframe[dataframe['total_bill'].apply(lambda x: x in sample)==True] –применяем методы
end = datetime.now() – вычисляем время выполнения
print('Duration:', end-start) – выводим время выполнения
sub_df – выводим результат

Теперь заменим применение этих методов на выполнение одного метода – isin():

start = datetime.now() – вычисляем время выполнения
sub_df = dataframe[dataframe['total_bill'].isin(sample)] – применяем метод
end = datetime.now() – вычисляем время выполнения
print('Duration:', end-start) – выводим время выполнения
sub_df – выводим результат

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

5. VALUE_COUNTS И НЕМНОГО ГРАФИКОВ

Часто при работе с данными бывает полезно построить распределение величины.

В нашем случае зададимся простой задачей: построить распределение размера оплаченных чаевых.

Для этого нам понадобится DataFrame из двух столбцов: в первом будет размер чаевых, во втором – количество человек, оставивших эти чаевые.

Подойти к решению этой задачи можно с двух сторон. Первый подход основывается на уже классической связке groupby-aggregate. Мы группируем данные в DataFrame по размеру чаевых и считаем количество счетов, в которых эти чаевые были оставлены. В коде это реализуется следующим образом:

start = datetime.now() – вычисляем время выполнения
sub_df = dataframe.groupby('tip', as_index = False).aggregate({'total_bill':'count'}).rename({'total_bill':'count'},axis = 1) – группируем
sub_df = sub_df.sort_values(by='count',ascending = False) - сортируем
end = datetime.now()  – вычисляем время выполнения
print('Duration:', end-start) – выводим время выполнения
sub_df – выводим результат

Посмотрим на первые 5 позиций, которые нам выдаст такой код:

При помощи метода values_count() и метода reset_index() выполним эту же операцию без группировки.

reset_index()  нужен для сохранения результатов работы первого метода в формате DataFrame.

start = datetime.now() – вычисляем время выполнения
sub_df = dataframe.tip.value_counts().reset_index() –применяем методы
sub_df.rename({'index':'tip','tip':'count'},axis = 1, inplace = True) – переименовываем столбцы
end = datetime.now()  – вычисляем время выполнения
print('Duration:', end-start) – выводим время выполнения
sub_df – выводим результат

Посмотрим на результат работы этого кода. Возьмём для наглядности также первые 5 позиций.

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

Теперь полученный DataFrame subdf можно использовать по его прямому назначению – построить график. Сделаем это по десяти значениям с наибольшей частотой:

import plotly.express as px – импортируем библиотеку
fig = px.bar(sub_df.head(10), x='tip', y='count') – строим график
fig.show() – выводим на экран

Кстати, использованная в примере библиотека plotly, позволяет строить в Jupiter Notebook интерактивные графики с широкими возможностями для настройки и детализации.

Почитать о её возможностях можно здесь.

ВЫВОД

Эти небольшие лайфхаки позволят сделать ваш код более красивым и быстрым. Естественно, этими приёмам не ограничивается весь набор интересных возможностей при работе с DataFrame в pandas. Чтобы научиться большему и начать писать лучше, необходимо читать документацию, тематические форумы, а главное – практиковаться.

Советуем почитать