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

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 . Следовательно, большая часть вопроса и ответов не слишком актуальны.

SMCI
источник
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/ !

Zelazny7
источник
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
ostrokach
источник
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
user1827356
источник
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. Мне было бы интересно услышать мнение людей, хотя, если есть ошибка в моей работе.

RFox
источник
Это действительно полезно! Я получил 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))

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

user2902302
источник
2
Это не возвращает «несколько новых столбцов»
Педрам Башири
Это не возвращает «несколько новых столбцов», поэтому не отвечает на вопрос. Не могли бы вы удалить его?
SMCI