Повернути кілька стовпців з панд apply ()


103

У мене є панди DataFrame, df_test. Він містить стовпець "розмір", який представляє розмір у байтах. Я розрахував КБ, МБ та ГБ, використовуючи такий код:

df_test = pd.DataFrame([
    {'dir': '/Users/uname1', 'size': 994933},
    {'dir': '/Users/uname2', 'size': 109338711},
])

df_test['size_kb'] = df_test['size'].astype(int).apply(lambda x: locale.format("%.1f", x / 1024.0, grouping=True) + ' KB')
df_test['size_mb'] = df_test['size'].astype(int).apply(lambda x: locale.format("%.1f", x / 1024.0 ** 2, grouping=True) + ' MB')
df_test['size_gb'] = df_test['size'].astype(int).apply(lambda x: locale.format("%.1f", x / 1024.0 ** 3, grouping=True) + ' GB')

df_test


             dir       size       size_kb   size_mb size_gb
0  /Users/uname1     994933      971.6 KB    0.9 MB  0.0 GB
1  /Users/uname2  109338711  106,776.1 KB  104.3 MB  0.1 GB

[2 rows x 5 columns]

Я провів це понад 120 000 рядків, і час займає близько 2,97 секунди на стовпець * 3 = ~ 9 секунд відповідно до% timeit.

Чи можу я якось зробити це швидше? Наприклад, чи можу я замість того, щоб повертати по одному стовпцю за раз із застосувати і запускати його 3 рази, чи можу я повернути всі три стовпці за один прохід, щоб вставити їх назад у вихідний фрейм даних?

В інших питаннях, які я знайшов, усі хочуть взяти кілька значень і повернути одне значення . Я хочу взяти одне значення і повернути кілька стовпців .

Відповіді:


116

Це старе запитання, але для повноти ви можете повернути серію із застосованої функції, яка містить нові дані, запобігаючи необхідності тричі повторювати. Перехід axis=1до функції apply застосовує функцію sizesдо кожного рядка кадру даних, повертаючи серію для додавання до нового кадру даних. Ці серії містять нові значення, а також вихідні дані.

def sizes(s):
    s['size_kb'] = locale.format("%.1f", s['size'] / 1024.0, grouping=True) + ' KB'
    s['size_mb'] = locale.format("%.1f", s['size'] / 1024.0 ** 2, grouping=True) + ' MB'
    s['size_gb'] = locale.format("%.1f", s['size'] / 1024.0 ** 3, grouping=True) + ' GB'
    return s

df_test = df_test.append(rows_list)
df_test = df_test.apply(sizes, axis=1)

11
Я здивований, що він провів майже 2 роки без правильної відповіді. Я шукав щось інше і наткнувся на це. Сподіваюся, ще не пізно бути корисним!
Nelz11

10
Що rows_listв цій відповіді?
Девід Стенсбі,

Це лише список Серій для побудови Dataframe.
Nelz11,

1
Якщо pd.Series потрібен індекс, вам потрібно надати його pd.Series(data, index=...). В іншому випадку ви отримуєте загадкові помилки при спробі призначити результат назад у батьківський фрейм даних.
smci

96

Використовуйте застосувати і zip буде в 3 рази швидше, ніж серійний спосіб.

def sizes(s):    
    return locale.format("%.1f", s / 1024.0, grouping=True) + ' KB', \
        locale.format("%.1f", s / 1024.0 ** 2, grouping=True) + ' MB', \
        locale.format("%.1f", s / 1024.0 ** 3, grouping=True) + ' GB'
df_test['size_kb'],  df_test['size_mb'], df_test['size_gb'] = zip(*df_test['size'].apply(sizes))

Результатом тесту є:

Separate df.apply(): 

    100 loops, best of 3: 1.43 ms per loop

Return Series: 

    100 loops, best of 3: 2.61 ms per loop

Return tuple:

    1000 loops, best of 3: 819 µs per loop

Я здивований, що це не отримало більше голосів. Дякуємо, що поділилися додатковими даними варіанта та часу.
гумпція

Не могли б ви пояснити, як ви повернули кортеж? Здається, це найшвидший варіант
Каміло

Будь ласка, зверніться до мого зразка коду, це кращий спосіб.
Джессі,

здається також найшвидшим та найпростішим. здивований, що я сам не зміг його знайти.
Шахір Ансарі

59

Деякі з поточних відповідей чудово працюють, але я хочу запропонувати інший, можливо, більш "папіфікований" варіант. Це працює для мене з поточними пандами 0,23 (не впевнений, чи буде це працювати в попередніх версіях):

import pandas as pd

df_test = pd.DataFrame([
  {'dir': '/Users/uname1', 'size': 994933},
  {'dir': '/Users/uname2', 'size': 109338711},
])

def sizes(s):
  a = locale.format("%.1f", s['size'] / 1024.0, grouping=True) + ' KB'
  b = locale.format("%.1f", s['size'] / 1024.0 ** 2, grouping=True) + ' MB'
  c = locale.format("%.1f", s['size'] / 1024.0 ** 3, grouping=True) + ' GB'
  return a, b, c

df_test[['size_kb', 'size_mb', 'size_gb']] = df_test.apply(sizes, axis=1, result_type="expand")

Зверніть увагу, що фокус стосується result_typeпараметра apply, який розширить його результат у такий, DataFrameякий можна безпосередньо призначити новим / старим стовпцям.


1
Правильно ... вибачте ... після певної перевірки в деяких випадках він працював з 0,22, але я був у віртуальному середовищі і насправді працював 0,23, коли пробував це ...: /
jaumebonet

5
Це найбільш оптимальна відповідь. Дякую
AdR

17

Просто ще один читабельний спосіб. Цей код додасть три нові стовпці та його значення, повертаючи ряди без використання параметрів у функції apply.

def sizes(s):

    val_kb = locale.format("%.1f", s['size'] / 1024.0, grouping=True) + ' KB'
    val_mb = locale.format("%.1f", s['size'] / 1024.0 ** 2, grouping=True) + ' MB'
    val_gb = locale.format("%.1f", s['size'] / 1024.0 ** 3, grouping=True) + ' GB'
    return pd.Series([val_kb,val_mb,val_gb],index=['size_kb','size_mb','size_gb'])

df[['size_kb','size_mb','size_gb']] = df.apply(lambda x: sizes(x) , axis=1)

Загальний приклад із: https://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.apply.html

df.apply(lambda x: pd.Series([1, 2], index=['foo', 'bar']), axis=1)

#foo  bar
#0    1    2
#1    1    2
#2    1    2

9

Дійсно круті відповіді! Дякую Джессі та jaumebonet! Лише деяке спостереження щодо:

  • zip(* ...
  • ... result_type="expand")

Незважаючи на те, що розширення є начебто більш елегантним ( паніфікованим ), zip принаймні ** вдвічі швидший . На цьому простому прикладі, наведеному нижче, я отримав в 4 рази швидше .

import pandas as pd

dat = [ [i, 10*i] for i in range(1000)]

df = pd.DataFrame(dat, columns = ["a","b"])

def add_and_sub(row):
    add = row["a"] + row["b"]
    sub = row["a"] - row["b"]
    return add, sub

df[["add", "sub"]] = df.apply(add_and_sub, axis=1, result_type="expand")
# versus
df["add"], df["sub"] = zip(*df.apply(add_and_sub, axis=1))

8

Ефективність між найкращими відповідями істотно різниться, і Jesse & famaral42 це вже обговорювали, але варто поділитися справедливим порівнянням між найпопулярнішими відповідями та детально розглянути делікатну, але важливу деталь відповіді Джессі: аргумент, переданий функція, також впливає на продуктивність .

(Python 3.7.4, Pandas 1.0.3)

import pandas as pd
import locale
import timeit


def create_new_df_test():
    df_test = pd.DataFrame([
      {'dir': '/Users/uname1', 'size': 994933},
      {'dir': '/Users/uname2', 'size': 109338711},
    ])
    return df_test


def sizes_pass_series_return_series(series):
    series['size_kb'] = locale.format_string("%.1f", series['size'] / 1024.0, grouping=True) + ' KB'
    series['size_mb'] = locale.format_string("%.1f", series['size'] / 1024.0 ** 2, grouping=True) + ' MB'
    series['size_gb'] = locale.format_string("%.1f", series['size'] / 1024.0 ** 3, grouping=True) + ' GB'
    return series


def sizes_pass_series_return_tuple(series):
    a = locale.format_string("%.1f", series['size'] / 1024.0, grouping=True) + ' KB'
    b = locale.format_string("%.1f", series['size'] / 1024.0 ** 2, grouping=True) + ' MB'
    c = locale.format_string("%.1f", series['size'] / 1024.0 ** 3, grouping=True) + ' GB'
    return a, b, c


def sizes_pass_value_return_tuple(value):
    a = locale.format_string("%.1f", value / 1024.0, grouping=True) + ' KB'
    b = locale.format_string("%.1f", value / 1024.0 ** 2, grouping=True) + ' MB'
    c = locale.format_string("%.1f", value / 1024.0 ** 3, grouping=True) + ' GB'
    return a, b, c

Ось результати:

# 1 - Accepted (Nels11 Answer) - (pass series, return series):
9.82 ms ± 377 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

# 2 - Pandafied (jaumebonet Answer) - (pass series, return tuple):
2.34 ms ± 48.6 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

# 3 - Tuples (pass series, return tuple then zip):
1.36 ms ± 62.8 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

# 4 - Tuples (Jesse Answer) - (pass value, return tuple then zip):
752 µs ± 18.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Зверніть увагу , як повернення кортежі це найшвидший метод, але то , що передається в якості аргументу, також впливає на продуктивність. Різниця в коді незначна, але покращення продуктивності суттєве.

Тест №4 (передача в одному значенні) вдвічі швидший, ніж тест №3 (передача у серії), хоча виконана операція нібито ідентична.

Але є ще ...

# 1a - Accepted (Nels11 Answer) - (pass series, return series, new columns exist):
3.23 ms ± 141 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

# 2a - Pandafied (jaumebonet Answer) - (pass series, return tuple, new columns exist):
2.31 ms ± 39.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

# 3a - Tuples (pass series, return tuple then zip, new columns exist):
1.36 ms ± 58.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

# 4a - Tuples (Jesse Answer) - (pass value, return tuple then zip, new columns exist):
694 µs ± 3.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

У деяких випадках (# 1a та # 4a) застосування функції до DataFrame, у якому вже існують вихідні стовпці, відбувається швидше, ніж їх створення з функції.

Ось код для запуску тестів:

# Paste and run the following in ipython console. It will not work if you run it from a .py file.
print('\nAccepted Answer (pass series, return series, new columns dont exist):')
df_test = create_new_df_test()
%timeit result = df_test.apply(sizes_pass_series_return_series, axis=1)
print('Accepted Answer (pass series, return series, new columns exist):')
df_test = create_new_df_test()
df_test = pd.concat([df_test, pd.DataFrame(columns=['size_kb', 'size_mb', 'size_gb'])])
%timeit result = df_test.apply(sizes_pass_series_return_series, axis=1)

print('\nPandafied (pass series, return tuple, new columns dont exist):')
df_test = create_new_df_test()
%timeit df_test[['size_kb', 'size_mb', 'size_gb']] = df_test.apply(sizes_pass_series_return_tuple, axis=1, result_type="expand")
print('Pandafied (pass series, return tuple, new columns exist):')
df_test = create_new_df_test()
df_test = pd.concat([df_test, pd.DataFrame(columns=['size_kb', 'size_mb', 'size_gb'])])
%timeit df_test[['size_kb', 'size_mb', 'size_gb']] = df_test.apply(sizes_pass_series_return_tuple, axis=1, result_type="expand")

print('\nTuples (pass series, return tuple then zip, new columns dont exist):')
df_test = create_new_df_test()
%timeit df_test['size_kb'],  df_test['size_mb'], df_test['size_gb'] = zip(*df_test.apply(sizes_pass_series_return_tuple, axis=1))
print('Tuples (pass series, return tuple then zip, new columns exist):')
df_test = create_new_df_test()
df_test = pd.concat([df_test, pd.DataFrame(columns=['size_kb', 'size_mb', 'size_gb'])])
%timeit df_test['size_kb'],  df_test['size_mb'], df_test['size_gb'] = zip(*df_test.apply(sizes_pass_series_return_tuple, axis=1))

print('\nTuples (pass value, return tuple then zip, new columns dont exist):')
df_test = create_new_df_test()
%timeit df_test['size_kb'],  df_test['size_mb'], df_test['size_gb'] = zip(*df_test['size'].apply(sizes_pass_value_return_tuple))
print('Tuples (pass value, return tuple then zip, new columns exist):')
df_test = create_new_df_test()
df_test = pd.concat([df_test, pd.DataFrame(columns=['size_kb', 'size_mb', 'size_gb'])])
%timeit df_test['size_kb'],  df_test['size_mb'], df_test['size_gb'] = zip(*df_test['size'].apply(sizes_pass_value_return_tuple))

Дякуємо, що також розбили експлуатаційні характеристики!
PaulMest

3

Я вважаю, що версія 1.1 порушує поведінку, запропоновану у відповіді тут.

import pandas as pd
def test_func(row):
    row['c'] = str(row['a']) + str(row['b'])
    row['d'] = row['a'] + 1
    return row

df = pd.DataFrame({'a': [1, 2, 3], 'b': ['i', 'j', 'k']})
df.apply(test_func, axis=1)

Наведений вище код, запущений на pandas 1.1.0, повертає:

   a  b   c  d
0  1  i  1i  2
1  1  i  1i  2
2  1  i  1i  2

Перебуваючи в pandas 1.0.5, він повернувся:

   a   b    c  d
0  1   i   1i  2
1  2   j   2j  3
2  3   k   3k  4

Я думаю, що це те, чого ви очікували.

Не впевнені, як примітки до випуску пояснюють таку поведінку, однак, як пояснено тут, уникнення мутації вихідних рядків шляхом їх копіювання відроджує стару поведінку. тобто:

def test_func(row):
    row = row.copy()   #  <---- Avoid mutating the original reference
    row['c'] = str(row['a']) + str(row['b'])
    row['d'] = row['a'] + 1
    return row

Я думаю, що у вашому зразку коду могла бути помилка копіювання / вставки. Не могли б ви перевірити це і побачити, чи це ви мали намір подати?
PaulMest

1
Дякую @PaulMest, що ви мали рацію. Я виправив дві помилки і додав нове посилання / посилання, де відповідає на запитання.
му

1
Ласкаво просимо до Stack Overflow! @moo
PaulMest

1

Як правило, щоб повернути декілька значень, це те, що я роблю

def gimmeMultiple(group):
    x1 = 1
    x2 = 2
    return array([[1, 2]])
def gimmeMultipleDf(group):
    x1 = 1
    x2 = 2
    return pd.DataFrame(array([[1,2]]), columns=['x1', 'x2'])
df['size'].astype(int).apply(gimmeMultiple)
df['size'].astype(int).apply(gimmeMultipleDf)

Повернення кадру даних остаточно має свої переваги, але іноді не потрібно. Ви можете подивитися, що apply()повертається, і трохи пограти з функціями;)


Дякую за цей зразок. Однак це не виводить жодного кадру даних для всіх результатів. Коли я намагаюся додати його назад до початкового фрейму даних, я отримую "ValueError: масив не можна транслювати для виправлення фігури".
PaulMest

Чи можете ви надати код для отримання невеликої вибірки даних?
FooBar

Звісна річ. Я щойно оновив код у своєму початковому дописі, щоб включити зразкові дані та результати.
PaulMest

0

Він дає новий фрейм даних із двома стовпцями від вихідного.

import pandas as pd
df = ...
df_with_two_columns = df.apply(lambda row:pd.Series([row['column_1'], row['column_2']], index=['column_1', 'column_2']),axis = 1)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.