วิธีการระเบิดรายการภายในเซลล์ Dataframe เป็นแถวแยกกัน


96

ฉันต้องการเปลี่ยนเซลล์แพนด้าที่มีรายการเป็นแถวสำหรับแต่ละค่าเหล่านั้น

ดังนั้นใช้สิ่งนี้:

ป้อนคำอธิบายภาพที่นี่

หากฉันต้องการคลายแพ็กและซ้อนค่าในnearest_neighborsคอลัมน์เพื่อให้แต่ละค่าเป็นแถวภายในopponentดัชนีแต่ละรายการฉันจะทำอย่างไรดีที่สุด มีวิธีการของแพนด้าที่มีไว้สำหรับการดำเนินการเช่นนี้หรือไม่?


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

คุณสามารถใช้pd.DataFrame(df.nearest_neighbors.values.tolist())เพื่อแกะคอลัมน์นี้จากนั้นนำpd.mergeไปติดกับคอลัมน์ อื่น ๆ
hellpanderr

@helpanderr ฉันไม่คิดว่าvalues.tolist()จะทำอะไรที่นี่; คอลัมน์นี้เป็นรายการแล้ว
maxymoo


1
ที่เกี่ยวข้อง แต่มีรายละเอียดเพิ่มเติมstackoverflow.com/questions/53218931/…
BENY

คำตอบ:


56

ในโค้ดด้านล่างฉันจะรีเซ็ตดัชนีก่อนเพื่อให้การวนซ้ำง่ายขึ้น

ฉันสร้างลิสต์รายการโดยแต่ละองค์ประกอบของรายการภายนอกเป็นแถวของ DataFrame เป้าหมายและแต่ละองค์ประกอบของรายการด้านในเป็นหนึ่งในคอลัมน์ ในที่สุดรายการที่ซ้อนกันนี้จะเชื่อมต่อกันเพื่อสร้าง DataFrame ที่ต้องการ

ฉันใช้lambdaฟังก์ชันร่วมกับการวนซ้ำรายการเพื่อสร้างแถวสำหรับแต่ละองค์ประกอบของการnearest_neighborsจับคู่กับที่เกี่ยวข้องnameและopponent.

สุดท้ายฉันสร้าง DataFrame ใหม่จากรายการนี้ (โดยใช้ชื่อคอลัมน์เดิมและตั้งค่าดัชนีกลับไปที่nameและopponent)

df = (pd.DataFrame({'name': ['A.J. Price'] * 3, 
                    'opponent': ['76ers', 'blazers', 'bobcats'], 
                    'nearest_neighbors': [['Zach LaVine', 'Jeremy Lin', 'Nate Robinson', 'Isaia']] * 3})
      .set_index(['name', 'opponent']))

>>> df
                                                    nearest_neighbors
name       opponent                                                  
A.J. Price 76ers     [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
           blazers   [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
           bobcats   [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]

df.reset_index(inplace=True)
rows = []
_ = df.apply(lambda row: [rows.append([row['name'], row['opponent'], nn]) 
                         for nn in row.nearest_neighbors], axis=1)
df_new = pd.DataFrame(rows, columns=df.columns).set_index(['name', 'opponent'])

>>> df_new
                    nearest_neighbors
name       opponent                  
A.J. Price 76ers          Zach LaVine
           76ers           Jeremy Lin
           76ers        Nate Robinson
           76ers                Isaia
           blazers        Zach LaVine
           blazers         Jeremy Lin
           blazers      Nate Robinson
           blazers              Isaia
           bobcats        Zach LaVine
           bobcats         Jeremy Lin
           bobcats      Nate Robinson
           bobcats              Isaia

แก้ไขมิถุนายน 2017

วิธีอื่นมีดังนี้:

>>> (pd.melt(df.nearest_neighbors.apply(pd.Series).reset_index(), 
             id_vars=['name', 'opponent'],
             value_name='nearest_neighbors')
     .set_index(['name', 'opponent'])
     .drop('variable', axis=1)
     .dropna()
     .sort_index()
     )

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

2
การระเบิดคอลัมน์ที่เหมือนรายการถูกทำให้ง่ายขึ้นอย่างมากในแพนด้า 0.25ด้วยการเพิ่มexplode()วิธี ฉันเพิ่มคำตอบพร้อมตัวอย่างโดยใช้การตั้งค่า df เดียวกันกับที่นี่
joelostblom

@joelostblom น่าฟัง. ขอบคุณที่เพิ่มตัวอย่างพร้อมการใช้งานปัจจุบัน
Alexander

39
df = (pd.DataFrame({'name': ['A.J. Price'] * 3, 
                    'opponent': ['76ers', 'blazers', 'bobcats'], 
                    'nearest_neighbors': [['Zach LaVine', 'Jeremy Lin', 'Nate Robinson', 'Isaia']] * 3})
      .set_index(['name', 'opponent']))

df.explode('nearest_neighbors')

ออก:

                    nearest_neighbors
name       opponent                  
A.J. Price 76ers          Zach LaVine
           76ers           Jeremy Lin
           76ers        Nate Robinson
           76ers                Isaia
           blazers        Zach LaVine
           blazers         Jeremy Lin
           blazers      Nate Robinson
           blazers              Isaia
           bobcats        Zach LaVine
           bobcats         Jeremy Lin
           bobcats      Nate Robinson
           bobcats              Isaia

2
โปรดทราบว่าสิ่งนี้ใช้ได้กับคอลัมน์เดียวเท่านั้น (ตั้งแต่ 0.25) ดูที่นี่และที่นี่สำหรับโซลูชันทั่วไปเพิ่มเติม
cs95

นี่เป็นวิธีแก้ปัญหาที่เร็วที่สุดที่ง่ายที่สุด (หากคุณมีเพียงคอลัมน์เดียวที่มีรายการที่จะระเบิดหรือ "คลาย" ตามที่เรียกใน mongodb)
annakeuchenius

วิธีแก้ปัญหาที่เร็วที่สุดโดยแพนด้า docu แต่ระวัง: .explode ไม่ได้ถูกแทนที่! ค่อนข้างทำdf = df.explode(...)
harmonica141

34

ใช้apply(pd.Series)และstackจากนั้นreset_indexและto_frame

In [1803]: (df.nearest_neighbors.apply(pd.Series)
              .stack()
              .reset_index(level=2, drop=True)
              .to_frame('nearest_neighbors'))
Out[1803]:
                    nearest_neighbors
name       opponent
A.J. Price 76ers          Zach LaVine
           76ers           Jeremy Lin
           76ers        Nate Robinson
           76ers                Isaia
           blazers        Zach LaVine
           blazers         Jeremy Lin
           blazers      Nate Robinson
           blazers              Isaia
           bobcats        Zach LaVine
           bobcats         Jeremy Lin
           bobcats      Nate Robinson
           bobcats              Isaia

รายละเอียด

In [1804]: df
Out[1804]:
                                                   nearest_neighbors
name       opponent
A.J. Price 76ers     [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
           blazers   [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
           bobcats   [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]

1
รักความสง่างามของโซลูชันของคุณ! คุณได้เปรียบเทียบกับแนวทางอื่น ๆ โดยบังเอิญหรือไม่?
rpyzh

1
ผลลัพธ์ที่df.nearest_neighbors.apply(pd.Series)ได้นั้นน่าประหลาดใจมากสำหรับฉัน
Calum You

1
@rpyzh ใช่มันค่อนข้างสวย แต่ช้าอย่างน่าสมเพช
cs95

16

ฉันคิดว่านี่เป็นคำถามที่ดีจริงๆใน Hive ที่คุณจะใช้EXPLODEฉันคิดว่ามีกรณีที่นุ่นควรรวมฟังก์ชันนี้ไว้เป็นค่าเริ่มต้น ฉันอาจจะระเบิดคอลัมน์รายการด้วยความเข้าใจของเครื่องกำเนิดไฟฟ้าที่ซ้อนกันดังนี้:

pd.DataFrame({
    "name": i[0],
    "opponent": i[1],
    "nearest_neighbor": neighbour
    }
    for i, row in df.iterrows() for neighbour in row.nearest_neighbors
    ).set_index(["name", "opponent"])

ฉันชอบวิธีที่โซลูชันนี้ช่วยให้จำนวนรายการแตกต่างกันสำหรับแต่ละแถว
user1718097

มีวิธีการรักษาดัชนีเดิมด้วยวิธีนี้หรือไม่?
SummerEla

2
@SummerEla ฮ่า ๆ นี่เป็นคำตอบที่เก่ามากฉันได้อัปเดตเพื่อแสดงว่าฉันจะทำอย่างไรตอนนี้
maxymoo

1
@maxymoo ยังคงเป็นคำถามที่ดี ขอบคุณสำหรับการอัปเดต!
SummerEla

ฉันพบว่าสิ่งนี้มีประโยชน์และเปลี่ยนเป็นแพ็คเกจ
Oren

11

เร็วที่สุดวิธีที่ฉันพบเพื่อให้ห่างไกลมีการขยาย DataFrame ด้วย.ilocและการกำหนดหลังบี้คอลัมน์เป้าหมาย

รับอินพุตปกติ (จำลองบิต):

df = (pd.DataFrame({'name': ['A.J. Price'] * 3, 
                    'opponent': ['76ers', 'blazers', 'bobcats'], 
                    'nearest_neighbors': [['Zach LaVine', 'Jeremy Lin', 'Nate Robinson', 'Isaia']] * 3})
      .set_index(['name', 'opponent']))
df = pd.concat([df]*10)

df
Out[3]: 
                                                   nearest_neighbors
name       opponent                                                 
A.J. Price 76ers     [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
           blazers   [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
           bobcats   [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
           76ers     [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
           blazers   [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
...

ให้ทางเลือกที่แนะนำต่อไปนี้:

col_target = 'nearest_neighbors'

def extend_iloc():
    # Flatten columns of lists
    col_flat = [item for sublist in df[col_target] for item in sublist] 
    # Row numbers to repeat 
    lens = df[col_target].apply(len)
    vals = range(df.shape[0])
    ilocations = np.repeat(vals, lens)
    # Replicate rows and add flattened column of lists
    cols = [i for i,c in enumerate(df.columns) if c != col_target]
    new_df = df.iloc[ilocations, cols].copy()
    new_df[col_target] = col_flat
    return new_df

def melt():
    return (pd.melt(df[col_target].apply(pd.Series).reset_index(), 
             id_vars=['name', 'opponent'],
             value_name=col_target)
            .set_index(['name', 'opponent'])
            .drop('variable', axis=1)
            .dropna()
            .sort_index())

def stack_unstack():
    return (df[col_target].apply(pd.Series)
            .stack()
            .reset_index(level=2, drop=True)
            .to_frame(col_target))

ผมพบว่าextend_iloc()เป็นที่เร็วที่สุด :

%timeit extend_iloc()
3.11 ms ± 544 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit melt()
22.5 ms ± 1.25 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit stack_unstack()
11.5 ms ± 410 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

การประเมินผลที่ดี
StephenBoesch

2
ขอบคุณสำหรับสิ่งนี้มันช่วยฉันได้จริงๆ ผมใช้วิธีการแก้ปัญหา extend_iloc และพบว่า cols = [c for c in df.columns if c != col_target] ควรจะ: ข้อผิดพลาดหากไม่ได้นำเสนอกับดัชนีคอลัมน์ cols = [i for i,c in enumerate(df.columns) if c != col_target]df.iloc[ilocations, cols].copy()
jdungan

ขอขอบคุณอีกครั้งสำหรับคำแนะนำ iloc ฉันเขียนคำอธิบายโดยละเอียดว่ามันทำงานอย่างไรที่นี่: medium.com/@johnadungan/… . หวังว่ามันจะช่วยทุกคนที่มีความท้าทายคล้าย ๆ กัน
jdungan

7

โซลูชันทางเลือกที่ดีกว่าพร้อมใช้ (pd.Series):

df = pd.DataFrame({'listcol':[[1,2,3],[4,5,6]]})

# expand df.listcol into its own dataframe
tags = df['listcol'].apply(pd.Series)

# rename each variable is listcol
tags = tags.rename(columns = lambda x : 'listcol_' + str(x))

# join the tags dataframe back to the original dataframe
df = pd.concat([df[:], tags[:]], axis=1)

อันนี้ขยายคอลัมน์ไม่ใช่แถว
Oleg

@Oleg ถูกต้อง แต่คุณสามารถเปลี่ยน DataFrame ได้ตลอดเวลาจากนั้นใช้ pd ซีรีส์ - ง่ายกว่าคำแนะนำอื่น ๆ
Philipp Schwarz

7

คล้ายกับฟังก์ชัน EXPLODE ของ Hive:

import copy

def pandas_explode(df, column_to_explode):
    """
    Similar to Hive's EXPLODE function, take a column with iterable elements, and flatten the iterable to one element 
    per observation in the output table

    :param df: A dataframe to explod
    :type df: pandas.DataFrame
    :param column_to_explode: 
    :type column_to_explode: str
    :return: An exploded data frame
    :rtype: pandas.DataFrame
    """

    # Create a list of new observations
    new_observations = list()

    # Iterate through existing observations
    for row in df.to_dict(orient='records'):

        # Take out the exploding iterable
        explode_values = row[column_to_explode]
        del row[column_to_explode]

        # Create a new observation for every entry in the exploding iterable & add all of the other columns
        for explode_value in explode_values:

            # Deep copy existing observation
            new_observation = copy.deepcopy(row)

            # Add one (newly flattened) value from exploding iterable
            new_observation[column_to_explode] = explode_value

            # Add to the list of new observations
            new_observations.append(new_observation)

    # Create a DataFrame
    return_df = pandas.DataFrame(new_observations)

    # Return
    return return_df

1
เมื่อฉันเรียกใช้สิ่งนี้ฉันได้รับข้อผิดพลาดต่อไปนี้:NameError: global name 'copy' is not defined
frmsaul

4

ดังนั้นคำตอบทั้งหมดนี้ดี แต่ฉันต้องการบางอย่าง ^ ง่ายมาก ^ ดังนั้นนี่คือผลงานของฉัน:

def explode(series):
    return pd.Series([x for _list in series for x in _list])                               

เพียงแค่ใช้สิ่งนี้เมื่อคุณต้องการซีรีส์ใหม่ที่รายการ "ระเบิด" นี่คือตัวอย่างที่เราทำ value_counts () ในตัวเลือกทาโก้ :)

In [1]: my_df = pd.DataFrame(pd.Series([['a','b','c'],['b','c'],['c']]), columns=['tacos'])      
In [2]: my_df.head()                                                                               
Out[2]: 
   tacos
0  [a, b, c]
1     [b, c]
2        [c]

In [3]: explode(my_df['tacos']).value_counts()                                                     
Out[3]: 
c    3
b    2
a    1

2

นี่คือการเพิ่มประสิทธิภาพที่เป็นไปได้สำหรับดาต้าเฟรมขนาดใหญ่ ซึ่งจะทำงานได้เร็วขึ้นเมื่อมีค่าเท่ากันหลายค่าในช่อง "ระเบิด" (ยิ่งดาต้าเฟรมมีขนาดใหญ่เมื่อเทียบกับจำนวนค่าที่ไม่ซ้ำกันในฟิลด์โค้ดนี้ก็จะทำงานได้ดีขึ้นเท่านั้น)

def lateral_explode(dataframe, fieldname): 
    temp_fieldname = fieldname + '_made_tuple_' 
    dataframe[temp_fieldname] = dataframe[fieldname].apply(tuple)       
    list_of_dataframes = []
    for values in dataframe[temp_fieldname].unique().tolist(): 
        list_of_dataframes.append(pd.DataFrame({
            temp_fieldname: [values] * len(values), 
            fieldname: list(values), 
        }))
    dataframe = dataframe[list(set(dataframe.columns) - set([fieldname]))]\ 
        .merge(pd.concat(list_of_dataframes), how='left', on=temp_fieldname) 
    del dataframe[temp_fieldname]

    return dataframe

1

การขยาย.ilocคำตอบของ Oleg เพื่อแบนคอลัมน์รายการทั้งหมดโดยอัตโนมัติ:

def extend_iloc(df):
    cols_to_flatten = [colname for colname in df.columns if 
    isinstance(df.iloc[0][colname], list)]
    # Row numbers to repeat 
    lens = df[cols_to_flatten[0]].apply(len)
    vals = range(df.shape[0])
    ilocations = np.repeat(vals, lens)
    # Replicate rows and add flattened column of lists
    with_idxs = [(i, c) for (i, c) in enumerate(df.columns) if c not in cols_to_flatten]
    col_idxs = list(zip(*with_idxs)[0])
    new_df = df.iloc[ilocations, col_idxs].copy()

    # Flatten columns of lists
    for col_target in cols_to_flatten:
        col_flat = [item for sublist in df[col_target] for item in sublist]
        new_df[col_target] = col_flat

    return new_df

สิ่งนี้ถือว่าคอลัมน์รายการแต่ละคอลัมน์มีความยาวรายการเท่ากัน


1

แทนที่จะใช้ Apply (pd.Series) คุณสามารถแบนคอลัมน์ได้ สิ่งนี้ช่วยเพิ่มประสิทธิภาพ

df = (pd.DataFrame({'name': ['A.J. Price'] * 3, 
                'opponent': ['76ers', 'blazers', 'bobcats'], 
                'nearest_neighbors': [['Zach LaVine', 'Jeremy Lin', 'Nate Robinson', 'Isaia']] * 3})
  .set_index(['name', 'opponent']))



%timeit (pd.DataFrame(df['nearest_neighbors'].values.tolist(), index = df.index)
           .stack()
           .reset_index(level = 2, drop=True).to_frame('nearest_neighbors'))

1.87 ms ± 9.74 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)


%timeit (df.nearest_neighbors.apply(pd.Series)
          .stack()
          .reset_index(level=2, drop=True)
          .to_frame('nearest_neighbors'))

2.73 ms ± 16.7 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

IndexError: หลายระดับเกินไป: ดัชนีมีเพียง 2 ระดับไม่ใช่ 3 เมื่อฉันลองตัวอย่างของฉัน
vinsent paramanantham

1
คุณต้องเปลี่ยน "ระดับ" ใน reset_index ตามตัวอย่างของคุณ
suleep kumar
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.