Архив метки: pandas

Группировка (groupby) в pandas c сохранением количества строк, генерация агрегатных фич.

Пусть у нас есть датафрейм и нам надо сгруппировать его по колонке «A»

import pandas as pd
import numpy as np


df = pd.DataFrame()
df['A'] = [1, 1, 1, 1, 1, 2, 2, 3, 3, 3]
df['v'] = [3, 5, 7, 10, 20, 11, 1, 3, 8, 10]

print(df.head(10))
# распечатаем датафрейм для наглядности
   A   v
0  1   3
1  1   5
2  1   7
3  1  10
4  1  20
5  2  11
6  2   1
7  3   3
8  3   8
9  3  10

Для группировка выберем функцию среднего и после применения операции группировки в DataFrame остается всего 2 колонки. Пример:

res = df.groupby(by=['A'], as_index=False)['v'].mean()
print(res.head())
# результат
   A  v
0  1  9
1  2  6
2  3  7

Как сделать так, чтобы просто добавить результат агрегации в новую колонку? Для этого воспользуемся функцией transform

df['col_mean'] = df.groupby(by=['A'], as_index=False)['v'].transform(lambda s: np.mean(s.values))
print(df.head(10))
# получаем результат агрегации в новой колонке
   A   v  col_mean
0  1   3       9.0
1  1   5       9.0
2  1   7       9.0
3  1  10       9.0
4  1  20       9.0
5  2  11       6.0
6  2   1       6.0
7  3   3       7.0
8  3   8       7.0
9  3  10       7.0

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

import pandas as pd
import numpy as np
import scipy
from scipy import stats


def make_agg(df_in, group_col_name, stat_col_name):
    df = df_in.copy()
    # то же самое что и count, количество
    df['col_count'] = df.groupby(by=[group_col_name], as_index=False)[stat_col_name].transform(lambda s: s.values.shape[0])
    # среднее значение по сгруппированному столбцу
    df['col_mean'] = df.groupby(by=[group_col_name], as_index=False)[stat_col_name].transform(lambda s: np.mean(s.values))
    # медиана по сгруппированному столбцу
    df['col_median'] = df.groupby(by=[group_col_name], as_index=False)[stat_col_name].transform(lambda s: np.median(s.values))
    # сумма по сгруппированному столбцу
    df['col_sum'] = df.groupby(by=[group_col_name], as_index=False)[stat_col_name].transform(lambda s: np.sum(s.values))
    # минимум и максимум по сгруппированному столбцу
    df['col_max'] = df.groupby(by=[group_col_name], as_index=False)[stat_col_name].transform(lambda s: np.max(s.values))
    df['col_min'] = df.groupby(by=[group_col_name], as_index=False)[stat_col_name].transform(lambda s: np.min(s.values))
    # стандартное отклонение по сгруппированному столбцу
    df['col_std'] = df.groupby(by=[group_col_name], as_index=False)[stat_col_name].transform(lambda s: np.std(s.values))
    # квантили в 20 и 80 % по сгруппированному столбцу
    df['col_q20'] = df.groupby(by=[group_col_name], as_index=False)[stat_col_name].transform(lambda s: np.quantile(s.values, q=0.2))
    df['col_q80'] = df.groupby(by=[group_col_name], as_index=False)[stat_col_name].transform(lambda s: np.quantile(s.values, q=0.8))
    # skew
    df['col_skew'] = df.groupby(by=[group_col_name], as_index=False)[stat_col_name].transform(lambda s: scipy.stats.skew(s))

    return df

res = make_agg(df, 'A', 'v')
print(res.head(10))

Как видите в transform можно написать любую свою lambda функцию. Только помните, что переменная придет типом Series, и для того чтобы случайно не запутаться в индексах, рекомендую добавлять values, тем самым переходя к стандартному numpy массиву. Также считать агрегатные статистики лучше сразу по объединенному train+test датасету, а не по отдельности для train и test. Так как объединенный датасет будет более показательным.

Как обойти строки dataframe в цикле (pandas)

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

import pandas as pd

dataframe_from_list = [[1,2], [3,4], [10,20]]
df = pd.DataFrame(dataframe_from_list, columns=['col1', 'col2'])

for index, row in df.iterrows():
    print(index, row)
    print(row['col1'], row['col2'], row['col1'] + row['col2'])

В данном примере использовалась функция iterrows для обхода датафрейма. Для обращения к колоночным значениям в строке используется row['название_колонки'].

А теперь давайте подумаем, зачем нам итерироваться по датафрейму: самое очевидно это взять некоторые колоночные значения из строки и подсчитать некоторую функцию.
Но это можно сделать и с помощью apply метода с указанием направления по оси x:

result = df.apply(lambda row: row['col1'] + row['col2'], axis=1)
print(result)
# 3, 7, 30

Соответсвенно, вместо lambda функции можно поставить свою, или в простом случае использовать оптимизированные numpy функции, например np.sum

Ну а на последок, давайте представим что у нас большое кол-во строк, сравнимое с миллионом, и нам надо подсчитать некую функцию. Проблема в том, что pandas вычисляет apply в один процесс, а все современные процессоры имют несколько ядер. Поэтому необходимо распараллелить apply функцию для оптимального расчета. Для распараллеливания и быстрого расчета функции по датафрейму воспользуемя функцией ниже:

from multiprocessing import Pool
import numpy as np

# для примера возьмем функцию суммы по строке, приведенную выше
def calculate_sum_column(df):
    df['sum_column'] = df.apply(lambda row: row['col1'] + row['col2'], axis=1)
    return df

# в данном примере расспараллеливаем на восемь потоков. Будьте аккуратны - при распараллеливании тратится больше оперативной памяти
def parallelize_dataframe(df, func):
    a,b,c,d,e,f,g,h = np.array_split(df, 8)
    pool = Pool(8)
    some_res = pool.map(func, [a,b,c,d,e,f,g,h])
    df = pd.concat(some_res)
    pool.close()
    pool.join()
    return df

# имитация большого датасета
df = pd.concat([df, df, df], ignore_index=True)

df = parallelize_dataframe(df, calculate_sum_column)
print(df.head(10))

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

DataFrame pandas базовые манипуляции с данными

Pandas — очень мощный инструмент для работы и манипуляций над данными с помощью python. Можно сказать, что pandas предоставляет возможности SQL базы данных, дополненные мощью python. Однако, работа с данной библиотекой иногда вызывает некоторые трудности, так как работать приходится по сути с матричными данными и объектами pandas да numpy, а переход к стандартным python типам и циклам неминуемо грозит кратной потерей функциональности. Поэтому для себя создал шпаргалку по работе с дата-фреймами:

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

df = pd.read_csv('some.csv')
# если Вам заранее известны типы, то чрезвычайно полезно задать типы колонок сразу - это сэкономит оперативную память.
# также можно использоть лишь несколько колонок, а не все колонки в файле
df = pd.read_csv('some.csv', usecols=["user_id", "id3"], dtype={"user_id": np.int64, "id3": np.uint16})

Возможно и самостоятельно создать DataFrame и добавить строки

# пример создания объекта дата-фрейм
pd.DataFrame(columns=['A', 'B'])
# пример добавления строки в дата-фрейм
df.append({'A':1, 'B':2}, ignore_index=True)

После создания или загрузки DF полезно посмотреть чтоже там за данные, для этого можно воспользоваться следующими операциями:

df.head()       # первые строки
df.tail()       # последние строки
df.sample(5)    # случайно выбранное кол-ва строк, полезно использовать для уменьшения матрицы для прогонки тестов
df.shape        # по аналогии с numpy - размерность матрицы
df.describe()   # математические данные
df.info()       # использование памяти

Возможно далее вам захочется получить точечное значение по координатам из дата фрема, тогда используйте ix

some_value = df.ix[2, 'som_col']  # похожие функции loc и iloc

Для фильтрации по колонкам пандас использует булевую логику, проще показать на примере:

filtered_data = df[df.some_col == 'apple']
filtered_data = df[(df.price > 10.0) & (df.some_col == 'apple')]

Далее приведём несколько примеров манипуляции с данными:

# пример удаления колонки:
df = df.drop("A", axis=1)
# удалит дубликаты
df = df.drop_duplicates()
# Создание новой колонки со значением по умолчанию
df['new_col'] = 1
# а вот так можно добавлять колонку с условием
df['new_col2'] = np.where(df['score']>=10.0, True, False)

Применение произвольной функции ко всей колонке

def get_score(s):
    return s * 0.21

df['score'] = df.some_col.apply(get_score)

Переименование колонки, сортировка

# переименовываем колонку A на колонку C
df = df.rename(columns={'A': 'C'})
# сортировка по одной колонке
df = df.sort_values('price', axis=0, ascending=False)
# сортировка сразу по нескольким колонкам
df = df.sort_values(['price', 'score'], ascending=[1, 0])

пример на соединение или конкатенацию дата-фреймов

df = pd.concat([df1, df2], ignore_index=True, axis=0)

группировка в дата-фрейме без мультииндекса

df.groupby(['A'], as_index=False).agg({'B': lambda series: series.iloc[0]})

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

# в numpy массив
df.values
# в numpy массив, но сразу всю матрицу
df.as_matrix
# сохранить в файл
df.to_csv('submission.csv', index=False)