Как применить функцию к двум столбцам Pandas dataframe

369

Предположим, у меня dfесть столбцы 'ID', 'col_1', 'col_2'. И я определяю функцию:

f = lambda x, y : my_function_expression,

Теперь я хочу , чтобы применить fк dfдвум колонкам «S 'col_1', 'col_2'для поэлементнога рассчитает новый столбец 'col_3', несколько как:

df['col_3'] = df[['col_1','col_2']].apply(f)  
# Pandas gives : TypeError: ('<lambda>() takes exactly 2 arguments (1 given)'

Как сделать ?

** Добавить образец детали, как показано ниже ***

import pandas as pd

df = pd.DataFrame({'ID':['1','2','3'], 'col_1': [0,2,3], 'col_2':[1,4,5]})
mylist = ['a','b','c','d','e','f']

def get_sublist(sta,end):
    return mylist[sta:end+1]

#df['col_3'] = df[['col_1','col_2']].apply(get_sublist,axis=1)
# expect above to output df as below 

  ID  col_1  col_2            col_3
0  1      0      1       ['a', 'b']
1  2      2      4  ['c', 'd', 'e']
2  3      3      5  ['d', 'e', 'f']
bigbug
источник
4
можете ли вы применить f непосредственно к столбцам: df ['col_3'] = f (df ['col_1'], df ['col_2'])
btel
1
Было бы полезно узнать, что fделает
tehmisvh
2
нет, df ['col_3'] = f (df ['col_1'], df ['col_2']) не работает. Для f принимает только скалярные входные данные, а не векторные входные. Хорошо, вы можете считать, что f = лямбда x, y: x + y. (конечно, мой настоящий f не так прост, иначе я могу напрямую df ['col_3'] = df ['col_1'] + df ['col_2'])
bigbug
1
Я нашел соответствующие вопросы и ответы по ссылке ниже, но моя проблема - вычисление нового столбца по двум существующим столбцам, а не по 2 из 1. stackoverflow.com/questions/12356501/…
bigbug
Я думаю, что мой ответ stackoverflow.com/a/52854800/5447172 отвечает на этот вопрос самым питонским / панданским способом, без обходных путей или числовой индексации. Он производит именно то, что вам нужно в вашем примере.
ajrwhite

Ответы:

291

Вот пример использования applyна фрейме данных, с которым я звоню axis = 1.

Обратите внимание, что отличие состоит в том, что вместо того, чтобы пытаться передать в функцию два значения f, перепишите функцию, чтобы принять объект Series pandas, а затем проиндексируйте Series, чтобы получить необходимые значения.

In [49]: df
Out[49]: 
          0         1
0  1.000000  0.000000
1 -0.494375  0.570994
2  1.000000  0.000000
3  1.876360 -0.229738
4  1.000000  0.000000

In [50]: def f(x):    
   ....:  return x[0] + x[1]  
   ....:  

In [51]: df.apply(f, axis=1) #passes a Series object, row-wise
Out[51]: 
0    1.000000
1    0.076619
2    1.000000
3    1.646622
4    1.000000

В зависимости от вашего варианта использования иногда полезно создать groupобъект pandas , а затем использовать его applyв группе.

Человек
источник
Да, я пытался использовать apply, но не могу найти правильное синтаксическое выражение. И если каждая строка df уникальна, все еще использовать groupby?
bigbug
Добавил пример в мой ответ, надеюсь, что это делает то, что вы ищете. Если нет, предоставьте более конкретный пример функции, поскольку sumона успешно решается любым из предложенных методов.
Аман
1
Не могли бы вы вставить свой код? Я переписываю функцию: def get_sublist (x): возвращает mylist [x [1]: x [2] + 1], а df ['col_3'] = df.apply (get_sublist, axis = 1) дает 'ValueError: операнды могли не будет транслироваться вместе с фигурами (2) (3) '
bigbug
3
@ Aman: с версией Pandas 0.14.1 (и, возможно, более ранней), use может также использовать лямбда-выражение. Дайте dfобъект, который вы определили, другой подход (с эквивалентными результатами) df.apply(lambda x: x[0] + x[1], axis = 1).
Jubbles
2
@CanCeylan, вы можете просто использовать имена столбцов в функции вместо индексов, тогда вам не нужно беспокоиться об изменении порядка или получить индекс по имени, например, см. Stackoverflow.com/questions/13021654/…
Davos
168

В Pandas есть чистый, простой способ сделать это:

df['col_3'] = df.apply(lambda x: f(x.col_1, x.col_2), axis=1)

Это позволяет fбыть пользовательской функцией с несколькими входными значениями и использует (безопасные) имена столбцов, а не (небезопасные) числовые индексы для доступа к столбцам.

Пример с данными (на основе оригинального вопроса):

import pandas as pd

df = pd.DataFrame({'ID':['1', '2', '3'], 'col_1': [0, 2, 3], 'col_2':[1, 4, 5]})
mylist = ['a', 'b', 'c', 'd', 'e', 'f']

def get_sublist(sta,end):
    return mylist[sta:end+1]

df['col_3'] = df.apply(lambda x: get_sublist(x.col_1, x.col_2), axis=1)

Выход print(df):

  ID  col_1  col_2      col_3
0  1      0      1     [a, b]
1  2      2      4  [c, d, e]
2  3      3      5  [d, e, f]

Если имена ваших столбцов содержат пробелы или имеют общее имя с существующим атрибутом dataframe, вы можете индексировать в квадратных скобках:

df['col_3'] = df.apply(lambda x: f(x['col 1'], x['col 2']), axis=1)
ajrwhite
источник
2
Обратите внимание, что при использовании axis=1столбца and you nameон на самом деле не возвращает данные вашего столбца, а index. Похоже на получение nameв groupby(). Я решил это, переименовав мою колонку.
Том
2
ЭТО ОНО! Я просто не осознавал, что вы можете вставить пользовательские функции с несколькими входными параметрами в лямбды. Важно отметить (я думаю), что вы используете DF.apply (), а не Series.apply (). Это позволяет вам индексировать df, используя два нужных вам столбца, и передавать весь столбец в функцию, но поскольку вы используете apply (), он применяет функцию поэлементно ко всему столбцу. Brilliant! Спасибо за публикацию!
Data-phile
1
НАКОНЕЦ-ТО! Ты спас мой день!
Mysterio
Я полагаю, что предложенный способ сделать это - df.loc [:, 'new col'] = df.apply .....
valearner
@valearner Я не думаю, что есть какая-то причина .locв этом примере. Это может понадобиться, если вы адаптируете это к другой проблеме (например, работаете со слайсами).
ajrwhite
86

Простое решение:

df['col_3'] = df[['col_1','col_2']].apply(lambda x: f(*x), axis=1)
SJM
источник
1
Чем этот ответ отличается от подхода в вопросе: df ['col_3'] = df [['col_1', 'col_2']]. apply (f) просто для подтверждения, подход в вопросе не сработал, потому что В плакате не указана эта ось = 1, по умолчанию это ось = 0?
Потерян 1
1
Этот ответ сравним с ответом @ Anman, но немного забавнее. Он создает анонимную функцию, которая берет итерацию и распаковывает ее перед передачей в функцию f.
ий
39

Интересный вопрос! мой ответ как ниже:

import pandas as pd

def sublst(row):
    return lst[row['J1']:row['J2']]

df = pd.DataFrame({'ID':['1','2','3'], 'J1': [0,2,3], 'J2':[1,4,5]})
print df
lst = ['a','b','c','d','e','f']

df['J3'] = df.apply(sublst,axis=1)
print df

Вывод:

  ID  J1  J2
0  1   0   1
1  2   2   4
2  3   3   5
  ID  J1  J2      J3
0  1   0   1     [a]
1  2   2   4  [c, d]
2  3   3   5  [d, e]

Я изменил имя столбца на ID, J1, J2, J3, чтобы обеспечить идентификатор <J1 <J2 <J3, поэтому столбец отображается в правильной последовательности.

Еще одна краткая версия:

import pandas as pd

df = pd.DataFrame({'ID':['1','2','3'], 'J1': [0,2,3], 'J2':[1,4,5]})
print df
lst = ['a','b','c','d','e','f']

df['J3'] = df.apply(lambda row:lst[row['J1']:row['J2']],axis=1)
print df

источник
23

Метод, который вы ищете, - Series.combine. Тем не менее, кажется, что некоторые типы данных необходимо соблюдать осторожность. В вашем примере вы бы (как я делал при тестировании ответа) наивно

df['col_3'] = df.col_1.combine(df.col_2, func=get_sublist)

Тем не менее, это выдает ошибку:

ValueError: setting an array element with a sequence.

Мое лучшее предположение состоит в том, что, похоже, ожидается, что результат будет того же типа, что и серия, вызывающая метод (здесь df.col_1). Тем не менее, следующие работы:

df['col_3'] = df.col_1.astype(object).combine(df.col_2, func=get_sublist)

df

   ID   col_1   col_2   col_3
0   1   0   1   [a, b]
1   2   2   4   [c, d, e]
2   3   3   5   [d, e, f]
JoeCondron
источник
12

То, как вы написали f, требует двух входов. Если вы посмотрите на сообщение об ошибке, оно говорит, что вы не предоставляете два ввода для f, только один. Сообщение об ошибке верно.
Несовпадение связано с тем, что df [['col1', 'col2']] возвращает один кадр данных с двумя столбцами, а не двумя отдельными столбцами.

Вам нужно изменить свой f так, чтобы он принимал один вход, сохранить указанный кадр данных в качестве входного, а затем разбить его на x, y внутри тела функции. Затем делайте все, что вам нужно, и возвращайте одно значение.

Вам нужна эта сигнатура функции, потому что ее синтаксис: .apply (f). Таким образом, f нужно взять одну вещь = dataframe, а не две вещи, которые ожидают ваши текущие f.

Поскольку вы не предоставили основную часть f, я не могу помочь более подробно - но это должно обеспечить выход без фундаментального изменения кода или использования каких-либо других методов, а не применения

Нитин
источник
12

Я собираюсь проголосовать за np.vectorize. Он позволяет вам снимать более x столбцов и не иметь дело с кадром данных в функции, поэтому он отлично подходит для функций, которые вы не контролируете или не выполняете что-то вроде отправки 2 столбцов и константы в функцию (например, col_1, col_2, 'Foo').

import numpy as np
import pandas as pd

df = pd.DataFrame({'ID':['1','2','3'], 'col_1': [0,2,3], 'col_2':[1,4,5]})
mylist = ['a','b','c','d','e','f']

def get_sublist(sta,end):
    return mylist[sta:end+1]

#df['col_3'] = df[['col_1','col_2']].apply(get_sublist,axis=1)
# expect above to output df as below 

df.loc[:,'col_3'] = np.vectorize(get_sublist, otypes=["O"]) (df['col_1'], df['col_2'])


df

ID  col_1   col_2   col_3
0   1   0   1   [a, b]
1   2   2   4   [c, d, e]
2   3   3   5   [d, e, f]
Трей Уоллес
источник
1
Это на самом деле не отвечает на вопрос с помощью панд.
mnky9800n
18
Вопрос состоит в том, «Как применить функцию к двум столбцам информационного кадра Pandas», а не «Как применить функцию к двум столбцам информационного кадра Pandas, используя только методы Pandas», а numpy - это зависимость Pandas, так что вам все равно нужно ее установить, так что это кажется странным возражением.
Тре Уоллес
12

Возврат списка из applyявляется опасной операцией, поскольку результирующий объект не гарантированно будет Series или DataFrame. И исключения могут быть подняты в некоторых случаях. Давайте рассмотрим простой пример:

df = pd.DataFrame(data=np.random.randint(0, 5, (5,3)),
                  columns=['a', 'b', 'c'])
df
   a  b  c
0  4  0  0
1  2  0  1
2  2  2  2
3  1  2  2
4  3  0  0

Есть три возможных результата с возвратом списка из apply

1) Если длина возвращаемого списка не равна количеству столбцов, возвращается серия списков.

df.apply(lambda x: list(range(2)), axis=1)  # returns a Series
0    [0, 1]
1    [0, 1]
2    [0, 1]
3    [0, 1]
4    [0, 1]
dtype: object

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

df.apply(lambda x: list(range(3)), axis=1) # returns a DataFrame
   a  b  c
0  0  1  2
1  0  1  2
2  0  1  2
3  0  1  2
4  0  1  2

3) Если длина возвращаемого списка равна количеству столбцов для первой строки, но имеет хотя бы одну строку, в которой список имеет количество элементов, отличное от количества столбцов, возникает ошибка ValueError.

i = 0
def f(x):
    global i
    if i == 0:
        i += 1
        return list(range(3))
    return list(range(4))

df.apply(f, axis=1) 
ValueError: Shape of passed values is (5, 4), indices imply (5, 3)

Отвечая на проблему без применения

Использование applyс осью = 1 очень медленно. С помощью базовых итерационных методов можно добиться гораздо большей производительности (особенно для больших наборов данных).

Создать больший размер

df1 = df.sample(100000, replace=True).reset_index(drop=True)

Задержки

# apply is slow with axis=1
%timeit df1.apply(lambda x: mylist[x['col_1']: x['col_2']+1], axis=1)
2.59 s ± 76.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

# zip - similar to @Thomas
%timeit [mylist[v1:v2+1] for v1, v2 in zip(df1.col_1, df1.col_2)]  
29.5 ms ± 534 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

@ Томас ответ

%timeit list(map(get_sublist, df1['col_1'],df1['col_2']))
34 ms ± 459 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Тед Петру
источник
1
Приятно видеть столь подробные ответы, где можно учиться.
Андреа Моро
7

Я уверен, что это не так быстро, как решения, использующие операции Pandas или Numpy, но если вы не хотите переписывать свою функцию, вы можете использовать map. Используя исходные данные примера -

import pandas as pd

df = pd.DataFrame({'ID':['1','2','3'], 'col_1': [0,2,3], 'col_2':[1,4,5]})
mylist = ['a','b','c','d','e','f']

def get_sublist(sta,end):
    return mylist[sta:end+1]

df['col_3'] = list(map(get_sublist,df['col_1'],df['col_2']))
#In Python 2 don't convert above to list

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

ID  col_1  col_2      col_3
0  1      0      1     [a, b]
1  2      2      4  [c, d, e]
2  3      3      5  [d, e, f]
Томас
источник
1
На самом деле это гораздо быстрее тех ответов, которые используют applyсaxis=1
Тед Петру
2

Мой пример на ваши вопросы:

def get_sublist(row, col1, col2):
    return mylist[row[col1]:row[col2]+1]
df.apply(get_sublist, axis=1, col1='col_1', col2='col_2')
Цин Лю
источник
2

Если у вас огромный набор данных, вы можете использовать простой, но более быстрый (время выполнения) способ сделать это с помощью swifter:

import pandas as pd
import swifter

def fnc(m,x,c):
    return m*x+c

df = pd.DataFrame({"m": [1,2,3,4,5,6], "c": [1,1,1,1,1,1], "x":[5,3,6,2,6,1]})
df["y"] = df.swifter.apply(lambda x: fnc(x.m, x.x, x.c), axis=1)
Durjoy
источник
1

Я предполагаю, что вы не хотите менять get_sublistфункцию, а просто хотите использовать applyметод DataFrame для выполнения этой работы. Чтобы получить желаемый результат, я написал две справочные функции: get_sublist_listи unlist. Как следует из названия функции, сначала получите список подсписков, затем извлеките этот подсписок из этого списка. Наконец, нам нужно вызвать applyфункцию, чтобы df[['col_1','col_2']]впоследствии применить эти две функции к DataFrame.

import pandas as pd

df = pd.DataFrame({'ID':['1','2','3'], 'col_1': [0,2,3], 'col_2':[1,4,5]})
mylist = ['a','b','c','d','e','f']

def get_sublist(sta,end):
    return mylist[sta:end+1]

def get_sublist_list(cols):
    return [get_sublist(cols[0],cols[1])]

def unlist(list_of_lists):
    return list_of_lists[0]

df['col_3'] = df[['col_1','col_2']].apply(get_sublist_list,axis=1).apply(unlist)

df

Если вы не используете []для включения get_sublistфункции, то get_sublist_listфункция вернет простой список, который будет ValueError: could not broadcast input array from shape (3) into shape (2)создан, как упоминал @Ted Petrou.

allenyllee
источник