複数の新しい列を作成するためにパンダ関数を列に適用しますか?


215

パンダでこれを行う方法:

extract_text_features単一のテキスト列に関数があり、複数の出力列を返します。具体的には、この関数は6つの値を返します。

関数は機能しますが、出力が正しく割り当てられるような適切な戻り値の型(pandas DataFrame / numpy array / Pythonリスト)はないようです df.ix[: ,10:16] = df.textcol.map(extract_text_features)

だから私はこれdf.iterrows()、での反復に戻る必要があると思いますか?

更新:での反復df.iterrows()は少なくとも20倍遅くなるため、関数を引き渡して関数を6つの異なる.map(lambda ...)呼び出しに分割しました。

更新2:この質問はv0.11.0前後で尋ねられました。したがって、質問と回答の多くはあまり関連性がありません。


1
私はあなたがそれを書いたように複数の割り当てを行うことができないと思います:df.ix[: ,10:16]。あなたはmergeあなたの特徴をデータセットに入れなければならないでしょう。
Zelazny7 2013

1
よりパフォーマンスの高いソリューションが必要な場合は、以下を使用しないでくださいapply
Ted Petrou

パンダを使用するほとんどの数値演算はベクトル化できます。つまり、従来の反復よりもはるかに高速です。OTOH、一部の操作(文字列や正規表現など)は、本質的にベクトル化が困難です。この場合、データをループする方法を理解することが重要です。データのループ処理をいつどのように実行するかについての詳細は、「パンダによるForループ-いつ気にする必要がありますか?」を参照してください。
cs95

@coldspeed:主な問題は、いくつかのオプションの中でどちらがより高いパフォーマンスであるかを選択することではなく、v0.11.0前後でこれをまったく機能させるためのパンダ構文との戦いでした
smci

実際、コメントは、反復的な解決策を探している、これ以上何も知らない、または何をしているかを知っている将来の読者を対象としています。
cs95

回答:


109

user1827356の回答に基づいて、次を使用して1つのパスで割り当てを行うことができます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

編集: 大量のメモリ消費と低速に注意してください:https : //ys-l.github.io/posts/2015/08/28/how-not-to-use-pandas-apply/


2
好奇心から、これを行うことで多くのメモリを消費することが予想されますか?2.5milの行を保持するデータフレームでこれを実行していますが、メモリの問題にほぼ遭遇しました(1列だけを返すよりもはるかに遅いです)。
Jeffrey04

2
'df.join(df.textcol.apply(lambda s:pd.Series({' feature1 ':s + 1、' feature2 ':s-1})))'の方が良いオプションだと思います。
Shivam K. Thakkar 2018

@ShivamKThakkarなぜあなたの提案がより良い選択肢になると思いますか?それはあなたが考えるより効率的ですか、それともメモリコストが少ないでしょうか?
tsando 2018年

1
必要な速度とメモリを考慮してください:ys-l.github.io/posts/2015/08/28/how-not-to-use-pandas-apply
Make42

189

私は通常これを使用してこれを行います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
しかし、このように6列ではなく50列を追加した場合はどうしますか?
最大

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

新しいcolsを元のデータフレームに接続する場合、concat()はmerge()よりも単純に見えます。
クミン2017

2
良い答えです。適用外の列を指定する場合、dictやマージを使用する必要はありませんdf[['col1', 'col2']] = df['col3'].apply(lambda x: pd.Series('val1', 'val2'))
Matt

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いいえ、この場合、例をファーストクラスオブジェクトとして扱っているため、関数自体を渡します。この関数は各行に適用されます。
Michael David Watson

こんにちはマイケル、あなたの答えは私の問題を助けてくれました。確かにあなたのソリューションは元のパンダのdf.assign()メソッドより優れています。これは列ごとに1回です。assign()を使用して、2つの新しい列を作成する場合は、df1を使用してdfを操作して新しいcolumn1を取得し、次にdf2を使用してdf1を操作して2番目の新しい列を作成する必要があります...これは非常に単調です。しかし、あなたの方法は私の命を救いました!!! ありがとう!!!
commentallez-vous

1
それは列割り当てコードを行ごとに1回実行しませんか?pd.Series({k:v})Ewanの回答のようにaを返して列の割り当てをシリアル化する方が良いのではないでしょうか。
デニス・デ・Bernardy

29

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 パフォーマンスの問題について常に素敵である
テオRubenach

1
これは良い解決策です。唯一の問題は、新しく追加された2つの列の名前を選択できないことです。後でdf.rename(columns = {0: 'col1'、1: 'col2'})を行う必要があります
pedram bashiri

2
@pedrambashiri渡した関数がをdf.apply返すdict場合、列はキーに従って名前が付けられて出力されます。
セブ

24

使うだけ 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

いいですね、これはシンプルでありながら、きちんと機能します。これは私が探していたものです。ありがとう
Isaac Sim

以前の回答を複製します:stackoverflow.com/a/52363890/823470
タール

22

概要:いくつかの列のみを作成する場合は、df[['new_col1','new_col2']] = df[['data1','data2']].apply( function_of_your_choosing(x), axis=1)

このソリューションでは、作成する新しい列の数は、.apply()関数への入力として使用する列の数と同じである必要があります。他に何かしたい場合は、他の答えを見てください。

詳細 2列のデータフレームがあるとします。最初の列は、10歳のときの人の身長です。2番目は、20歳のときの人物の身長です。

各人の身長の平均と各人の身長の合計の両方を計算する必要があるとします。これは、各行に2つの値です。

これは、すぐに適用される次の関数を使用して実行できます。

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)

[mean、sum]リストを含む新しい列を1つ作成します。これは、別のLambda / Applyが必要になるため、おそらく回避する必要があります。

代わりに、各値を独自の列に分割する必要があります。これを行うには、一度に2つの列を作成します。

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])
カニシュクマー

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

私はこれを行ういくつかの方法を調べてきましたが、ここに示す方法(pandasシリーズを返す)は最も効率的ではないようです。

ランダムデータの大規模なデータフレームから始める場合:

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

私の計算では、一連のタプルを取得してからそれをDataFrameに変換する方がはるかに効率的です。私の仕事に誤りがあったとしても、人々の考えを聞いてみたいと思います。


これは本当に便利です!シリーズメソッドを返す関数と比較して、30倍のスピードアップが得られました。
プシュカルニムカル

9

承認されたソリューションは、大量のデータに対して非常に遅くなります。賛成票の数が最も多いソリューションは、少し読みにくく、数値データの場合も遅くなります。新しい列をそれぞれ独立して計算できる場合は、を使用せずに、各列を直接割り当てるだけapplyです。

偽の文字データの例

DataFrameに100,000個の文字列を作成する

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

元の質問で行ったように、いくつかのテキスト機能を抽出したいとしましょう。たとえば、最初の文字を抽出し、文字「e」の出現を数えて、フレーズを大文字にします。

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)

偽の数値データの別の例

100万の乱数を作成し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

他の2つの同様の質問に同じ回答を投稿しました。私がこれを行うことを好む方法は、一連の関数の戻り値をまとめることです:

def f(x):
    return pd.Series([x**2, x**3])

次に、applyを次のように使用して、個別の列を作成します。

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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.