ส่งคืนหลายคอลัมน์จากการใช้แพนด้า ()


113

ฉันมี DataFrame แพนด้า, df_test. ประกอบด้วยคอลัมน์ 'ขนาด' ซึ่งแสดงขนาดเป็นไบต์ ฉันคำนวณ KB, MB และ GB โดยใช้รหัสต่อไปนี้:

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 ครั้งฉันสามารถส่งคืนทั้งสามคอลัมน์ในครั้งเดียวเพื่อแทรกกลับเข้าไปในดาต้าเฟรมเดิมได้หรือไม่

คำถามอื่น ๆ ที่ฉันได้พบทุกคนต้องการที่จะใช้เวลาหลายค่าและกลับค่าเดียว ฉันต้องการที่จะใช้ค่าเดียวและกลับหลายคอลัมน์

คำตอบ:


130

นี่เป็นคำถามเก่า แต่เพื่อความสมบูรณ์คุณสามารถส่งคืนชุดข้อมูลจากฟังก์ชันที่ใช้ซึ่งมีข้อมูลใหม่ซึ่งป้องกันไม่ให้ต้องทำซ้ำสามครั้ง การส่งผ่านaxis=1ไปยังฟังก์ชันใช้จะนำฟังก์ชัน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)

12
ฉันประหลาดใจที่ใช้เวลาเกือบ 2 ปีโดยไม่มีคำตอบที่ถูกต้อง ฉันกำลังมองหาอย่างอื่นและสะดุดกับสิ่งนี้ หวังว่าคงไม่สายเกินไปที่จะเป็นประโยชน์!
Nelz11

11
คืออะไรrows_listในคำตอบนี้หรือไม่?
David Stansby

มันเป็นเพียงรายการของ Series ในการสร้าง Dataframe
Nelz11

1
หากต้องการ pd.Series pd.Series(data, index=...)ดัชนีคุณจะต้องจัดหาด้วย มิฉะนั้นคุณจะได้รับข้อผิดพลาดที่เป็นความลับเมื่อพยายามกำหนดผลลัพธ์กลับไปที่ดาต้าเฟรมหลัก
smci

105

ใช้ Apply และ zip จะเร็วกว่า Series 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

ฉันประหลาดใจที่ไม่ได้รับการโหวตเพิ่มขึ้น ขอขอบคุณที่แบ่งปันข้อมูลตัวเลือกเพิ่มเติมและเวลา
gumption

คุณช่วยอธิบายได้ไหมว่าคุณส่งคืนทูเปิลได้อย่างไร ดูเหมือนว่าจะเป็นตัวเลือกที่เร็วที่สุด
Camilo

โปรดดูโค้ดตัวอย่างของฉันซึ่งเป็นวิธีการทูเพิล
Jesse

ดูเหมือนเร็วและง่ายที่สุดด้วย ประหลาดใจที่ฉันไม่พบมันด้วยตัวเอง
Shahir Ansari

65

คำตอบในปัจจุบันบางส่วนใช้งานได้ดี แต่ฉันต้องการเสนอตัวเลือกอื่นที่อาจจะเป็นตัวเลือก "pandifyed" สิ่งนี้ใช้ได้กับฉันกับแพนด้าปัจจุบัน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
ที่เหมาะสม ... ขออภัย ... หลังจากการตรวจสอบบางส่วนก็ dos ทำงานร่วมกับ 0.22 ในบางกรณี แต่ผมอยู่ในสภาพแวดล้อมเสมือนจริงและการทำงาน 0.23 เมื่อฉันพยายามที่ ... : /
jaumebonet

7
นี่คือคำตอบที่เหมาะสมที่สุด ขอบคุณ
AdR

นี่ควรเป็นคำตอบที่ได้รับการยอมรับ!
B.Bogart

18

อีกวิธีหนึ่งที่อ่านได้ รหัสนี้จะเพิ่มคอลัมน์ใหม่สามคอลัมน์และค่าของมันโดยส่งกลับชุดข้อมูลโดยไม่ใช้พารามิเตอร์ในฟังก์ชันใช้

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

10

คำตอบเด็ดจริง! ขอบคุณ Jesse และ jaumebonet! ข้อสังเกตบางประการเกี่ยวกับ:

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

แม้ว่าจะขยายเป็นชนิดของสง่างามมากขึ้น ( pandifyed ) ซิปอย่างน้อย ** 2x เร็วขึ้น ตัวอย่างการร้องแบบนี้ผมได้4x เร็วขึ้น

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

9

ประสิทธิภาพระหว่างคำตอบยอดนิยมมีความแตกต่างกันอย่างมีนัยสำคัญและ Jesse & famaral42 ได้กล่าวถึงเรื่องนี้แล้ว แต่ก็คุ้มค่าที่จะแบ่งปันการเปรียบเทียบที่ยุติธรรมระหว่างคำตอบยอดนิยมและการอธิบายรายละเอียดที่ละเอียดอ่อน แต่สำคัญของคำตอบของเจสซี: การโต้แย้งส่งผ่านไปยัง ฟังก์ชั่นยังมีผลต่อประสิทธิภาพการทำงาน

(Python 3.7.4, นุ่น 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)

แจ้งให้ทราบว่า tuples กลับเป็นวิธีที่เร็วที่สุด แต่สิ่งที่ถูกส่งผ่านไปในเป็นอาร์กิวเมนต์ยังมีผลต่อประสิทธิภาพการทำงาน ความแตกต่างในรหัสมีความละเอียดอ่อน แต่การปรับปรุงประสิทธิภาพมีความสำคัญ

การทดสอบ # 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)

โค้ดด้านบนรันบนผลตอบแทนแพนด้า 1.1.0:

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

ในขณะที่แพนด้า 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)

การส่งคืน dataframe มีสิทธิประโยชน์อย่างแน่นอน แต่บางครั้งก็ไม่จำเป็น คุณสามารถดูสิ่งที่apply()ผลตอบแทนและเล่นกับฟังก์ชั่นเล็กน้อย;)


ขอบคุณสำหรับตัวอย่างนี้ อย่างไรก็ตามสิ่งนี้ไม่ได้ส่งออกดาต้าเฟรมเดียวสำหรับผลลัพธ์ทั้งหมด เมื่อฉันพยายามเพิ่มกลับเข้าไปในดาต้าเฟรมเดิมฉันได้รับ "ValueError: array ไม่สามารถถ่ายทอดเพื่อแก้ไขรูปร่างได้"
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)
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.