Terapkan fungsi panda ke kolom untuk membuat beberapa kolom baru?

215

Cara melakukannya di panda:

Saya memiliki fungsi extract_text_featurespada satu kolom teks, menghasilkan banyak kolom keluaran. Secara khusus, fungsi mengembalikan 6 nilai.

Fungsi ini berfungsi, namun sepertinya tidak ada jenis pengembalian yang tepat (panda DataFrame / array numpy / daftar Python) sehingga output dapat ditugaskan dengan benar df.ix[: ,10:16] = df.textcol.map(extract_text_features)

Jadi saya pikir saya harus kembali ke iterating df.iterrows(), seperti ini ?

UPDATE: Iterating dengan df.iterrows()setidaknya 20x lebih lambat, jadi saya menyerah dan membagi fungsi menjadi enam .map(lambda ...)panggilan berbeda .

UPDATE 2: pertanyaan ini ditanyakan kembali sekitar v0.11.0 . Karenanya banyak pertanyaan dan jawaban tidak terlalu relevan.

smci
sumber
1
Saya tidak berpikir Anda dapat melakukan beberapa tugas dengan cara yang Anda memilikinya tertulis: df.ix[: ,10:16]. Saya pikir Anda harus memiliki mergefitur Anda ke dalam dataset.
Zelazny7
1
Bagi mereka yang menginginkan solusi yang jauh lebih baik, periksa yang di bawah ini yang tidak digunakanapply
Ted Petrou
Sebagian besar operasi numerik dengan panda dapat di-vektorisasi - ini berarti mereka jauh lebih cepat daripada iterasi konvensional. OTOH, beberapa operasi (seperti string dan regex) secara inheren sulit untuk diubah menjadi vektor. Dalam hal ini, penting untuk memahami cara melilitkan data Anda. Informasi lebih lanjut tentang kapan dan bagaimana perulangan data Anda harus dilakukan, silakan baca Untuk loop dengan Pandas - Kapan saya harus peduli? .
cs95
@coldspeed: masalah utama adalah tidak memilih yang mana adalah kinerja yang lebih tinggi di antara beberapa opsi, itu melawan panda sintaks untuk membuatnya bekerja sama sekali, kembali sekitar v0.11.0 .
smci
Memang, komentar ini ditujukan untuk pembaca masa depan yang mencari solusi berulang, yang entah tidak tahu apa-apa, atau yang tahu apa yang mereka lakukan.
cs95

Jawaban:

109

Membangun dari jawaban user1827356, Anda dapat melakukan tugas dalam satu pass menggunakan 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

EDIT: Harap perhatikan konsumsi memori yang besar dan kecepatan rendah: https://ys-l.github.io/posts/2015/08/28/how-not-to-use-pandas-apply/ !

Zelazny7
sumber
2
hanya karena penasaran, apakah ini akan menghabiskan banyak memori dengan melakukan ini? Saya melakukan ini pada dataframe yang menampung baris 2.5mil, dan saya hampir mengalami masalah memori (juga jauh lebih lambat daripada mengembalikan hanya 1 kolom).
Jeffrey04
2
'df.join (df.textcol.apply (lambda s: pd.Series ({' feature1 ': s + 1,' feature2 ': s-1})))' akan menjadi pilihan yang lebih baik menurut saya.
Shivam K. Thakkar
@ShivamKThakkar, mengapa menurut Anda saran Anda akan menjadi pilihan yang lebih baik? Apakah akan lebih efisien menurut Anda atau memiliki biaya memori lebih sedikit?
tsando
1
Silakan pertimbangkan kecepatan dan memori yang diperlukan: ys-l.github.io/posts/2015/08/28/how-not-to-use-pandas-apply
Make42
189

Saya biasanya melakukan ini menggunakan 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
ostrokach
sumber
8
Tapi apa yang Anda lakukan jika Anda memiliki 50 kolom yang ditambahkan seperti ini daripada 6?
maks
14
@maxtemp = list(zip(*df['num'].map(powers))); for i, c in enumerate(columns): df[c] = temp[c]
ostrokach
8
@ostrokach, saya pikir maksud Anda for i, c in enumerate(columns): df[c] = temp[i]. Berkat ini, saya benar-benar mendapatkan tujuan enumerate: D
rocarvaj
4
Sejauh ini, ini adalah solusi paling elegan dan mudah dibaca yang pernah saya temui. Kecuali jika Anda mendapatkan masalah kinerja, idiom zip(*df['col'].map(function))mungkin adalah cara untuk pergi.
François Leblanc
84

Inilah yang telah saya lakukan di masa lalu

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

Mengedit untuk kelengkapan

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
pengguna1827356
sumber
concat () terlihat lebih sederhana dari gabungan () untuk menghubungkan cols baru ke kerangka data asli.
cumin
2
jawaban yang bagus, Anda tidak perlu menggunakan dict atau gabungan jika Anda menentukan kolom di luar yang berlakudf[['col1', 'col2']] = df['col3'].apply(lambda x: pd.Series('val1', 'val2'))
Matt
66

Ini adalah cara yang benar dan termudah untuk mencapai hal ini untuk 95% kasus penggunaan:

>>> 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
Michael David Watson
sumber
Anda tidak boleh menulis: df = df.apply (contoh (df), sumbu = 1) koreksi saya jika saya salah, saya hanya seorang pemula
user299791
1
@ user299791, Tidak dalam hal ini Anda memperlakukan contoh sebagai objek kelas satu sehingga Anda meneruskan fungsi itu sendiri. Fungsi ini akan diterapkan ke setiap baris.
Michael David Watson
hai Michael, jawaban Anda membantu saya dalam masalah saya. Jelas solusi Anda lebih baik daripada metode df.assign () panda asli, karena ini satu kali per kolom. Menggunakan assign (), jika Anda ingin membuat 2 kolom baru, Anda harus menggunakan df1 untuk bekerja pada df untuk mendapatkan kolom1 baru, kemudian gunakan df2 untuk bekerja pada df1 untuk membuat kolom baru kedua ... ini cukup monoton. Tapi metodemu menyelamatkan hidupku !!! Terima kasih!!!
commentallez-vous
1
Bukankah itu menjalankan kode tugas kolom sekali per baris? Bukankah lebih baik mengembalikan a pd.Series({k:v})dan membuat serialisasi tugas kolom seperti dalam jawaban Ewan?
Denis de Bernardy
29

Pada 2018, saya menggunakan apply()argumenresult_type='expand'

>>> appiled_df = df.apply(lambda row: fn(row.text), axis='columns', result_type='expand')
>>> df = pd.concat([df, appiled_df], axis='columns')
Ben
sumber
6
Itulah cara Anda melakukannya, saat ini!
Make42
1
Ini berhasil di luar kotak pada tahun 2020 sementara banyak pertanyaan lain tidak. Juga tidak menggunakan pd.Series yang selalu baik tentang masalah kinerja
Théo Rubenach
1
Ini solusi yang bagus. Satu-satunya masalah adalah, Anda tidak dapat memilih nama untuk 2 kolom yang baru ditambahkan. Anda nanti harus melakukan df.rename (kolom = {0: 'col1', 1: 'col2'})
pedram bashiri
2
@pedrambashiri Jika fungsi yang Anda lewati untuk df.applymengembalikan a dict, kolom akan keluar dinamai sesuai dengan tombol.
Seb
24

Gunakan saja 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")
Abhishek
sumber
4
Ini membantu untuk menunjukkan bahwa opsi baru di 0,23 . Pertanyaan itu ditanyakan kembali pada 0,11
smci
Bagus, ini sederhana dan masih berfungsi dengan baik. Ini yang saya cari. Terima kasih
Isaac Sim
Gandakan jawaban sebelumnya: stackoverflow.com/a/52363890/823470
tar
22

Ringkasan: Jika Anda hanya ingin membuat beberapa kolom, gunakandf[['new_col1','new_col2']] = df[['data1','data2']].apply( function_of_your_choosing(x), axis=1)

Untuk solusi ini, jumlah kolom baru yang Anda buat harus sama dengan kolom angka yang Anda gunakan sebagai input ke fungsi .apply (). Jika Anda ingin melakukan hal lain, lihat jawaban lainnya.

Detail Katakanlah Anda memiliki kerangka data dua kolom. Kolom pertama adalah tinggi seseorang ketika mereka berusia 10 tahun; yang kedua adalah tinggi orang ketika mereka berusia 20 tahun.

Misalkan Anda perlu menghitung rata-rata tinggi dan tinggi masing-masing orang. Itu dua nilai per setiap baris.

Anda dapat melakukan ini melalui fungsi berikut yang segera diterapkan:

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]

Anda mungkin menggunakan fungsi ini seperti:

 df[['height_at_age_10','height_at_age_20']].apply(mean_and_sum(x),axis=1)

(Agar lebih jelas: fungsi terapkan ini mengambil nilai dari setiap baris dalam kerangka data yang terdaftar dan mengembalikan daftar.)

Namun, jika Anda melakukan ini:

df['Mean_&_Sum'] = df[['height_at_age_10','height_at_age_20']].apply(mean_and_sum(x),axis=1)

Anda akan membuat 1 kolom baru yang berisi daftar [rata-rata, jumlah], yang mungkin ingin Anda hindari, karena itu akan membutuhkan Lambda / Terapkan lainnya.

Sebagai gantinya, Anda ingin membagi setiap nilai ke dalam kolomnya sendiri. Untuk melakukan ini, Anda dapat membuat dua kolom sekaligus:

df[['Mean','Sum']] = df[['height_at_age_10','height_at_age_20']]
.apply(mean_and_sum(x),axis=1)
Evan W.
sumber
4
Untuk panda 0.23, Anda harus menggunakan sintaks:df["mean"], df["sum"] = df[['height_at_age_10','height_at_age_20']] .apply(mean_and_sum(x),axis=1)
SummerEla
Fungsi ini mungkin menimbulkan kesalahan. Fungsi pengembalian harus return pd.Series([mean,sum])
Kanishk Mair
22

Bagi saya ini berhasil:

Masukan df

df = pd.DataFrame({'col x': [1,2,3]})
   col x
0      1
1      2
2      3

Fungsi

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

Buat 2 kolom baru:

df[['square x', 'cube x']] = df['col x'].apply(f)

Keluaran:

   col x  square x  cube x
0      1         1       1
1      2         4       8
2      3         9      27
Joe
sumber
13

Saya telah melihat beberapa cara untuk melakukan ini dan metode yang ditampilkan di sini (mengembalikan seri panda) tampaknya tidak paling efisien.

Jika kita mulai dengan kerangka data berukuran besar dari data acak:

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

Contoh yang ditunjukkan di sini:

# 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 loop, terbaik 3: 2,77 detik per loop

Metode alternatif:

# 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 loop, terbaik 3: 8,85 ms per loop

Menurut saya itu jauh lebih efisien untuk mengambil serangkaian tupel dan kemudian mengubahnya menjadi DataFrame. Saya akan tertarik mendengar pemikiran orang lain jika ada kesalahan dalam pekerjaan saya.

RFox
sumber
Ini sangat berguna! Saya mendapat 30x percepatan dibandingkan dengan fungsi mengembalikan metode seri.
Pushkar Nimkar
9

Solusi yang diterima akan sangat lambat untuk banyak data. Solusi dengan jumlah upvote terbesar agak sulit dibaca dan juga lambat dengan data numerik. Jika setiap kolom baru dapat dihitung secara independen dari yang lain, saya hanya akan menetapkan masing-masing secara langsung tanpa menggunakan apply.

Contoh dengan data karakter palsu

Buat 100.000 string dalam DataFrame

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

Katakanlah kita ingin mengekstraksi beberapa fitur teks seperti yang dilakukan pada pertanyaan awal. Misalnya, mari kita ekstrak karakter pertama, hitung kemunculan huruf 'e' dan gunakan huruf besar frase.

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

Pengaturan waktu

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

Anehnya, Anda bisa mendapatkan kinerja yang lebih baik dengan mengulang setiap nilai

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

Contoh lain dengan data numerik palsu

Buat 1 juta angka acak dan uji powersfungsi dari atas.

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)

Menetapkan setiap kolom 25x lebih cepat dan sangat mudah dibaca:

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

Saya membuat respons serupa dengan lebih detail di sini tentang mengapa applybiasanya bukan cara untuk pergi.

Ted Petrou
sumber
8

Telah memposting jawaban yang sama di dua pertanyaan serupa lainnya. Cara saya lebih suka melakukan ini adalah untuk membungkus nilai-nilai pengembalian fungsi dalam suatu seri:

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

Dan kemudian gunakan terapkan sebagai berikut untuk membuat kolom terpisah:

df[['x**2','x**3']] = df.apply(lambda row: f(row['x']), axis=1)
Dmytro Bugayev
sumber
1

Anda dapat mengembalikan seluruh baris alih-alih nilai:

df = df.apply(extract_text_features,axis = 1)

di mana fungsi mengembalikan baris

def extract_text_features(row):
      row['new_col1'] = value1
      row['new_col2'] = value2
      return row
Saket Bajaj
sumber
Tidak, saya tidak ingin menerapkan extract_text_featureske setiap kolom df, hanya ke kolom teksdf.textcol
smci
-2
def myfunc(a):
    return a * a

df['New Column'] = df['oldcolumn'].map(myfunc))

Ini berhasil untuk saya. Kolom Baru akan dibuat dengan data kolom lama yang diproses.

pengguna2902302
sumber
2
Ini tidak mengembalikan 'beberapa kolom baru'
pedram bashiri
Ini tidak mengembalikan 'beberapa kolom baru', sehingga tidak menjawab pertanyaan. Bisakah Anda menghapusnya?
smci