楔子
曾經在處理有關地鐵人員數據的時候,遇到過兩種格式的數據,當時確實把我給難住了。雖然最后解決了,但是方法不夠優雅,一個是借助SQL來曲線救國,一個是使用純Python邏輯。但是pandas作為一個非常優秀的第三方庫,肯定提供了相應的解決方案,只不過當時在解決之后就沒有之后了。然鵝最近這樣的數據又碰到了,所以下定決心一定要使用pandas提供的方式解決,最后經過努力總算找到了解決的辦法。
先來看看當時是哪兩種數據讓我感到困惑吧。
第一種數據:
我們需要變成以下這種格式:
第二種數據:
我們需要變成以下這種格式:
下面我們就來看看這兩種格式的數據應該怎么處理。
處理第一種數據(行轉列)
首先pandas中的Series對象有一個方法叫做unstack,調用一個具有二級索引的Series對象的unstack方法,會得到一個DataFrame對象。其索引就是Series對象的一級索引,列就是Series對象的二級索引。
print(df)
"""
姓名 科目 分數
0 古明地覺 語文 90
1 古明地覺 數學 95
2 古明地覺 英語 96
3 芙蘭朵露 語文 87
4 芙蘭朵露 數學 92
5 芙蘭朵露 英語 98
6 琪露諾 語文 100
7 琪露諾 數學 9
8 琪露諾 英語 91
"""
# 將"姓名"和"科目"設置為索引, 然后取出"分數"這一列, 得到的對應的具有二級索引的Series對象
two_level_index_series = df.set_index(["姓名", "科目"])["分數"]
# 此時得到的Series就是一個具有二級索引的Series
# 一級索引就是"姓名"這一列, 二級索引就是"科目"這一列
print(two_level_index_series)
"""
姓名 科目
古明地覺 語文 90
數學 95
英語 96
芙蘭朵露 語文 87
數學 92
英語 98
琪露諾 語文 100
數學 9
英語 91
Name: 分數, dtype: int64
"""
# 調用具有二級索引的Series的unstack, 會得到一個DataFrame
# 並會自動把一級索引變成DataFrame的索引, 二級索引變成DataFrame的列
new_df = two_level_index_series.unstack()
print(new_df)
"""
科目 數學 英語 語文
姓名
古明地覺 95 96 90
琪露諾 9 91 100
芙蘭朵露 92 98 87
"""
# 是不是改回來了呢? 但是還有不完美的地方, 這個DataFrame的索引和列都有一個名字
# 索引的名字叫"姓名", 列的名字叫"科目", 因為原來Series的兩個索引就叫"姓名"和"科目"
# 可以通過 rename_axis(index=, columns=) 來給坐標軸重命名
new_df = new_df.rename_axis(columns=None)
# 這里我們只給列重命名, 沒有給索引重命名, 至於原因請往下看
new_df = new_df.reset_index()
print(new_df)
"""
姓名 數學 英語 語文
0 古明地覺 95 96 90
1 琪露諾 9 91 100
2 芙蘭朵露 92 98 87
"""
# 大功告成, 如果index的名字變為空的話, 那么reset_index之后, 列名就會變成index
# 但如果原來的索引有名字, 那么reset_index之后, 列名就是原來的索引名
調用unstack默認是將一級索引變成DataFrame的索引,二級索引變成DataFrame的列。更准確的說,unstack是將最后一級的索引變成DataFrame的列,前面的索引變成DataFrame的索引。比如有一個具有八級索引的Series,它在調用unstack的時候,默認是將最后一級索引變成DataFrame的列,前面七個索引整體作為DataFrame的索引。
只不過索引一般很少有超過二級的,所以這里就用二級舉例了。因此問題來了,那么可不可以將一級索引(這里的"姓名")
變成DataFrame的列,二級索引(這里的"科目")
變成DataFrame的行呢?答案是可以的,在unstack中指定一個參數即可。
print(df)
"""
姓名 科目 分數
0 古明地覺 語文 90
1 古明地覺 數學 95
2 古明地覺 英語 96
3 芙蘭朵露 語文 87
4 芙蘭朵露 數學 92
5 芙蘭朵露 英語 98
6 琪露諾 語文 100
7 琪露諾 數學 9
8 琪露諾 英語 91
"""
two_level_index_series = df.set_index(["姓名", "科目"])["分數"]
# 這里的level默認是-1, 表示將最后一級的索引變成列
# 這里我們指定為0(注意: 索引從0開始), 告訴pandas, 把第一級索引變成列
new_df = two_level_index_series.unstack(level=0)
new_df = new_df.rename_axis(columns=None)
new_df = new_df.reset_index()
print(new_df)
"""
科目 古明地覺 琪露諾 芙蘭朵露
0 數學 95 9 92
1 英語 96 91 98
2 語文 90 100 87
"""
# 我們看到結果就變了, 這種表示方式雖然有點奇怪, 但它也確實可以正確地表達出數據的含義
其實如果你思維夠靈活的話,那么即使不使用level=0這個參數也可以實現這一點,那就是設置索引的時候不指定
["姓名", "科目"]
,而是指定["科目", "姓名"]
,這樣"科目"就成為了一級索引,"姓名"就成了二級索引,一樣可以完成任務。
除了stack之外, pandas還提供了一個模塊級別的函數: pivot, 可以讓我們更加方便地處理這種數據。
print(df)
"""
姓名 科目 分數
0 古明地覺 語文 90
1 古明地覺 數學 95
2 古明地覺 英語 96
3 芙蘭朵露 語文 87
4 芙蘭朵露 數學 92
5 芙蘭朵露 英語 98
6 琪露諾 語文 100
7 琪露諾 數學 9
8 琪露諾 英語 91
"""
print(pd.pivot(df, index="姓名", columns="科目", values="分數"))
"""
科目 數學 英語 語文
姓名
古明地覺 95 96 90
琪露諾 9 91 100
芙蘭朵露 92 98 87
"""
# 可以看到上面這一步,就直接相當於df.set_index(["姓名", "科目"])["分數"].unstack()
# 然后再手動rename_axis、再reset_index即可
# 如果我們是想將"姓名"變成列的話, 那么就指定columns="姓名"即可
print(pd.pivot(df, index="科目", columns="姓名", values="分數"))
"""
姓名 古明地覺 琪露諾 芙蘭朵露
科目
數學 95 9 92
英語 96 91 98
語文 90 100 87
"""
可以看到pivot算是unstack的一個很好的替代品,但是unstack的靈活性要更高一些,當然啦,兩種解決方式都要掌握。
處理第二種數據(一行生成多行)
print(df)
"""
姓名 生日 聲優
0 琪亞娜·卡斯蘭娜 12月7日 陶典,釘宮理惠
1 布洛妮婭·扎伊切克 8月18日 TetraCalyx,Hanser,阿澄佳奈,花澤香菜
2 德麗莎·阿波卡利斯 3月28日 花玲,田村由香里
"""
df = df.set_index(["姓名", "生日"])["聲優"].str.split(",", expand=True)\
.stack().reset_index(drop=True, level=-1).reset_index().rename(columns={0: "聲優"})
print(df)
"""
姓名 生日 聲優
0 琪亞娜·卡斯蘭娜 12月7日 陶典
1 琪亞娜·卡斯蘭娜 12月7日 釘宮理惠
2 布洛妮婭·扎伊切克 8月18日 TetraCalyx
3 布洛妮婭·扎伊切克 8月18日 Hanser
4 布洛妮婭·扎伊切克 8月18日 阿澄佳奈
5 布洛妮婭·扎伊切克 8月18日 花澤香菜
6 德麗莎·阿波卡利斯 3月28日 花玲
7 德麗莎·阿波卡利斯 3月28日 田村由香里
"""
估計可能有人會懵,我們來一步一步拆解,不過我們需要介紹一下DataFrame的stack方法。
首先Series只有unstack,沒有stack。而DataFrame既有unstack,又有stack。Series(具有二級索引)
的unstack是將自身變成一個DataFrame,而DataFrame的unstack和stack則是將自身變成一個具有二級索引(前提是該DataFrame的索引和列都只有一級)
的Series。
我們上面說了,在默認情況下,對於Series來講,調用其unstack方法,會默認將一級索引變成DataFrame的索引,二級索引變成DataFrame的列(當然可以通過level來控制)
。但是調用DataFrame的unstack則是將自身的索引變成對應Series的二級索引,將自身的列變成對應Series的一級索引;調用DataFrame的stack,則是將自身的索引變成對應Series的一級索引,將自身的列變成對應Series的二級索引。
文字讀起來繞的話,可以看一張圖,在默認情況下如圖所示:
下面我們就來分析之前的那一大長串。
print(df)
"""
姓名 生日 聲優
0 琪亞娜·卡斯蘭娜 12月7日 陶典,釘宮理惠
1 布洛妮婭·扎伊切克 8月18日 TetraCalyx,Hanser,阿澄佳奈,花澤香菜
2 德麗莎·阿波卡利斯 3月28日 花玲,田村由香里
"""
# 我們是對"聲優"這個字段進行分解, 那么將除了該字段之外其它字段設置為索引
df = df.set_index(["姓名", "生日"])
# 此時的df只有"聲優"這一列, 原來的"姓名"和"生日"被設置成了索引
print(df)
"""
聲優
姓名 生日
琪亞娜·卡斯蘭娜 12月7日 陶典,釘宮理惠
布洛妮婭·扎伊切克 8月18日 TetraCalyx,Hanser,阿澄佳奈,花澤香菜
德麗莎·阿波卡利斯 3月28日 花玲,田村由香里
"""
# 篩選出"聲優"這個字段, 此時得到的是一個具有二級索引的Series
# 索引的名字叫做: "姓名" 和 "生日"
s = df["聲優"]
print(s)
"""
姓名 生日
琪亞娜·卡斯蘭娜 12月7日 陶典,釘宮理惠
布洛妮婭·扎伊切克 8月18日 TetraCalyx,Hanser,阿澄佳奈,花澤香菜
德麗莎·阿波卡利斯 3月28日 花玲,田村由香里
Name: 聲優, dtype: object
"""
# 然后對這個字段進行分解, 我這里設置的是以逗號為分隔符
# 具體是以什么分隔, 以你自己的數據為准
# 顯然這里得到的是一個具有二級索引的DataFrame
df = s.str.split(",", expand=True)
# split之后, 字段名使用0 1 2 3..., 默認以最長為基准, 長度不夠的使用None填充
print(df)
"""
0 1 2 3
姓名 生日
琪亞娜·卡斯蘭娜 12月7日 陶典 釘宮理惠 None None
布洛妮婭·扎伊切克 8月18日 TetraCalyx Hanser 阿澄佳奈 花澤香菜
德麗莎·阿波卡利斯 3月28日 花玲 田村由香里 None None
"""
# 調用stack方法, 按照之前說的, 會變成一個Series
# 其索引就是DataFrame的索引加上列變成的索引, 由於DataFrame的索引有兩級, 顯然此時得到的是一個具有三級索引的Series
# 不過也可以把DataFrame的索引當成一個整體看成是Series的一級索引, 然后列變成對應的二級索引
s = df.stack()
# 此時的數據好像有那么回事了, 另外我們發現在stack的時候自動將None給過濾掉了, 這也是我們希望的結果
print(s)
"""
姓名 生日
琪亞娜·卡斯蘭娜 12月7日 0 陶典
1 釘宮理惠
布洛妮婭·扎伊切克 8月18日 0 TetraCalyx
1 Hanser
2 阿澄佳奈
3 花澤香菜
德麗莎·阿波卡利斯 3月28日 0 花玲
1 田村由香里
dtype: object
"""
# 然后調用reset_index, 但是我們發現由於索引有三級,那么reset_index之后就會使得0 1 0 1 2...這些也變成了一列
# 可以reset_index之后手動drop掉, 但是我們也可以直接刪掉
s = s.reset_index(drop=True, level=-1)
# 於是我們指定drop=True, 但是這樣會把所有索引都刪掉, 因此還要指定level=-1, 這樣只會刪除最后一級索引
print(s)
"""
姓名 生日
琪亞娜·卡斯蘭娜 12月7日 陶典
12月7日 釘宮理惠
布洛妮婭·扎伊切克 8月18日 TetraCalyx
8月18日 Hanser
8月18日 阿澄佳奈
8月18日 花澤香菜
德麗莎·阿波卡利斯 3月28日 花玲
3月28日 田村由香里
dtype: object
"""
# 但是我們發現,上面的reset_index(drop=True, level=-1)並沒有把前面的索引變成列
# 這是因為我們指定了level,如果不指定level,那么drop=True會把所有的索引都刪掉
# 但指定了level只會刪除對應級別的索引,而不會同時對前面的索引進行reset,於是需要再調用一次reset_index,此時就什么也不需要指定了
df = s.reset_index()
# 會自動進行笛卡爾乘積
print(df)
"""
姓名 生日 0
0 琪亞娜·卡斯蘭娜 12月7日 陶典
1 琪亞娜·卡斯蘭娜 12月7日 釘宮理惠
2 布洛妮婭·扎伊切克 8月18日 TetraCalyx
3 布洛妮婭·扎伊切克 8月18日 Hanser
4 布洛妮婭·扎伊切克 8月18日 阿澄佳奈
5 布洛妮婭·扎伊切克 8月18日 花澤香菜
6 德麗莎·阿波卡利斯 3月28日 花玲
7 德麗莎·阿波卡利斯 3月28日 田村由香里
"""
# 但是我們發現列名,是自動生成的0,於是再進行rename
df = df.rename(columns={0: "聲優"})
print(df)
"""
姓名 生日 聲優
0 琪亞娜·卡斯蘭娜 12月7日 陶典
1 琪亞娜·卡斯蘭娜 12月7日 釘宮理惠
2 布洛妮婭·扎伊切克 8月18日 TetraCalyx
3 布洛妮婭·扎伊切克 8月18日 Hanser
4 布洛妮婭·扎伊切克 8月18日 阿澄佳奈
5 布洛妮婭·扎伊切克 8月18日 花澤香菜
6 德麗莎·阿波卡利斯 3月28日 花玲
7 德麗莎·阿波卡利斯 3月28日 田村由香里
"""
# 此時就大功告成啦
在pandas0.25版本的時候, DataFrame中新增了一個explode方法, 專門用來將一行變多行。
如果你用過hive的話,那么explode你肯定會很熟悉,是專門用來對一個數組進行"炸裂"的。只不過hive中的explode在對字段進行炸裂的時候不可以有其它的字段,如果在炸裂的同時還能和其它字段進行笛卡爾積,那么還需要加上一個"側寫"的語法。但是pandas中explode是可以直接對DataFrame進行使用的,我們來看一下:
print(df)
"""
姓名 生日 聲優
0 琪亞娜·卡斯蘭娜 12月7日 陶典,釘宮理惠
1 布洛妮婭·扎伊切克 8月18日 TetraCalyx,Hanser,阿澄佳奈,花澤香菜
2 德麗莎·阿波卡利斯 3月28日 花玲,田村由香里
"""
# 此時不需要指定expand=True了, 這里我們需要的是一個列表
df["聲優"] = df["聲優"].str.split(",")
print(df)
"""
姓名 生日 聲優
0 琪亞娜·卡斯蘭娜 12月7日 [陶典, 釘宮理惠]
1 布洛妮婭·扎伊切克 8月18日 [TetraCalyx, Hanser, 阿澄佳奈, 花澤香菜]
2 德麗莎·阿波卡利斯 3月28日 [花玲, 田村由香里]
"""
# 對該字段進行炸裂
df = df.explode("聲優")
print(df)
"""
姓名 生日 聲優
0 琪亞娜·卡斯蘭娜 12月7日 陶典
0 琪亞娜·卡斯蘭娜 12月7日 釘宮理惠
1 布洛妮婭·扎伊切克 8月18日 TetraCalyx
1 布洛妮婭·扎伊切克 8月18日 Hanser
1 布洛妮婭·扎伊切克 8月18日 阿澄佳奈
1 布洛妮婭·扎伊切克 8月18日 花澤香菜
2 德麗莎·阿波卡利斯 3月28日 花玲
2 德麗莎·阿波卡利斯 3月28日 田村由香里
"""
# 我們看到直接就把這個字段給炸開了, 並且炸開的同時會自動和其它字段進行笛卡爾積, 也就是自動匹配
所以explode這個功能真的是好用,而且這種數據也工作中也是非常常見的。
讓我當時感到困惑的數據就是上面那兩種,但是其實還有幾種數據也是非常常見、並且不好處理的,而pandas同樣提供了高效的解決辦法。
根據字典拆分成多列
數據如下:
id info
001 {'姓名': '琪亞娜·卡斯蘭娜', '生日': '12月7日', '外號': '草履蟲'}
002 {'姓名': '布洛妮婭·扎伊切克', '生日': '8月18日', '外號': '板鴨'}
003 {'姓名': '德麗莎·阿波卡利斯', '生日': '3月28日', '外號': '德麗傻', "武器": "猶大的誓約"}
我們需要變成下面這種形式:
id 姓名 生日 外號 武器
001 琪亞娜·卡斯蘭娜 12月7日 草履蟲 None
002 布洛妮婭·扎伊切克 8月18日 板鴨 None
003 德麗莎·阿波卡利斯 3月28日 德麗傻 猶大的誓約
在pandas中,我們知道apply算是一個比較慢的操作,如果可以使用向量化的操作的話,就不要使用apply。但是對於當前這個例子來說,則非常適合apply。
import pandas as pd
df = pd.DataFrame({"id": ["001", "002", "003"],
"info": [{"姓名": "琪亞娜·卡斯蘭娜", "生日": "12月7日", "外號": "草履蟲"},
{"姓名": "布洛妮婭·扎伊切克", "生日": "8月18日", "外號": "板鴨"},
{"姓名": "德麗莎·阿波卡利斯", "生日": "3月28日", "外號": "德麗傻", "武器": "猶大的誓約"}]
})
# 篩選出"info"這一列, 然后使用apply, 里面是一個pd.Series
tmp = df["info"].apply(pd.Series)
# 打印一看, 神奇的事情發生了, 直接就變成了我們想要的結果
print(tmp)
"""
姓名 生日 外號 武器
0 琪亞娜·卡斯蘭娜 12月7日 草履蟲 NaN
1 布洛妮婭·扎伊切克 8月18日 板鴨 NaN
2 德麗莎·阿波卡利斯 3月28日 德麗傻 猶大的誓約
"""
# 因為我們這里的值是一個字典, 而Series接收一個字典的話, 那么字典的key就是索引, value就是值
# 在擴展成DataFrame的時候同樣會考慮到字典中所有的key, 有多少個不重復的key就會生成多少個列
# 如果該行沒有對應的值則使用NaN填充
# 然后就簡單了, 將tmp添加到df中
df[tmp.columns] = tmp
# 然后刪掉"info"這一列
df = df.drop(columns=["info"])
print(df)
"""
id 姓名 生日 外號 武器
0 001 琪亞娜·卡斯蘭娜 12月7日 草履蟲 NaN
1 002 布洛妮婭·扎伊切克 8月18日 板鴨 NaN
2 003 德麗莎·阿波卡利斯 3月28日 德麗傻 猶大的誓約
"""
所以我們看到,只需要對info這一列使用apply(pd.Series),即可將字典變成多個字段,列名就是字典的key,如果不存在,那么值就用NaN填充。
注意:使用apply(pd.Series)的時候,對應的列里面的值必須是一個字典,不能是字典格式的字符串。
很多時候我們的數據從對方的接口、或者對方的數據庫中獲取的,某個字段只是長得像字典,但其實並不是,只是一個類似於json的字符串罷了。這個時候就不可以直接使用apply了。
舉個栗子:
import pandas as pd
df = pd.DataFrame({"id": ["001", "002", "003"],
"info": [str({"姓名": "琪亞娜·卡斯蘭娜", "生日": "12月7日"}),
str({"姓名": "布洛妮婭·扎伊切克", "生日": "8月18日"}),
str({"姓名": "德麗莎·阿波卡利斯", "生日": "3月28日"})]
})
# 顯然"info"字段的所有值都是一個字符串
tmp = df["info"].apply(pd.Series)
print(tmp)
"""
0
0 {'姓名': '琪亞娜·卡斯蘭娜', '生日': '12月7日'}
1 {'姓名': '布洛妮婭·扎伊切克', '生日': '8月18日'}
2 {'姓名': '德麗莎·阿波卡利斯', '生日': '3月28日'}
"""
# 我們看到此時再對info字段使用apply(pd.Series)得到的就不是我們希望的結果了, 因為它不是一個字典
# 這個時候, 我們可以eval一下, 將其變成一個字典
tmp = df["info"].map(eval).apply(pd.Series)
print(tmp)
"""
姓名 生日
0 琪亞娜·卡斯蘭娜 12月7日
1 布洛妮婭·扎伊切克 8月18日
2 德麗莎·阿波卡利斯 3月28日
"""
# 此時就完成啦
不過在生產環境中還會有一個問題,我們知道Python中的eval是將一個字符串里面的內容當成值,或者你理解為就是把字符串周圍的引號給剝掉。比如說:
a = "123", 那么eval(a)得到的就是整型123
a = "[1, 2, 3]", 那么eval(a)得到的就是列表[1, 2, 3]
a = "{'a': 1, 'b': 'xxx'}", 那么eval(a)得到的就是字典{'a': 1, 'b': 'xxx'}
a = "name", 那么eval(a)得到的就是變量name指向的值, 而如果不存在name這個變量, 則會拋出一個NameError
a = "'name'", 那么eval(a)得到的就是'name'這個字符串; 同理a = '"name"', 那么eval(a)得到的依舊是'name'這個字符串
可能有人好奇我為什么要說這些呢?是因為我在工作中經常要從對方的接口中獲取數據,而由於編程語言的不同,導致空值和布爾值之間的表示方式不一樣。
舉個栗子:
import pandas as pd
df = pd.DataFrame({"id": ["001", "002", "003"],
"info": ['{"result": "TH", "is_ok": true}',
'{"result": null, "is_ok": false}',
'{"result": "CL", "is_ok": true}']
})
print(df)
"""
id info
0 001 {"result": "TH", "is_ok": true}
1 002 {"result": null, "is_ok": false}
2 003 {"result": "CL", "is_ok": true}
"""
# 就筆者所在公司而言, 對方公司基本上使用的都是java語言
# 所以返回的值如果包含空值或者布爾值的話, 那么結果是null, true, false
# 顯然這不符合Python中的語法, 一旦eval, 那么就是幾個沒有定義的變量罷了
try:
df["info"] = df["info"].map(eval)
except Exception as e:
print(e) # name 'true' is not defined
# 顯然在對第一個字符串進行eval的時候就報錯了
# 所以這個時候我們需要將null換成None、true換成True、false換成False
# 但是某個值里面也可能恰好包含null、true或者false, 所以它們的前面和后面不可以是\w
df["info"] = df["info"].str.replace(r"(?i)(?<!\w)null(?!\w)", "None").str.\
replace(r"(?i)(?<!\w)true(?!\w)", "True").str.replace(r"(?i)(?<!\w)false(?!\w)", "False")
print(df)
"""
id info
0 001 {"result": "TH", "is_ok": True}
1 002 {"result": None, "is_ok": False}
2 003 {"result": "CL", "is_ok": True}
"""
# 替換成功, 下面進行eval
df["info"] = df["info"].map(eval)
print(type(df.loc[0, "info"])) # <class 'dict'>
# 我們看到此時得到的是一個字典類型, 之后的做法就和之前一樣啦, 不再贅述了
然而當時我遇到的數據沒有這么簡單,它不光是字典格式的字符串,而是一個字符串格式的數組,數組里面是多個字典。如果你認真看完上面的內容,估計你已經猜到需求和解決方法了,直接先"炸裂",再apply(pd.Series)即可。
import pandas as pd
df = pd.DataFrame({"id": ["001", "002", "003"],
"info": [
'[{"result": "TH", "is_ok": true}, {"result": "TH", "is_ok": true}]',
'[{"result": null, "is_ok": false}]',
'[{"result": "CL", "is_ok": true}, {"result": "WH", "is_ok": true}]'
]
})
print(df)
"""
id info
0 001 [{"result": "TH", "is_ok": true}, {"result": "TH", "is_ok": true}]
1 002 [{"result": null, "is_ok": false}]
2 003 [{"result": "CL", "is_ok": true}, {"result": "WH", "is_ok": true}]
"""
# 替換
df["info"] = df["info"].str.replace(r"(?i)(?<!\w)null(?!\w)", "None").str. \
replace(r"(?i)(?<!\w)true(?!\w)", "True").str.replace(r"(?i)(?<!\w)false(?!\w)", "False")
# 此時info字段中的值都還是字符串
print(df)
"""
id info
0 001 [{"result": "TH", "is_ok": True}, {"result": "TH", "is_ok": True}]
1 002 [{"result": None, "is_ok": False}]
2 003 [{"result": "CL", "is_ok": True}, {"result": "WH", "is_ok": True}]
"""
# eval
df["info"] = df["info"].map(eval)
# 雖然打印出來的結果沒有什么變化, 但是字段的值變成了list類型
print(df)
"""
id info
0 001 [{"result": "TH", "is_ok": True}, {"result": "TH", "is_ok": True}]
1 002 [{"result": None, "is_ok": False}]
2 003 [{"result": "CL", "is_ok": True}, {"result": "WH", "is_ok": True}]
"""
# 炸裂, 如果值是一個字符串、整型等標量的話, 那么炸裂得到的結果還是其本身
# 我們需要對列表、元組等容器進行炸裂, 將其內部的元組一個一個的全部炸出來
df = df.explode("info")
print(df)
"""
id info
0 001 {'result': 'TH', 'is_ok': True}
0 001 {'result': 'TH', 'is_ok': True}
1 002 {'result': None, 'is_ok': False}
2 003 {'result': 'CL', 'is_ok': True}
2 003 {'result': 'WH', 'is_ok': True}
"""
# apply(pd.Series)
tmp = df["info"].apply(pd.Series)
df[tmp.columns] = tmp
# drop info
df = df.drop(columns=["info"])
print(df)
"""
id result is_ok
0 001 TH True
0 001 TH True
1 002 None False
2 003 CL True
2 003 WH True
"""
總的來說,pandas在處理這種數據時的表現是非常優秀的。
列轉行
最后我們來看一看列轉行,列轉行可以簡單地認為是將數據庫中的寬表變成一張高表;而之前介紹的行轉列則是把一張高表變成一張寬表。
數據如下:
姓名 水果 星期一 星期二 星期三
古明地覺 草莓 70斤 72斤 60斤
霧雨魔理沙 櫻桃 61斤 60斤 81斤
琪露諾 西瓜 103斤 116斤 153斤
我們希望變成下面這種形式
姓名 水果 日期 銷量
古明地覺 草莓 星期一 70斤
霧雨魔理沙 櫻桃 星期一 61斤
琪露諾 西瓜 星期一 103斤
古明地覺 草莓 星期二 72斤
霧雨魔理沙 櫻桃 星期二 60斤
琪露諾 西瓜 星期二 116斤
古明地覺 草莓 星期三 60斤
霧雨魔理沙 櫻桃 星期三 81斤
琪露諾 西瓜 星期三 153斤
當然我們這里字段比較少,如果把星期一到星期日全部都寫上去有點太長了。不過從這里我們也能看出前者對應的是一張寬表,就是字段非常多,我們要將其轉換成一張高表。
pandas提供了一個模塊級的函數:melt,可以方便地實現這一點。
import pandas as pd
df = pd.DataFrame({"姓名": ["古明地覺", "霧雨魔理沙", "琪露諾"],
"水果": ["草莓", "櫻桃", "西瓜"],
"星期一": ["70斤", "61斤", "103斤"],
"星期二": ["72斤", "60斤", "116斤"],
"星期三": ["60斤", "81斤", "153斤"],
})
print(df)
"""
姓名 水果 星期一 星期二 星期三
古明地覺 草莓 70斤 72斤 60斤
霧雨魔理沙 櫻桃 61斤 60斤 81斤
琪露諾 西瓜 103斤 116斤 153斤
"""
print(pd.melt(df, id_vars=["姓名", "水果"], value_vars=["星期一", "星期二", "星期三"]))
"""
姓名 水果 variable value
古明地覺 草莓 星期一 70斤
霧雨魔理沙 櫻桃 星期一 61斤
琪露諾 西瓜 星期一 103斤
古明地覺 草莓 星期二 72斤
霧雨魔理沙 櫻桃 星期二 60斤
琪露諾 西瓜 星期二 116斤
古明地覺 草莓 星期三 60斤
霧雨魔理沙 櫻桃 星期三 81斤
琪露諾 西瓜 星期三 153斤
"""
# 但是默認起得字段名叫做variable和value, 我們可以在結果的基礎之上手動rename, 也可以直接在參數中指定
print(pd.melt(df, id_vars=["姓名", "水果"],
value_vars=["星期一", "星期二", "星期三"],
var_name="星期幾?",
value_name="銷量"))
"""
姓名 水果 星期幾? 銷量
古明地覺 草莓 星期一 70斤
霧雨魔理沙 櫻桃 星期一 61斤
琪露諾 西瓜 星期一 103斤
古明地覺 草莓 星期二 72斤
霧雨魔理沙 櫻桃 星期二 60斤
琪露諾 西瓜 星期二 116斤
古明地覺 草莓 星期三 60斤
霧雨魔理沙 櫻桃 星期三 81斤
琪露諾 西瓜 星期三 153斤
"""
我們看到實現起來非常方便,轉換之后列變少了、行變多了,這個過程就是所謂的列轉行;而反過來行變少了、列變多了則叫做行轉列。那么這里的melt是如何變換的呢?示意圖如下:
如果還有其它字段的話,那么和explode一樣,會自動進行匹配,一一對應。
所以我們可以介紹一下melt里面的幾個參數了:
frame: 第一個參數, 接收一個DataFrame, 這沒有什么好說的
id_vars: 第二個參數, 不需要進行列轉行的字段, 比如這里的"姓名"和"水果", 在列轉行之后會自動進行匹配
value_vars: 第三個參數, 需要進行列轉行的字段
var_name: 第四個參數, 我們說列轉行之后會生成兩個列, 第一個列存儲的值是"列轉行之前的列的列名",第二個列存儲的值是"列轉行之前的列的值"。但是生成的兩個列重要有列名吧,所以var_name就是生成的第一個列的列名
value_name: 生成的第二個列的列名
col_level: 針對於具有二級列名的DataFrame, 這個一般可以不用管
另外,我們指定列的時候也可以指定一部分的列,比如:
import pandas as pd
df = pd.DataFrame({"姓名": ["古明地覺", "霧雨魔理沙", "琪露諾"],
"水果": ["草莓", "櫻桃", "西瓜"],
"星期一": ["70斤", "61斤", "103斤"],
"星期二": ["72斤", "60斤", "116斤"],
"星期三": ["60斤", "81斤", "153斤"],
})
print(df)
"""
姓名 水果 星期一 星期二 星期三
古明地覺 草莓 70斤 72斤 60斤
霧雨魔理沙 櫻桃 61斤 60斤 81斤
琪露諾 西瓜 103斤 116斤 153斤
"""
# id_vars只指定"水果"
print(pd.melt(df, id_vars=["水果"],
value_vars=["星期一", "星期二"],
var_name="日期",
value_name="銷量"))
"""
水果 日期 銷量
0 草莓 星期一 70斤
1 櫻桃 星期一 61斤
2 西瓜 星期一 103斤
3 草莓 星期二 72斤
4 櫻桃 星期二 60斤
5 西瓜 星期二 116斤
"""
# 也可以指定字段順序, 比如: "水果"在前, "姓名"在后
print(pd.melt(df, id_vars=["水果", "姓名"],
value_vars=["星期一", "星期二"],
var_name="日期",
value_name="銷量"))
"""
水果 姓名 日期 銷量
0 草莓 古明地覺 星期一 70斤
1 櫻桃 霧雨魔理沙 星期一 61斤
2 西瓜 琪露諾 星期一 103斤
3 草莓 古明地覺 星期二 72斤
4 櫻桃 霧雨魔理沙 星期二 60斤
5 西瓜 琪露諾 星期二 116斤
"""
如果要進行"列轉行"的列比較多的話,可以只指定id_vars,那么默認將剩余的所有列作為value_vars。
import pandas as pd
df = pd.DataFrame({"姓名": ["古明地覺", "霧雨魔理沙", "琪露諾"],
"水果": ["草莓", "櫻桃", "西瓜"],
"星期一": ["70斤", "61斤", "103斤"],
"星期二": ["72斤", "60斤", "116斤"],
"星期三": ["60斤", "81斤", "153斤"],
})
print(df)
"""
姓名 水果 星期一 星期二 星期三
古明地覺 草莓 70斤 72斤 60斤
霧雨魔理沙 櫻桃 61斤 60斤 81斤
琪露諾 西瓜 103斤 116斤 153斤
"""
# 默認將除了"姓名"、"水果"之外所有列都進行列轉行了
print(pd.melt(df, id_vars=["水果", "姓名"],
var_name="日期",
value_name="銷量"))
"""
姓名 水果 日期 銷量
古明地覺 草莓 星期一 70斤
霧雨魔理沙 櫻桃 星期一 61斤
琪露諾 西瓜 星期一 103斤
古明地覺 草莓 星期二 72斤
霧雨魔理沙 櫻桃 星期二 60斤
琪露諾 西瓜 星期二 116斤
古明地覺 草莓 星期三 60斤
霧雨魔理沙 櫻桃 星期三 81斤
琪露諾 西瓜 星期三 153斤
"""
# 但是注意: 在value_vars省略的時候, 則需要仔細考慮一下id_vars
# 什么意思呢?看個栗子
print(pd.melt(df, id_vars=["水果"],
var_name="日期",
value_name="銷量"))
# 我們看到它將"姓名"這一列也進行列轉行了, 但是顯然我們不想這么做
# 但是在省略value_vars的時候, 會將除了id_vars指定的字段之外的其它所有字段都進行列轉行
# 所以在省略value_vars的時候, 務必注意id_vars, 假設這里要列傳行的列很多, 不想一個一個寫, 但是id_vars又不想指定"姓名"
# 那么可以在列轉行之前就將"姓名"這一列刪掉即可, 也就是把上面的df換成df.drop(columns=["姓名"])即可
"""
水果 日期 銷量
0 草莓 姓名 古明地覺
1 櫻桃 姓名 霧雨魔理沙
2 西瓜 姓名 琪露諾
3 草莓 星期一 70斤
4 櫻桃 星期一 61斤
5 西瓜 星期一 103斤
6 草莓 星期二 72斤
7 櫻桃 星期二 60斤
8 西瓜 星期二 116斤
9 草莓 星期三 60斤
10 櫻桃 星期三 81斤
11 西瓜 星期三 153斤
"""
# 此時就沒有任何問題了
print(pd.melt(df.drop(columns=["姓名"]),
id_vars=["水果"],
var_name="日期",
value_name="銷量"))
"""
水果 日期 銷量
0 草莓 星期一 70斤
1 櫻桃 星期一 61斤
2 西瓜 星期一 103斤
3 草莓 星期二 72斤
4 櫻桃 星期二 60斤
5 西瓜 星期二 116斤
6 草莓 星期三 60斤
7 櫻桃 星期三 81斤
8 西瓜 星期三 153斤
"""
事實上,列轉行除了使用melt,還可以使用我們之前說的stack,只不過melt會更加的方便。那這樣,我們就來使用stack來模擬一下melt吧。
import pandas as pd
def my_melt(frame: pd.DataFrame,
id_vars: list,
value_vars: list,
var_name: str,
value_name: str):
# 先將id_vars和value_vars指定的字段篩選出來
frame = frame[id_vars + value_vars]
# 將id_vars指定的字段設置為索引
frame = frame.set_index(id_vars)
print(">>>篩選字段、設置索引之后對應的DataFrame:\n", frame)
# 調用frame的stack方法, 會得到一個具有多級索引的Series
# frame的列就是生成的Series的最后一級索引
s = frame.stack()
print(">>>得到的具有多級索引的Series:\n", s)
# 直接對該Series進行reset_index即可, 會得到DataFrame, 將索引變成列
frame = s.reset_index()
print(">>>具有多級索引的Series執行reset_index:\n", frame)
# 重命名
frame.columns = id_vars + [var_name, value_name]
print(">>>最終返回的DataFrame:")
return frame
df = pd.DataFrame({"姓名": ["古明地覺", "霧雨魔理沙", "琪露諾"],
"水果": ["草莓", "櫻桃", "西瓜"],
"星期一": ["70斤", "61斤", "103斤"],
"星期二": ["72斤", "60斤", "116斤"],
"星期三": ["60斤", "81斤", "153斤"],
})
print(df)
"""
姓名 水果 星期一 星期二 星期三
古明地覺 草莓 70斤 72斤 60斤
霧雨魔理沙 櫻桃 61斤 60斤 81斤
琪露諾 西瓜 103斤 116斤 153斤
"""
print(my_melt(df, id_vars=["姓名", "水果"],
value_vars=["星期一", "星期二"],
var_name="日期", value_name="銷量"))
"""
>>>篩選字段、設置索引之后對應的DataFrame:
星期一 星期二
姓名 水果
古明地覺 草莓 70斤 72斤
霧雨魔理沙 櫻桃 61斤 60斤
琪露諾 西瓜 103斤 116斤
>>>得到的具有多級索引的Series:
姓名 水果
古明地覺 草莓 星期一 70斤
星期二 72斤
霧雨魔理沙 櫻桃 星期一 61斤
星期二 60斤
琪露諾 西瓜 星期一 103斤
星期二 116斤
dtype: object
>>>具有多級索引的Series執行reset_index:
姓名 水果 level_2 0
0 古明地覺 草莓 星期一 70斤
1 古明地覺 草莓 星期二 72斤
2 霧雨魔理沙 櫻桃 星期一 61斤
3 霧雨魔理沙 櫻桃 星期二 60斤
4 琪露諾 西瓜 星期一 103斤
5 琪露諾 西瓜 星期二 116斤
>>>最終返回的DataFrame:
姓名 水果 日期 銷量
0 古明地覺 草莓 星期一 70斤
1 古明地覺 草莓 星期二 72斤
2 霧雨魔理沙 櫻桃 星期一 61斤
3 霧雨魔理沙 櫻桃 星期二 60斤
4 琪露諾 西瓜 星期一 103斤
5 琪露諾 西瓜 星期二 116斤
"""
此時我們就通過stack自己實現了pandas中的melt,對比內置的melt函數實現的結果,我們發現順序有些差異,但顯然結果是一樣的。
小結
以上就是一些經常會遇見,但是不熟悉的話處理起來又會感到麻煩的幾種數據以及需求,再來總結一下:
行轉列:1. 設置索引、篩選單個字段,得到一個二級的Series。2. 調用Series的unstack,得到一個DataFrame。3. rename_axis。4. reset_index。另外我們說還有一個pd.pivot可以讓我們直接跳轉到上面的第三步
一行生成多行:1. 如果該列里面的元素不是列表,那么變成列表。2. 調用explode對該字段進行炸裂。
根據字典拆分成多列:1. 如果該列里面的元素不是字典,那么變成字典。2. 調用.apply(pd.Series)。
一行生成多行和根據字典拆分成多列也可以結合起來,先explode,再apply
列轉行:通過melt一步搞定。或者使用stack
因此針對行轉列和列轉行,早期我個人經常使用unstack和stack。但是之后就沒怎么用了,因為pivot和melt算是unstack和stack的一個很好的替代品,可以幫助我們更快地實現。另外,除了行轉列和列轉行之外,關於展開列表實現一行生成多行、將字典變成多個字段,pandas也依舊提供了很棒的支持。