Trả về nhiều cột từ gấu trúc áp dụng ()


103

Tôi có một con gấu trúc DataFrame , df_test. Nó chứa một cột 'kích thước' đại diện cho kích thước tính bằng byte. Tôi đã tính toán KB, MB và GB bằng mã sau:

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]

Tôi đã chạy điều này hơn 120.000 hàng và thời gian mất khoảng 2,97 giây mỗi cột * 3 = ~ 9 giây theo% timeit.

Có cách nào tôi có thể làm cho việc này nhanh hơn không? Ví dụ: tôi có thể thay vì trả lại một cột tại một thời điểm từ áp dụng và chạy nó 3 lần, tôi có thể trả lại cả ba cột trong một lần vượt qua để chèn trở lại khung dữ liệu ban đầu không?

Các câu hỏi khác mà tôi đã tìm thấy đều muốn nhận nhiều giá trị và trả về một giá trị duy nhất . Tôi muốn lấy một giá trị duy nhất và trả về nhiều cột .

Câu trả lời:


116

Đây là một câu hỏi cũ, nhưng để hoàn chỉnh, bạn có thể trả về một Chuỗi từ hàm được áp dụng có chứa dữ liệu mới, tránh phải lặp lại ba lần. Việc axis=1chuyển đến hàm áp dụng sẽ áp dụng hàm sizescho mỗi hàng của khung dữ liệu, trả về một chuỗi để thêm vào khung dữ liệu mới. Chuỗi này chứa các giá trị mới cũng như dữ liệu gốc.

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
Tôi ngạc nhiên vì nó đã dành gần 2 năm mà không có câu trả lời chính xác. Tôi đang tìm kiếm thứ khác và tình cờ gặp phải điều này. Hy vọng nó không quá muộn để hữu ích!
Nelz11

10
Là gì rows_listtrong câu trả lời này?
David Stansby

Nó chỉ là danh sách các Series để xây dựng Dataframe.
Nelz 11

1
Nếu pd.Series cần một chỉ mục, bạn cần cung cấp cho nó pd.Series(data, index=...). Nếu không, bạn sẽ gặp phải lỗi khó hiểu khi cố gắng gán kết quả trở lại khung dữ liệu mẹ.
smci

96

Sử dụng apply và zip sẽ nhanh gấp 3 lần so với cách Series.

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

Kết quả kiểm tra là:

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

Tôi ngạc nhiên vì điều này đã không nhận được nhiều ủng hộ hơn. Cảm ơn bạn đã chia sẻ dữ liệu thời gian và biến thể bổ sung.
gumption

Bạn có thể vui lòng giải thích cách bạn đã trả lại tuple? Nó có vẻ là lựa chọn nhanh nhất
Camilo

Vui lòng tham khảo mã mẫu của tôi, đó là cách tuple.
Jesse

có vẻ nhanh nhất và dễ dàng nhất. ngạc nhiên, tôi không thể tự tìm thấy nó.
Shahir Ansari

59

Một số câu trả lời hiện tại hoạt động tốt, nhưng tôi muốn cung cấp một tùy chọn khác, có thể là "pandify" hơn. Điều này phù hợp với tôi với gấu trúc 0.23 hiện tại (không chắc liệu nó có hoạt động trong các phiên bản trước hay không):

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

Lưu ý rằng thủ thuật nằm trên result_typetham số của apply, điều đó sẽ mở rộng kết quả của nó thành một DataFramecó thể được gán trực tiếp cho các cột mới / cũ.


1
Đúng vậy ... xin lỗi ... sau khi một số kiểm tra, nó dos làm việc với 0.22 trong một số trường hợp, nhưng tôi đang ở trong một môi trường ảo và thực sự chạy 0,23 khi tôi đã cố gắng mà ...: /
jaumebonet

5
Đây là câu trả lời tối ưu nhất. Cảm ơn bạn
AdR

17

Chỉ là một cách dễ đọc khác. Mã này sẽ thêm ba cột mới và giá trị của nó, trả về chuỗi mà không sử dụng tham số trong hàm áp dụng.

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)

Ví dụ tổng quát từ: 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

Câu trả lời thực sự thú vị! Cảm ơn Jesse và jaumebonet! Chỉ là một số quan sát liên quan đến:

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

Mặc dù mở rộng là loại thanh lịch hơn ( pandifyed ), zip nhanh hơn ít nhất ** 2 lần . Trong ví dụ đơn giản dưới đây, tôi đã nhanh hơn gấp 4 lần .

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

Hiệu suất giữa các câu trả lời hàng đầu rất khác nhau và Jesse & famaral42 đã thảo luận về điều này, nhưng đáng để chia sẻ một so sánh công bằng giữa các câu trả lời hàng đầu và giải thích chi tiết tinh tế nhưng quan trọng trong câu trả lời của Jesse: lập luận được chuyển đến chức năng, cũng ảnh hưởng đến hiệu suất .

(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

Đây là kết quả:

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

Lưu ý rằng trả về bộ giá trị là phương pháp nhanh nhất, nhưng những gì được chuyển vào dưới dạng đối số, cũng ảnh hưởng đến hiệu suất. Sự khác biệt trong mã là nhỏ nhưng cải thiện hiệu suất là đáng kể.

Bài kiểm tra số 4 (chuyển một giá trị) nhanh gấp đôi so với bài kiểm tra số 3 (chuyển trong một chuỗi), mặc dù hoạt động được thực hiện có vẻ giống hệt nhau.

Nhưng còn nhiều ...

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

Trong một số trường hợp (# 1a và # 4a), việc áp dụng hàm cho DataFrame trong đó các cột đầu ra đã tồn tại sẽ nhanh hơn việc tạo chúng từ hàm.

Đây là mã để chạy các bài kiểm tra:

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

Cảm ơn bạn đã chia nhỏ các đặc điểm hiệu suất!
PaulMest

3

Tôi tin rằng phiên bản 1.1 phá vỡ hành vi được đề xuất trong câu trả lời hàng đầu ở đây.

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)

Đoạn mã trên chạy trên pandas 1.1.0 trả về:

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

Trong khi ở gấu trúc 1.0.5, nó trả về:

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

Tôi nghĩ đó là những gì bạn mong đợi.

Không chắc các ghi chú phát hành giải thích hành vi này như thế nào , tuy nhiên như được giải thích ở đây, việc tránh đột biến các hàng gốc bằng cách sao chép chúng sẽ phục hồi hành vi cũ. I E:

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

Tôi nghĩ rằng mẫu mã của bạn có thể đã bị lỗi sao chép / dán. Bạn có thể kiểm tra xem đó có phải là những gì bạn định gửi không?
PaulMest

1
Cảm ơn @PaulMest bạn đã đúng. Tôi đã sửa hai lỗi chính tả và thêm một liên kết / tham chiếu mới để trả lời câu hỏi.
moo,

1
Chào mừng bạn đến với Stack Overflow! @moo
PaulMest

1

Nói chung, để trả về nhiều giá trị, đây là những gì tôi làm

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)

Việc trả lại khung dữ liệu chắc chắn có các đặc quyền của nó, nhưng đôi khi không bắt buộc. Bạn có thể xem kết apply()quả trả về và chơi một chút với các hàm;)


Cảm ơn vì mẫu này. Tuy nhiên, điều này không xuất ra một khung dữ liệu duy nhất cho tất cả các kết quả. Khi tôi cố gắng thêm nó trở lại khung dữ liệu ban đầu, tôi nhận được "ValueError: mảng không thể phát sóng để sửa hình dạng".
PaulMest

Bạn có thể cung cấp mã để tạo ra một số mẫu dữ liệu nhỏ không?
FooBar

Điều chắc chắn. Tôi vừa cập nhật mã trong bài đăng ban đầu của mình để bao gồm dữ liệu mẫu và đầu ra.
PaulMest

0

Nó cung cấp một khung dữ liệu mới với hai cột từ khung ban đầu.

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)
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.