パンダから複数の列を返しますapply()


100

私はパンダの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 =%timeitによると約9秒かかります。

とにかくこれを速くすることができますか?たとえば、適用から一度に1つの列を返し、それを3回実行する代わりに、3つの列すべてを1つのパスで返し、元のデータフレームに挿入して戻すことはできますか?

私が見つけた他の質問はすべて、複数の値取り、単一の値を返したいと思っています単一の値取り、複数の列を返したいのですが。

回答:


114

これは古い質問ですが、完全にするために、新しいデータを含む適用された関数からSeriesを返すことができます。これにより、3回繰り返す必要がなくなります。axis=1apply関数に渡すと、sizesデータフレームの各行に関数が適用され、新しいデータフレームに追加するシリーズが返されます。このシリーズsには、新しい値と元のデータが含まれています。

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、2016

10
rows_listこの答えには何がありますか?
David Stansby

データフレームを構築するためのシリーズのリストにすぎません。
Nelz11

1
pd.Seriesがインデックスを必要とする場合、インデックスを指定する必要がありますpd.Series(data, index=...)。そうしないと、結果を親データフレームに割り当てようとすると、不可解なエラーが発生します。
smci

92

applyと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

これがこれ以上の賛成票を受け取っていないことに驚いています。追加のバリアントとタイミングデータを共有していただきありがとうございます。
ガンプション

タプルを返す方法を説明していただけますか?最速のオプションのようです
Camilo

私のサンプルコードを参照してください、それはタプルの方法です。
ジェシー

最速かつ簡単なようです。びっくりしました。
Shahir Ansari

52

現在の返信のいくつかは問題なく機能しますが、別の、おそらく「より詳細な」オプションを提供したいと思います。これは、現在のパンダ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

4
これが最も最適な答えです。ありがとう
AdR

16

ただ別の読みやすい方法。このコードは、3つの新しい列とその値を追加し、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

本当にクールな答え!ジェシーとジャウメボネットに感謝!以下に関するいくつかの観察のみ:

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

展開はよりエレガント(パンディファイド)ですが、zipは少なくとも** 2倍高速です。以下の簡単な例では、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

トップの答え間のパフォーマンスが大幅に変化させ、そしてジェシー&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(連続して渡す)の2倍の速さです。

しかし、もっとあります...

# 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

2

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

パンダ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あなたは正しかった。私は2つのタイプミスを修正し、質問が回答される新しいリンク/参照を追加しました。
moo

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、2014年

小さなデータサンプルを生成するコードを提供できますか?
FooBar 2014年

確実なこと。元の投稿のコードを更新して、サンプルデータと出力を含めました。
PaulMest、2014年

0

元の列から2列の新しいデータフレームが作成されます。

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.