Застосувати функцію панд до стовпця, щоб створити кілька нових стовпців?


215

Як це зробити в пандах:

У мене є функція extract_text_featuresв одному текстовому стовпці, повертаючи кілька вихідних стовпців. Зокрема, функція повертає 6 значень.

Функція працює, однак, здається, немає відповідного типу повернення (pandas DataFrame / numpy масив / список Python), таким чином, щоб висновок міг бути правильно призначений 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
Тед Петру

Більшість числових операцій з пандами можуть бути векторизованими - це означає, що вони набагато швидші, ніж звичайні ітерації. ОТОН, деякі операції (такі як рядок і регулярний вираз), за своєю суттю важко векторизувати. У цьому випадку важливо зрозуміти, як здійснювати циклічність своїх даних. Більш детальну інформацію про те, коли і як робити циклічну передачу ваших даних, будь ласка, прочитайте Про петлі з Pandas - Коли мені потрібно піклуватися? .
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

EDIT: Будь ласка, пам’ятайте про величезне споживання пам’яті та низьку швидкість: 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 (лямбда 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]
ostrokach

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 () для підключення нових cols до вихідного фрейму даних.
кмин

2
приємна відповідь, вам не потрібно використовувати dict або merd, якщо ви вказуєте стовпці поза додатком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 (example (df), axis = 1) виправте мене, якщо я помиляюся, я просто новачок
user299791

1
@ user299791, Ні в цьому випадку ви розглядаєте приклад як об'єкт першого класу, тому ви переходите в саму функцію. Ця функція застосовуватиметься до кожного рядка.
Майкл Девід Уотсон

привіт Майкл, твоя відповідь допомогла мені в моїй проблемі. Однозначно ваше рішення краще, ніж оригінальний метод df.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
тар

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])

А потім скористайтеся застосувати так, щоб створити окремі стовпці:

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
Це не повертає "кілька нових стовпців"
pedram bashiri

Це не повертає "кілька нових стовпців", тому це не відповідає на запитання. Не могли б ви видалити його?
smci
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.