การแยกพจนานุกรม / รายการภายในคอลัมน์แพนด้าเป็นคอลัมน์แยก


166

ฉันมีข้อมูลที่บันทึกไว้ในpostgreSQLฐานข้อมูล ฉันกำลังค้นหาข้อมูลนี้โดยใช้ Python2.7 และเปลี่ยนเป็น Pandas DataFrame อย่างไรก็ตามคอลัมน์สุดท้ายของดาต้าเฟรมนี้มีพจนานุกรม (หรือรายการ?) ของค่าอยู่ภายใน DataFrame มีลักษณะดังนี้:

[1] df
Station ID     Pollutants
8809           {"a": "46", "b": "3", "c": "12"}
8810           {"a": "36", "b": "5", "c": "8"}
8811           {"b": "2", "c": "7"}
8812           {"c": "11"}
8813           {"a": "82", "c": "15"}

ฉันต้องการแยกคอลัมน์นี้ออกเป็นคอลัมน์แยกกันเพื่อให้ DataFrame มีลักษณะดังนี้:

[2] df2
Station ID     a      b       c
8809           46     3       12
8810           36     5       8
8811           NaN    2       7
8812           NaN    NaN     11
8813           82     NaN     15

ปัญหาหลักที่ฉันพบคือรายการมีความยาวไม่เท่ากัน แต่รายการทั้งหมดมีเพียง 3 ค่าเดียวกันคือ a, b และ c และมักจะปรากฏในลำดับเดียวกันเสมอ (a แรก, b วินาที, c สาม)

รหัสต่อไปนี้ใช้ในการทำงานและส่งคืนสิ่งที่ฉันต้องการ (df2)

[3] df 
[4] objs = [df, pandas.DataFrame(df['Pollutant Levels'].tolist()).iloc[:, :3]]
[5] df2 = pandas.concat(objs, axis=1).drop('Pollutant Levels', axis=1)
[6] print(df2)

ฉันใช้รหัสนี้เมื่อสัปดาห์ที่แล้วและมันก็ใช้งานได้ดี แต่ตอนนี้รหัสของฉันเสียและฉันได้รับข้อผิดพลาดนี้จากบรรทัด [4]:

IndexError: out-of-bounds on slice (end) 

ฉันไม่ได้ทำการเปลี่ยนแปลงรหัส แต่ตอนนี้ได้รับข้อผิดพลาด ฉันรู้สึกว่านี่เป็นเพราะวิธีการของฉันไม่แข็งแรงหรือเหมาะสม

ข้อเสนอแนะหรือคำแนะนำเกี่ยวกับวิธีแยกคอลัมน์รายการนี้ออกเป็นคอลัมน์แยกกันจะได้รับการชื่นชมเป็นอย่างยิ่ง

แก้ไข: ฉันคิดว่า.tolist()เมธอดและ. ใช้ไม่ทำงานกับรหัสของฉันเพราะเป็นUnicodeสตริงเดียวเช่น:

#My data format 
u{'a': '1', 'b': '2', 'c': '3'}

#and not
{u'a': '1', u'b': '2', u'c': '3'}

ข้อมูลกำลังนำเข้าจากpostgreSQLฐานข้อมูลในรูปแบบนี้ ความช่วยเหลือหรือความคิดเกี่ยวกับปัญหานี้หรือไม่? มีวิธีการแปลงUnicodeหรือไม่

คำตอบ:


188

การแปลงสตริงไปยัง Dict df['Pollutant Levels'].map(eval)ที่เกิดขึ้นจริงที่คุณสามารถทำได้ หลังจากนั้นคุณสามารถใช้วิธีแก้ปัญหาด้านล่างเพื่อแปลง dict เป็นคอลัมน์ต่างๆ


โดยใช้ตัวอย่างเล็ก ๆ คุณสามารถใช้.apply(pd.Series):

In [2]: df = pd.DataFrame({'a':[1,2,3], 'b':[{'c':1}, {'d':3}, {'c':5, 'd':6}]})

In [3]: df
Out[3]:
   a                   b
0  1           {u'c': 1}
1  2           {u'd': 3}
2  3  {u'c': 5, u'd': 6}

In [4]: df['b'].apply(pd.Series)
Out[4]:
     c    d
0  1.0  NaN
1  NaN  3.0
2  5.0  6.0

หากต้องการรวมเข้ากับส่วนที่เหลือของ dataframe คุณสามารถconcatใช้คอลัมน์อื่น ๆ ที่มีผลลัพธ์ข้างต้น:

In [7]: pd.concat([df.drop(['b'], axis=1), df['b'].apply(pd.Series)], axis=1)
Out[7]:
   a    c    d
0  1  1.0  NaN
1  2  NaN  3.0
2  3  5.0  6.0

การใช้รหัสของคุณสิ่งนี้ใช้ได้เช่นกันหากฉันไม่ใช้ilocส่วน:

In [15]: pd.concat([df.drop('b', axis=1), pd.DataFrame(df['b'].tolist())], axis=1)
Out[15]:
   a    c    d
0  1  1.0  NaN
1  2  NaN  3.0
2  3  5.0  6.0

2
ฉันใช้pd.DataFrame(df[col].tolist())มานานไม่เคยคิดเกี่ยวกับapply(pd.Series). ดีมาก.
ayhan

1
ตอนนี้ฉันตระหนักถึงปัญหาแล้ว .apply (pd.Series) ไม่ทำงานกับชุดข้อมูลของฉันเนื่องจากทั้งแถวเป็นสตริงเดียว คือ: u '{' a ':' 1 ',' b ':' 2 ',' c ':' 3 '} ไม่ใช่ {u'a': '1', u'b ':' 2 ', u'c ':' 3 '} ตามที่โซลูชันของคุณแสดง ดังนั้นโค้ดจึงไม่สามารถแบ่งออกเป็น 3 คอลัมน์ที่รู้จัก
llaffin

3
@ayhan ทดสอบแล้วและDataFrame(df['col'].tolist())แนวทางค่อนข้างเร็วกว่าวิธีใช้!
joris

3
@llaffin ถ้าเป็นสตริงคุณสามารถแปลงไปยังที่เกิดขึ้นจริงกับ Dict df[col].map(eval)ก่อนที่จะแปลงไป DataFrame
Joris

2
ทำงานได้สมบูรณ์แบบ แต่ (มาก) ช้ากว่าโซลูชันใหม่ (2019) จาก Lech Birek stackoverflow.com/a/55355928/2721710
drasc

107

ฉันรู้ว่าคำถามนี้ค่อนข้างเก่า แต่ฉันมาที่นี่เพื่อค้นหาคำตอบ ตอนนี้มีวิธีที่ดีกว่า (และเร็วกว่า) ในการทำสิ่งนี้โดยใช้json_normalize:

import pandas as pd

df2 = pd.json_normalize(df['Pollutant Levels'])

สิ่งนี้หลีกเลี่ยงการใช้ฟังก์ชันที่มีราคาแพง ...


21

ลองสิ่งนี้: ข้อมูลที่ส่งคืนจาก SQL ต้องแปลงเป็น Dict หรืออาจจะ "Pollutant Levels" เป็นตอนนี้Pollutants'

   StationID                   Pollutants
0       8809  {"a":"46","b":"3","c":"12"}
1       8810   {"a":"36","b":"5","c":"8"}
2       8811            {"b":"2","c":"7"}
3       8812                   {"c":"11"}
4       8813          {"a":"82","c":"15"}


df2["Pollutants"] = df2["Pollutants"].apply(lambda x : dict(eval(x)) )
df3 = df2["Pollutants"].apply(pd.Series )

    a    b   c
0   46    3  12
1   36    5   8
2  NaN    2   7
3  NaN  NaN  11
4   82  NaN  15


result = pd.concat([df, df3], axis=1).drop('Pollutants', axis=1)
result

   StationID    a    b   c
0       8809   46    3  12
1       8810   36    5   8
2       8811  NaN    2   7
3       8812  NaN  NaN  11
4       8813   82  NaN  15

13

คำตอบของ Merlin นั้นดีกว่าและง่ายสุด ๆ แต่เราไม่ต้องการฟังก์ชันแลมด้า การประเมินพจนานุกรมสามารถละเว้นได้อย่างปลอดภัยด้วยสองวิธีต่อไปนี้ดังภาพประกอบด้านล่าง:

วิธีที่ 1: สองขั้นตอน

# step 1: convert the `Pollutants` column to Pandas dataframe series
df_pol_ps = data_df['Pollutants'].apply(pd.Series)

df_pol_ps:
    a   b   c
0   46  3   12
1   36  5   8
2   NaN 2   7
3   NaN NaN 11
4   82  NaN 15

# step 2: concat columns `a, b, c` and drop/remove the `Pollutants` 
df_final = pd.concat([df, df_pol_ps], axis = 1).drop('Pollutants', axis = 1)

df_final:
    StationID   a   b   c
0   8809    46  3   12
1   8810    36  5   8
2   8811    NaN 2   7
3   8812    NaN NaN 11
4   8813    82  NaN 15

วิธีที่ 2: สองขั้นตอนข้างต้นสามารถรวมกันได้ในครั้งเดียว:

df_final = pd.concat([df, df['Pollutants'].apply(pd.Series)], axis = 1).drop('Pollutants', axis = 1)

df_final:
    StationID   a   b   c
0   8809    46  3   12
1   8810    36  5   8
2   8811    NaN 2   7
3   8812    NaN NaN 11
4   8813    82  NaN 15

13

ฉันขอแนะนำให้ใช้วิธีแยกคอลัมน์ 'Pollutants':

df_pollutants = pd.DataFrame(df['Pollutants'].values.tolist(), index=df.index)

เร็วกว่ามาก

df_pollutants = df['Pollutants'].apply(pd.Series)

เมื่อขนาดของ df เป็นยักษ์


จะดีมากถ้าคุณสามารถอธิบายได้ว่าทำไมถึงได้ผลและดีกว่านี้มาก! สำหรับฉันมันเร็วกว่าเสมอและเร็วขึ้นประมาณ 200 เท่าเมื่อคุณมีแถวมากกว่า ~ 1,000 แถว
Sam Mason

1
@SamMason เมื่อคุณทำapplydata frame ทั้งหมดจะถูกจัดการโดยแพนด้า แต่เมื่อพูดถึงvaluesมันจะเล่นเฉพาะกับnumpy ndarraysที่เร็วกว่าอย่างแท้จริงเนื่องจากมีcการใช้งานที่บริสุทธิ์
Sagar Kar

8

คุณสามารถใช้joinกับ+pop tolistประสิทธิภาพเทียบได้concatกับdrop+ tolistแต่บางคนอาจพบว่าตัวล้างไวยากรณ์นี้:

res = df.join(pd.DataFrame(df.pop('b').tolist()))

การเปรียบเทียบด้วยวิธีการอื่น ๆ :

df = pd.DataFrame({'a':[1,2,3], 'b':[{'c':1}, {'d':3}, {'c':5, 'd':6}]})

def joris1(df):
    return pd.concat([df.drop('b', axis=1), df['b'].apply(pd.Series)], axis=1)

def joris2(df):
    return pd.concat([df.drop('b', axis=1), pd.DataFrame(df['b'].tolist())], axis=1)

def jpp(df):
    return df.join(pd.DataFrame(df.pop('b').tolist()))

df = pd.concat([df]*1000, ignore_index=True)

%timeit joris1(df.copy())  # 1.33 s per loop
%timeit joris2(df.copy())  # 7.42 ms per loop
%timeit jpp(df.copy())     # 7.68 ms per loop

4
  1. pd.json_normalize(df.Pollutants) เร็วกว่าอย่างมาก df.Pollutants.apply(pd.Series)
    • ดู%%timeitด้านล่าง สำหรับแถว 1M, .json_normalize47 .applyครั้งเร็วกว่า
  2. ไม่ว่าจะอ่านข้อมูลจากไฟล์หรือจากอ็อบเจ็กต์ที่ส่งคืนโดยฐานข้อมูลหรือ API อาจไม่ชัดเจนว่าdictคอลัมน์มีdictหรือstrประเภทไม่
    • ถ้าพจนานุกรมในคอลัมน์นี้จะมีพวกเขาจะต้องแปลงกลับไปเป็นdictast.literal_evalประเภทใช้
  3. ใช้pd.json_normalizeในการแปลงdictsด้วยkeysเป็นส่วนหัวและvaluesสำหรับแถว
    • มีพารามิเตอร์เพิ่มเติม (เช่นrecord_path& meta) dictsสำหรับการรับมือกับซ้อนกัน
  4. ใช้pandas.DataFrame.joinเพื่อรวม DataFrame ดั้งเดิมเข้าdfกับคอลัมน์ที่สร้างโดยใช้pd.json_normalize
    • ถ้าดัชนีไม่ใช่จำนวนเต็ม (ตามตัวอย่าง) ให้ใช้df.reset_index()เพื่อรับดัชนีจำนวนเต็มก่อนที่จะทำการ normalize และ join
  5. สุดท้ายใช้pandas.DataFrame.dropเพื่อลบคอลัมน์ที่ไม่จำเป็นของdicts
  • โปรดทราบว่าหากมีคอลัมน์ใดคอลัมน์หนึ่งคอลัมน์NaNเหล่านี้จะต้องเต็มไปด้วยช่องว่างdict
import pandas as pd
from ast import literal_eval
import numpy as np

data = {'Station ID': [8809, 8810, 8811, 8812, 8813, 8814],
        'Pollutants': ['{"a": "46", "b": "3", "c": "12"}', '{"a": "36", "b": "5", "c": "8"}', '{"b": "2", "c": "7"}', '{"c": "11"}', '{"a": "82", "c": "15"}', np.nan]}

df = pd.DataFrame(data)

# display(df)
   Station ID                        Pollutants
0        8809  {"a": "46", "b": "3", "c": "12"}
1        8810   {"a": "36", "b": "5", "c": "8"}
2        8811              {"b": "2", "c": "7"}
3        8812                       {"c": "11"}
4        8813            {"a": "82", "c": "15"}
5        8814                               NaN

# replace NaN with '{}' if the column is strings, otherwise replace with {}
# df.Pollutants = df.Pollutants.fillna('{}')  # if the NaN is in a column of strings
df.Pollutants = df.Pollutants.fillna({i: {} for i in df.index})  # if the column is not strings

# Convert the column of stringified dicts to dicts
# skip this line, if the column contains dicts
df.Pollutants = df.Pollutants.apply(literal_eval)

# reset the index if the index is not unique integers from 0 to n-1
# df.reset_index(inplace=True)  # uncomment if needed

# normalize the column of dictionaries and join it to df
df = df.join(pd.json_normalize(df.Pollutants))

# drop Pollutants
df.drop(columns=['Pollutants'], inplace=True)

# display(df)
   Station ID    a    b    c
0        8809   46    3   12
1        8810   36    5    8
2        8811  NaN    2    7
3        8812  NaN  NaN   11
4        8813   82  NaN   15
5        8814  NaN  NaN  NaN

%%timeit

# dataframe with 1M rows
dfb = pd.concat([df]*200000).reset_index(drop=True)

%%timeit
dfb.join(pd.json_normalize(dfb.Pollutants))
[out]:
5.44 s ± 32.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%%timeit
pd.concat([dfb.drop(columns=['Pollutants']), dfb.Pollutants.apply(pd.Series)], axis=1)
[out]:
4min 17s ± 2.44 s per loop (mean ± std. dev. of 7 runs, 1 loop each)

1
ขอบคุณสำหรับสิ่งนี้. ฉันทำงานกับ 45M แถวและกำลังรอให้โซลูชันอื่นเสร็จสิ้นเป็นเวลานานกว่า 20 นาทีก่อนที่ฉันจะยกเลิก (อาจจะใช้เวลาหลายชั่วโมง) json_normalize ทำงานใน 2 นาที
Brendan

3

โซลูชันหนึ่งบรรทัดดังต่อไปนี้:

>>> df = pd.concat([df['Station ID'], df['Pollutants'].apply(pd.Series)], axis=1)
>>> print(df)
   Station ID    a    b   c
0        8809   46    3  12
1        8810   36    5   8
2        8811  NaN    2   7
3        8812  NaN  NaN  11
4        8813   82  NaN  15

1

my_df = pd.DataFrame.from_dict(my_dict, orient='index', columns=['my_col'])

.. จะได้แยกวิเคราะห์คำสั่งอย่างถูกต้อง (ใส่คีย์ dict แต่ละรายการลงในคอลัมน์ df ที่แยกจากกันและค่าคีย์ในแถว df) ดังนั้นคำสั่งจะไม่ถูกบีบให้เป็นคอลัมน์เดียวในตอนแรก


0

ฉันได้เชื่อมขั้นตอนเหล่านั้นเข้าด้วยกันแล้วคุณต้องส่งเฉพาะ dataframe และคอลัมน์ที่มี dict เพื่อขยาย:

def expand_dataframe(dw: pd.DataFrame, column_to_expand: str) -> pd.DataFrame:
    """
    dw: DataFrame with some column which contain a dict to expand
        in columns
    column_to_expand: String with column name of dw
    """
    import pandas as pd

    def convert_to_dict(sequence: str) -> Dict:
        import json
        s = sequence
        json_acceptable_string = s.replace("'", "\"")
        d = json.loads(json_acceptable_string)
        return d    

    expanded_dataframe = pd.concat([dw.drop([column_to_expand], axis=1),
                                    dw[column_to_expand]
                                    .apply(convert_to_dict)
                                    .apply(pd.Series)],
                                    axis=1)
    return expanded_dataframe


0
>>> df

   Station ID                        Pollutants
0        8809  {"a": "46", "b": "3", "c": "12"}
1        8810   {"a": "36", "b": "5", "c": "8"}
2        8811              {"b": "2", "c": "7"}
3        8812                       {"c": "11"}
4        8813            {"a": "82", "c": "15"}

การเปรียบเทียบความเร็วสำหรับชุดข้อมูลขนาดใหญ่ 10 ล้านแถว

>>> df = pd.concat([df]*100000).reset_index(drop=True)
>>> df = pd.concat([df]*20).reset_index(drop=True)
>>> print(df.shape)
(10000000, 2)
def apply_drop(df):
    return df.join(df['Pollutants'].apply(pd.Series)).drop('Pollutants', axis=1)  

def json_normalise_drop(df):
    return df.join(pd.json_normalize(df.Pollutants)).drop('Pollutants', axis=1)  

def tolist_drop(df):
    return df.join(pd.DataFrame(df['Pollutants'].tolist())).drop('Pollutants', axis=1)  

def vlues_tolist_drop(df):
    return df.join(pd.DataFrame(df['Pollutants'].values.tolist())).drop('Pollutants', axis=1)  

def pop_tolist(df):
    return df.join(pd.DataFrame(df.pop('Pollutants').tolist()))  

def pop_values_tolist(df):
    return df.join(pd.DataFrame(df.pop('Pollutants').values.tolist()))

>>> %timeit apply_drop(df.copy())
1 loop, best of 3: 53min 20s per loop
>>> %timeit json_normalise_drop(df.copy())
1 loop, best of 3: 54.9 s per loop
>>> %timeit tolist_drop(df.copy())
1 loop, best of 3: 6.62 s per loop
>>> %timeit vlues_tolist_drop(df.copy())
1 loop, best of 3: 6.63 s per loop
>>> %timeit pop_tolist(df.copy())
1 loop, best of 3: 5.99 s per loop
>>> %timeit pop_values_tolist(df.copy())
1 loop, best of 3: 5.94 s per loop
+---------------------+-----------+
| apply_drop          | 53min 20s |
| json_normalise_drop |    54.9 s |
| tolist_drop         |    6.62 s |
| vlues_tolist_drop   |    6.63 s |
| pop_tolist          |    5.99 s |
| pop_values_tolist   |    5.94 s |
+---------------------+-----------+

df.join(pd.DataFrame(df.pop('Pollutants').values.tolist())) เร็วที่สุด

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