ใช้ฟังก์ชัน pandas กับคอลัมน์เพื่อสร้างคอลัมน์ใหม่หลายรายการหรือไม่


215

วิธีทำในแพนด้า:

ฉันมีฟังก์ชั่นextract_text_featuresในคอลัมน์ข้อความเดียวกลับคอลัมน์หลายคอลัมน์ ฟังก์ชันส่งคืนค่า 6 ค่าโดยเฉพาะ

ฟังก์ชั่นใช้งานได้ แต่ดูเหมือนจะไม่มีประเภทการส่งคืนที่เหมาะสม (pandas DataFrame / numpy array / รายการ Python) เพื่อให้เอาต์พุตสามารถกำหนดได้อย่างถูกต้อง df.ix[: ,10:16] = df.textcol.map(extract_text_features)

ดังนั้นฉันคิดว่าฉันต้องถอยกลับไปทำซ้ำด้วยdf.iterrows()เช่นนี้ ?

UPDATE: การวนซ้ำด้วยdf.iterrows()ช้ากว่าอย่างน้อย 20x ดังนั้นฉันจึงยอมจำนนและแยกฟังก์ชั่นออกเป็นหก.map(lambda ...)สายที่แตกต่างกัน

UPDATE 2: คำถามนี้ถูกถามกลับรอบv0.11.0 ดังนั้นคำถามและคำตอบส่วนใหญ่จึงไม่เกี่ยวข้องเกินไป


1
df.ix[: ,10:16]ผมไม่คิดว่าคุณสามารถทำมอบหมายหลายวิธีที่คุณมีมันเขียน: ฉันคิดว่าคุณจะต้องใช้mergeคุณสมบัติของคุณในชุดข้อมูล
Zelazny7

1
สำหรับผู้ที่ต้องการวิธีแก้ปัญหาที่มีประสิทธิภาพมากขึ้นตรวจสอบข้อใดข้อหนึ่งด้านล่างนี้ซึ่งไม่ได้ใช้apply
Ted Petrou

การดำเนินการตัวเลขส่วนใหญ่ที่มีแพนด้าสามารถเป็น vectorized ได้ซึ่งหมายความว่ามันเร็วกว่าการทำซ้ำแบบเดิมมาก OTOH การดำเนินการบางอย่าง (เช่นสตริงและ regex) นั้นยากที่จะทำให้เป็นเวกเตอร์ได้ ในกรณีนี้สิ่งสำคัญคือต้องเข้าใจวิธีการวนลูปข้อมูลของคุณ ข้อมูลเพิ่มเติมเกี่ยวกับเวลาและวิธีการวนลูปข้อมูลของคุณจะทำอย่างไรโปรดอ่านสำหรับลูปกับนุ่น - ฉันควรจะดูแลเมื่อใด? .
cs95

@coldspeed: หมีแพนด้าปัญหาหลักไม่ได้ถูกเลือกซึ่งเป็นที่มีประสิทธิภาพสูงในหมู่หลายตัวเลือกมันก็ต่อสู้ไวยากรณ์ที่จะได้รับการทำงานที่ทุกคนกลับรอบv0.11.0
smci

แท้จริงแล้วความคิดเห็นนี้มีไว้สำหรับผู้อ่านในอนาคตที่กำลังมองหาวิธีแก้ปัญหาซ้ำ ๆ ซึ่งไม่รู้อะไรเลยหรือไม่รู้ว่ากำลังทำอะไรอยู่
cs95

คำตอบ:


109

จากคำตอบของผู้ใช้ 1827356 คุณสามารถทำการมอบหมายได้ในครั้งเดียวโดยใช้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
เพิ่งออกมาจากความอยากรู้อยากเห็นมันคาดว่าจะใช้หน่วยความจำมากขึ้นโดยการทำเช่นนี้? ฉันกำลังทำเช่นนี้กับ dataframe ที่เก็บแถว 2.5mil และฉันเกือบพบปัญหาหน่วยความจำ
เจฟฟรีย์ 04

2
'df.join (df.textcol.apply (lambda s: pd.Series ({' feature1 ': s + 1,' feature2 ': s-1}))) จะเป็นตัวเลือกที่ดีกว่าที่ฉันคิด
Shivam K. Thakkar

@ShivamKThakkar ทำไมคุณคิดว่าคำแนะนำของคุณจะเป็นตัวเลือกที่ดีกว่า คุณคิดว่ามีประสิทธิภาพมากขึ้นหรือมีค่าใช้จ่ายหน่วยความจำน้อยลงหรือไม่
tsando

1
โปรดพิจารณาความเร็วและหน่วยความจำที่ต้องการ: ys-l.github.io/posts/2015/08/28/how-not-to-use-pandas-apply
Make42

190

ฉันมักจะทำสิ่งนี้โดยใช้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
แต่คุณจะทำอย่างไรถ้าคุณเพิ่ม 50 คอลัมน์เป็นเช่นนี้แทนที่จะเป็น 6
สูงสุด

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))น่าจะเป็นหนทางไป
François Leblanc


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

concat () ดูง่ายกว่าการรวม () สำหรับเชื่อมต่อ cols ใหม่กับดาต้าเฟรมเดิม
ยี่หร่า

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 (ตัวอย่าง (df), Axis = 1) แก้ไขให้ฉันถ้าฉันผิดฉันเป็นมือใหม่
user299791

1
@ user299791, ไม่ในกรณีนี้คุณกำลังรักษาตัวอย่างเป็นวัตถุชั้นหนึ่งเพื่อให้คุณผ่านในฟังก์ชั่นของตัวเอง ฟังก์ชั่นนี้จะใช้กับแต่ละแถว
Michael David Watson

สวัสดี Michael คำตอบของคุณช่วยฉันแก้ไขปัญหา วิธีแก้ปัญหาของคุณดีกว่าวิธี df.assign () ของ pandas ดั้งเดิมเพราะนี่คือหนึ่งครั้งต่อคอลัมน์ การใช้ assign () หากคุณต้องการสร้าง 2 คอลัมน์ใหม่คุณต้องใช้ df1 เพื่อทำงานกับ df เพื่อรับ column1 ใหม่จากนั้นใช้ df2 เพื่อทำงานกับ df1 เพื่อสร้างคอลัมน์ใหม่ที่สอง ... นี่ค่อนข้างน่าเบื่อ แต่วิธีการของคุณช่วยชีวิตฉันไว้ !!! ขอบคุณ !!!
commentallez-vous

1
จะไม่เรียกใช้รหัสการกำหนดคอลัมน์หนึ่งครั้งต่อแถวหรือไม่ มันจะดีกว่าหรือไม่ถ้าจะส่งคืนpd.Series({k:v})และเรียงลำดับคอลัมน์ตามลำดับในคำตอบของ Ewan
เดนิสเดอ

ถ้ามันจะช่วยให้ทุกคนในขณะที่วิธีการนี้ถูกต้องและยังง่ายที่สุดของทั้งหมดที่นำเสนอโซลูชั่นการปรับปรุงแถวโดยตรงเช่นนี้จบลงด้วยการที่น่าแปลกใจช้า - ลำดับความสำคัญช้ากว่าใช้กับ 'ขยายโซลูชั่น + pd.concat
Dmytro Bugayev

31

ในปี 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 ซึ่งเป็นสิ่งที่ดีเสมอเกี่ยวกับปัญหาด้านประสิทธิภาพ
Théo Rubenach

1
นี่เป็นทางออกที่ดี ปัญหาเดียวคือคุณไม่สามารถเลือกชื่อสำหรับคอลัมน์ที่เพิ่งเพิ่มใหม่ 2 คอลัมน์ คุณต้องทำภายหลัง df.rename (คอลัมน์ = {0: 'col1', 1: 'col2'})
pedram bashiri

2
@pedrambashiri หากฟังก์ชั่นที่คุณส่งผ่านเพื่อdf.applyส่งกลับ a dictคอลัมน์จะออกมาตามชื่อคีย์
Seb

25

เพียงแค่ใช้ 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
tar

22

สรุป:หากคุณต้องการสร้างเพียงไม่กี่คอลัมน์ให้ใช้df[['new_col1','new_col2']] = df[['data1','data2']].apply( function_of_your_choosing(x), axis=1)

สำหรับวิธีนี้จำนวนคอลัมน์ใหม่ที่คุณสร้างจะต้องเท่ากับคอลัมน์จำนวนที่คุณใช้เป็นอินพุตไปยังฟังก์ชัน. apply () หากคุณต้องการทำอย่างอื่นให้ดูที่คำตอบอื่น ๆ

รายละเอียด สมมติว่าคุณมี dataframe สองคอลัมน์ คอลัมน์แรกคือส่วนสูงของบุคคลเมื่อพวกเขามีอายุ 10 ปี ที่สองคือความสูงของบุคคลที่กล่าวเมื่ออายุ 20

สมมติว่าคุณต้องคำนวณทั้งความสูงและความสูงของแต่ละคน นั่นคือสองค่าต่อแต่ละแถว

คุณสามารถทำสิ่งนี้ผ่านฟังก์ชั่นต่อไปนี้ซึ่งจะใช้เร็ว ๆ นี้:

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)

(เพื่อความชัดเจน: ฟังก์ชั่นการใช้งานนี้ใช้ในค่าจากแต่ละแถวใน dataframe ที่ถูกเซ็ตแล้วและส่งคืนรายการ)

อย่างไรก็ตามหากคุณทำสิ่งนี้:

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

คุณจะสร้าง 1 คอลัมน์ใหม่ที่มีรายการ [ค่าเฉลี่ย, ผลรวม] ซึ่งคุณอาจต้องการหลีกเลี่ยงเนื่องจากจะต้องมีแลมบ์ดา / สมัครอีกรายการ

คุณต้องการแยกแต่ละค่าออกเป็นคอลัมน์ของตัวเอง หากต้องการทำสิ่งนี้คุณสามารถสร้างสองคอลัมน์พร้อมกันได้:

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

4
สำหรับ pandas 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])
Kanishk Mair

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

ฉันดูหลายวิธีในการทำเช่นนี้และวิธีที่แสดงที่นี่ (ส่งคืนชุดแพนด้า) ดูเหมือนจะไม่ได้มีประสิทธิภาพมากที่สุด

หากเราเริ่มต้นด้วย dataframe ขนาดใหญ่ของข้อมูลแบบสุ่ม:

# 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 ต่อลูป

โดยการคำนวณของฉันมันมีประสิทธิภาพมากขึ้นที่จะใช้ชุดของ tuples แล้วแปลงเป็น DataFrame ฉันสนใจที่จะฟังความคิดของผู้คนแม้ว่าจะมีข้อผิดพลาดในการทำงานของฉัน


มันมีประโยชน์จริง ๆ ! ฉันได้รับความเร็ว 30x เมื่อเปรียบเทียบกับฟังก์ชั่นการส่งคืนชุดข้อมูล
Pushkar Nimkar

9

โซลูชันที่ได้รับการยอมรับจะช้ามากสำหรับข้อมูลจำนวนมาก โซลูชันที่มีจำนวนสูงสุดของการลงคะแนนทำได้ยากเล็กน้อยในการอ่านและช้าด้วยข้อมูลตัวเลข ถ้าแต่ละคอลัมน์ใหม่ที่สามารถคำนวณได้อย่างอิสระของคนอื่น ๆ applyผมก็จะมอบหมายให้แต่ละของพวกเขาได้โดยตรงโดยไม่ต้องใช้

ตัวอย่างที่มีข้อมูลตัวละครปลอม

สร้าง 100,000 สายใน 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

สมมติว่าเราต้องการแยกคุณลักษณะข้อความตามที่ทำไว้ในคำถามเดิม ตัวอย่างเช่นลองแยกอักขระตัวแรกนับการเกิดตัวอักษร '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)

อีกตัวอย่างหนึ่งที่มีข้อมูลตัวเลขปลอม

สร้างตัวเลขสุ่ม 1 ล้านตัวและทดสอบ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

โพสต์คำตอบเดียวกันในสองคำถามที่คล้ายกัน วิธีที่ฉันชอบทำคือห่อค่าที่ส่งคืนของฟังก์ชันในซีรีย์:

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

จากนั้นใช้ใช้ดังต่อไปนี้เพื่อสร้างคอลัมน์แยก:

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.