Применить функцию панды к столбцу, чтобы создать несколько новых столбцов?


216

Как это сделать в пандах:

У меня есть функция extract_text_featuresдля одного текстового столбца, возвращающая несколько выходных столбцов. В частности, функция возвращает 6 значений.

Функция работает, однако, похоже, не существует какого-либо правильного возвращаемого типа (pandas DataFrame / numpy array / Python list), чтобы выходные данные могли быть правильно назначены df.ix[: ,10:16] = df.textcol.map(extract_text_features)

Так что я думаю, что мне нужно вернуться к итерации с df.iterrows(), в соответствии с этим ?

ОБНОВЛЕНИЕ: Итерация с df.iterrows(), по крайней мере, в 20 раз медленнее, поэтому я сдался и разделил функцию на шесть отдельных .map(lambda ...)вызовов.

ОБНОВЛЕНИЕ 2: этот вопрос был задан около v0.11.0 . Следовательно, большая часть вопроса и ответов не слишком актуальны.


1
Я не думаю , что вы можете сделать Многократное назначение так , как вы это написано: df.ix[: ,10:16]. Я думаю, что вы будете использовать mergeваши функции в наборе данных.
Zelazny7

1
Для тех, кто хочет гораздо более производительного решения, проверьте это ниже, которое не используетapply
Тед Петру

Большинство числовых операций с пандами можно векторизовать - это означает, что они выполняются намного быстрее, чем обычные итерации. OTOH, некоторые операции (такие как string и regex) по своей природе трудно векторизовать. В этом случае важно понимать, как перебирать ваши данные. Более подробную информацию о том, когда и как следует выполнять циклическую обработку ваших данных, читайте в разделе «Циклы с пандами» - когда мне следует позаботиться? ,
cs95

@coldspeed: основная проблема заключалась не в выборе, который был более высокопроизводительным среди нескольких вариантов, а в борьбе с синтаксисом панд, чтобы заставить его работать вообще, начиная с v0.11.0 .
SMCI

Действительно, комментарий предназначен для будущих читателей, которые ищут итеративные решения, которые либо не знают ничего лучше, либо знают, что они делают.
CS95

Ответы:


109

Основываясь на ответе пользователя 1827356, вы можете выполнить задание за один проход, используя df.merge:

df.merge(df.textcol.apply(lambda s: pd.Series({'feature1':s+1, 'feature2':s-1})), 
    left_index=True, right_index=True)

    textcol  feature1  feature2
0  0.772692  1.772692 -0.227308
1  0.857210  1.857210 -0.142790
2  0.065639  1.065639 -0.934361
3  0.819160  1.819160 -0.180840
4  0.088212  1.088212 -0.911788

РЕДАКТИРОВАТЬ: Обратите внимание на огромное потребление памяти и низкую скорость: https://ys-l.github.io/posts/2015/08/28/how-not-to-use-pandas-apply/ !


2
просто из любопытства, ожидается ли, что это займет много памяти? Я делаю это на фрейме данных, который содержит 2,5 миллиона строк, и я почти столкнулся с проблемами с памятью (также это намного медленнее, чем возвращение только 1 столбца).
Jeffrey04

2
'df.join (df.textcol.apply (lambda s: pd.Series ({' feature1 ': s + 1,' feature2 ': s-1}))) "был бы лучшим вариантом, я думаю.
Шивам К. Таккар

@ShivamKThakkar, почему вы думаете, что ваше предложение будет лучшим вариантом? Это будет более эффективно, как вы думаете, или будет стоить меньше памяти?
Цандо

1
Обратите внимание на скорость и необходимую память: ys-l.github.io/posts/2015/08/28/how-not-to-use-pandas-apply
Make42

190

Я обычно делаю это, используя zip:

>>> df = pd.DataFrame([[i] for i in range(10)], columns=['num'])
>>> df
    num
0    0
1    1
2    2
3    3
4    4
5    5
6    6
7    7
8    8
9    9

>>> def powers(x):
>>>     return x, x**2, x**3, x**4, x**5, x**6

>>> df['p1'], df['p2'], df['p3'], df['p4'], df['p5'], df['p6'] = \
>>>     zip(*df['num'].map(powers))

>>> df
        num     p1      p2      p3      p4      p5      p6
0       0       0       0       0       0       0       0
1       1       1       1       1       1       1       1
2       2       2       4       8       16      32      64
3       3       3       9       27      81      243     729
4       4       4       16      64      256     1024    4096
5       5       5       25      125     625     3125    15625
6       6       6       36      216     1296    7776    46656
7       7       7       49      343     2401    16807   117649
8       8       8       64      512     4096    32768   262144
9       9       9       81      729     6561    59049   531441

8
Но что делать, если вы добавили 50 столбцов, а не 6?
максимум

14
@maxtemp = list(zip(*df['num'].map(powers))); for i, c in enumerate(columns): df[c] = temp[c]
острокач

8
@ostrokach Я думаю, ты имел в виду for i, c in enumerate(columns): df[c] = temp[i]. Благодаря этому я действительно получил цель enumerate: D
rocarvaj

4
Это, безусловно, самое элегантное и удобочитаемое решение, с которым мне приходилось сталкиваться. Если у вас не возникнут проблемы с производительностью, возможно, идиома zip(*df['col'].map(function))- это правильный путь.
Франсуа


84

Это то, что я сделал в прошлом

df = pd.DataFrame({'textcol' : np.random.rand(5)})

df
    textcol
0  0.626524
1  0.119967
2  0.803650
3  0.100880
4  0.017859

df.textcol.apply(lambda s: pd.Series({'feature1':s+1, 'feature2':s-1}))
   feature1  feature2
0  1.626524 -0.373476
1  1.119967 -0.880033
2  1.803650 -0.196350
3  1.100880 -0.899120
4  1.017859 -0.982141

Редактирование для полноты

pd.concat([df, df.textcol.apply(lambda s: pd.Series({'feature1':s+1, 'feature2':s-1}))], axis=1)
    textcol feature1  feature2
0  0.626524 1.626524 -0.373476
1  0.119967 1.119967 -0.880033
2  0.803650 1.803650 -0.196350
3  0.100880 1.100880 -0.899120
4  0.017859 1.017859 -0.982141

concat () выглядит проще, чем merge (), для подключения новых столбцов к исходному фрейму данных.
тмин

2
хороший ответ, вам не нужно использовать dict или слияние, если вы указываете столбцы за пределами примененияdf[['col1', 'col2']] = df['col3'].apply(lambda x: pd.Series('val1', 'val2'))
Мэтт

66

Это правильный и самый простой способ сделать это для 95% случаев:

>>> df = pd.DataFrame(zip(*[range(10)]), columns=['num'])
>>> df
    num
0    0
1    1
2    2
3    3
4    4
5    5

>>> def example(x):
...     x['p1'] = x['num']**2
...     x['p2'] = x['num']**3
...     x['p3'] = x['num']**4
...     return x

>>> df = df.apply(example, axis=1)
>>> df
    num  p1  p2  p3
0    0   0   0    0
1    1   1   1    1
2    2   4   8   16
3    3   9  27   81
4    4  16  64  256

не должны ли вы написать: df = df.apply (пример (df), axis = 1) поправьте меня, если я ошибаюсь, я просто новичок
user299791

1
@ user299791, Нет, в этом случае вы рассматриваете пример как объект первого класса, поэтому вы передаете саму функцию. Эта функция будет применяться к каждой строке.
Майкл Дэвид Уотсон

привет Майкл, твой ответ помог мне в моей проблеме. Определенно, ваше решение лучше, чем оригинальный метод df.assign () от pandas, потому что это один раз на столбец. Используя assign (), если вы хотите создать 2 новых столбца, вы должны использовать df1 для работы с df, чтобы получить новый column1, а затем использовать df2 для работы с df1, чтобы создать второй новый столбец ... это довольно монотонно. Но твой метод спас мне жизнь !!! Спасибо!!!
commentallez-vous

1
Разве это не будет запускать код присваивания столбцов один раз в строке? Не лучше ли вернуть pd.Series({k:v})и сериализовать присваивание столбца, как в ответе Эвана?
Дени де Бернарди

Если это кому-нибудь поможет, хотя этот подход верен, а также является самым простым из всех представленных решений, прямое обновление строки таким образом оказалось на удивление медленным - на порядок медленнее, чем применение с решениями "expand" + pd.concat
Дмитрий Бугаев

31

В 2018 году я использую apply()с аргументомresult_type='expand'

>>> appiled_df = df.apply(lambda row: fn(row.text), axis='columns', result_type='expand')
>>> df = pd.concat([df, appiled_df], axis='columns')

6
Вот как ты это делаешь, сегодня!
Make42

1
Это сработало из коробки в 2020 году, в то время как многие другие вопросы этого не сделали. Кроме того, он не использует, pd.Series что всегда хорошо в отношении проблем с производительностью
Тео Рубенах

1
Это хорошее решение. Единственная проблема заключается в том, что вы не можете выбрать имя для двух вновь добавленных столбцов. Позже вам нужно сделать df.rename (колонки = {0: 'col1', 1: 'col2'})
pedram bashiri

2
@pedrambashiri Если функция, которую вы передаете, df.applyвозвращает a dict, столбцы получат имена в соответствии с ключами.
Себ

25

Просто используйте result_type="expand"

df = pd.DataFrame(np.random.randint(0,10,(10,2)), columns=["random", "a"])
df[["sq_a","cube_a"]] = df.apply(lambda x: [x.a**2, x.a**3], axis=1, result_type="expand")

4
Это помогает отметить, что эта опция является новой в 0.23 . Вопрос был задан обратно на 0,11
SMCI

Хорошо, это просто и все еще работает аккуратно. Это то, что я искал. Спасибо
Исаак Сим

Дублирует предыдущий ответ: stackoverflow.com/a/52363890/823470
tar

22

Сводка: если вы хотите создать только несколько столбцов, используйтеdf[['new_col1','new_col2']] = df[['data1','data2']].apply( function_of_your_choosing(x), axis=1)

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

подробности Допустим, у вас есть двухколонный фрейм данных. Первый столбец - это рост человека, когда ему 10 лет; второй - рост человека, когда ему 20 лет.

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

Вы можете сделать это с помощью следующей функции, которая скоро будет применена:

def mean_and_sum(x):
    """
    Calculates the mean and sum of two heights.
    Parameters:
    :x -- the values in the row this function is applied to. Could also work on a list or a tuple.
    """

    sum=x[0]+x[1]
    mean=sum/2
    return [mean,sum]

Вы можете использовать эту функцию так:

 df[['height_at_age_10','height_at_age_20']].apply(mean_and_sum(x),axis=1)

(Для ясности: эта функция применяет значения из каждой строки в установленном кадре данных и возвращает список.)

Однако, если вы сделаете это:

df['Mean_&_Sum'] = df[['height_at_age_10','height_at_age_20']].apply(mean_and_sum(x),axis=1)

вы создадите 1 новый столбец, который содержит списки [среднее, сумма], которых вы, вероятно, хотели бы избежать, потому что для этого потребуется еще одна лямбда / аппликация.

Вместо этого вы хотите разбить каждое значение на отдельный столбец. Для этого вы можете создать два столбца одновременно:

df[['Mean','Sum']] = df[['height_at_age_10','height_at_age_20']]
.apply(mean_and_sum(x),axis=1)

4
Для панд 0.23 вам нужно использовать синтаксис:df["mean"], df["sum"] = df[['height_at_age_10','height_at_age_20']] .apply(mean_and_sum(x),axis=1)
SummerEla

Эта функция может вызвать ошибку. Функция возврата должна быть return pd.Series([mean,sum])
Kanishk Mair

22

Для меня это сработало:

Вход df

df = pd.DataFrame({'col x': [1,2,3]})
   col x
0      1
1      2
2      3

функция

def f(x):
    return pd.Series([x*x, x*x*x])

Создайте 2 новых столбца:

df[['square x', 'cube x']] = df['col x'].apply(f)

Вывод:

   col x  square x  cube x
0      1         1       1
1      2         4       8
2      3         9      27

13

Я рассмотрел несколько способов сделать это, и метод, показанный здесь (возвращающий серию панд), кажется, не самый эффективный.

Если мы начнем с большого фрейма случайных данных:

# Setup a dataframe of random numbers and create a 
df = pd.DataFrame(np.random.randn(10000,3),columns=list('ABC'))
df['D'] = df.apply(lambda r: ':'.join(map(str, (r.A, r.B, r.C))), axis=1)
columns = 'new_a', 'new_b', 'new_c'

Пример, показанный здесь:

# Create the dataframe by returning a series
def method_b(v):
    return pd.Series({k: v for k, v in zip(columns, v.split(':'))})
%timeit -n10 -r3 df.D.apply(method_b)

10 циклов, лучшее из 3: 2,77 с на цикл

Альтернативный метод:

# Create a dataframe from a series of tuples
def method_a(v):
    return v.split(':')
%timeit -n10 -r3 pd.DataFrame(df.D.apply(method_a).tolist(), columns=columns)

10 циклов, лучшее из 3: 8,85 мс на цикл

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


Это действительно полезно! Я получил 30-кратное ускорение по сравнению с методами, возвращающими функции.
Пушкар Нимкар

9

Принятое решение будет очень медленным для большого количества данных. Решение с наибольшим количеством голосов является немного сложным для чтения, а также медленным с числовыми данными. Если бы каждый новый столбец можно было вычислить независимо от других, я бы просто назначил каждый из них напрямую, не используяapply .

Пример с поддельными символами

Создать 100 000 строк в DataFrame

df = pd.DataFrame(np.random.choice(['he jumped', 'she ran', 'they hiked'],
                                   size=100000, replace=True),
                  columns=['words'])
df.head()
        words
0     she ran
1     she ran
2  they hiked
3  they hiked
4  they hiked

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

df['first'] = df['words'].str[0]
df['count_e'] = df['words'].str.count('e')
df['cap'] = df['words'].str.capitalize()
df.head()
        words first  count_e         cap
0     she ran     s        1     She ran
1     she ran     s        1     She ran
2  they hiked     t        2  They hiked
3  they hiked     t        2  They hiked
4  they hiked     t        2  They hiked

Задержки

%%timeit
df['first'] = df['words'].str[0]
df['count_e'] = df['words'].str.count('e')
df['cap'] = df['words'].str.capitalize()
127 ms ± 585 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

def extract_text_features(x):
    return x[0], x.count('e'), x.capitalize()

%timeit df['first'], df['count_e'], df['cap'] = zip(*df['words'].apply(extract_text_features))
101 ms ± 2.96 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

Удивительно, но вы можете повысить производительность, просматривая каждое значение

%%timeit
a,b,c = [], [], []
for s in df['words']:
    a.append(s[0]), b.append(s.count('e')), c.append(s.capitalize())

df['first'] = a
df['count_e'] = b
df['cap'] = c
79.1 ms ± 294 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Еще один пример с поддельными числовыми данными

Создайте 1 миллион случайных чисел и протестируйте powersфункцию сверху.

df = pd.DataFrame(np.random.rand(1000000), columns=['num'])


def powers(x):
    return x, x**2, x**3, x**4, x**5, x**6

%%timeit
df['p1'], df['p2'], df['p3'], df['p4'], df['p5'], df['p6'] = \
       zip(*df['num'].map(powers))
1.35 s ± 83.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Назначение каждого столбца в 25 раз быстрее и очень читабельно:

%%timeit 
df['p1'] = df['num'] ** 1
df['p2'] = df['num'] ** 2
df['p3'] = df['num'] ** 3
df['p4'] = df['num'] ** 4
df['p5'] = df['num'] ** 5
df['p6'] = df['num'] ** 6
51.6 ms ± 1.9 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

Я сделал аналогичный ответ с более подробной информацией о том, почему, applyкак правило, это не тот путь.


8

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

def f(x):
    return pd.Series([x**2, x**3])

А затем используйте apply следующим образом для создания отдельных столбцов:

df[['x**2','x**3']] = df.apply(lambda row: f(row['x']), axis=1)

1

вы можете вернуть всю строку вместо значений:

df = df.apply(extract_text_features,axis = 1)

где функция возвращает строку

def extract_text_features(row):
      row['new_col1'] = value1
      row['new_col2'] = value2
      return row

Нет, я не хочу применять extract_text_featuresк каждому столбцу df, только к текстовому столбцуdf.textcol
smci

-2
def myfunc(a):
    return a * a

df['New Column'] = df['oldcolumn'].map(myfunc))

Это сработало для меня. Новый столбец будет создан с обработанными данными старого столбца.


2
Это не возвращает «несколько новых столбцов»
Педрам Башири

Это не возвращает «несколько новых столбцов», поэтому не отвечает на вопрос. Не могли бы вы удалить его?
SMCI
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.