楔子
我們在用pandas處理數據的時候,經常會遇到用其中一列替換另一列的數據。比如A列和B列,對A列中不為空的數據不作處理,對A列中為空的數據使用B列對應的數據進行替換。這一類的需求估計很多人都遇到,當然還有其它更復雜的。
解決這類需求的辦法有很多,比如效率不高的apply,或者使用向量化的loc等等。那么這次我們來看一下幾個非常簡便,同樣高效率的辦法。
combine_first
這個方法是專門用來針對空值處理的,我們來看一下用法
import pandas as pd
df = pd.DataFrame(
{"A": ["001", None, "003", None, "005"],
"B": ["1", "2", "3", "4", "5"]}
)
print(df)
"""
A B
0 001 1
1 None 2
2 003 3
3 None 4
4 005 5
"""
# 我們現在需求如下,如果A列中的數據不為空,那么不做處理。
# 為空,則用B列中對應的數據進行替換
df["A"] = df["A"].combine_first(df["B"])
print(df)
"""
A B
0 001 1
1 2 2
2 003 3
3 4 4
4 005 5
"""
使用方法很簡單,首先是兩個Series對象,假設叫s1和s2,那么s1.combine_first(s2)就表示用s2替換掉s1中為空的數據,如果s1和s2的某個相同索引對應的數據都是空,那么結果只能是空。當然這個方法不是在原地操作,而是會返回一個新的Series對象
另外這個方法的理想前提是兩個Series對象的索引是一致的,因為替換是根據索引來指定位置的
比如s1中index為1的數據為空,那么就會使用s2中index為1的數據進行替換。但如果s2中沒有index為1數據,那么就不會替換了。並且,如果假設s2中存在index為100的數據,但是s1中沒有,那么結果就會多出一個index為100的數據。
下面來演示一下
import pandas as pd
s1 = pd.Series(["001", None, None, "004"], index=['a', 'b', 'c', 'd'])
s2 = pd.Series(["2", "3", "4"], index=['b', 'd', "e"])
print(s1)
"""
a 001
b None
c None
d 004
dtype: object
"""
print(s2)
"""
b 2
d 3
e 4
dtype: object
"""
print(s1.combine_first(s2))
"""
a 001
b 2
c NaN
d 004
e 4
dtype: object
"""
解釋一下,首先替換的都是s1中值為空的數據,如果不為空那么不做任何處理。s1中值為空的數據有兩個,分別是索引為"b"、"c",那么會用s2中索引為"b"、"c"的數據進行替換。但是s2中存在索引為"b"、卻不存在索引為"c"的數據,那么就只能替換一個值。
另外我們看到結尾還多了個索引為e的數據,是的,我們說如果s2中的數據,s1中沒有,那么會直接加上去。
import pandas as pd
s1 = pd.Series(['1', '2', '3', '4'], index=['a', 'b', 'c', 'd'])
s2 = pd.Series(['11', '22', '33'], index=['d', 'e', 'f'])
print(s1.combine_first(s2))
"""
a 1
b 2
c 3
d 4
e 22
f 33
dtype: object
"""
我們看到s2中,存在索引為'e'、'f'的數據,但是s1中沒有,那么就直接加進去了。當然,如果s1中的數據在s2中沒有,那么也會直接保留s1。如果兩者都有,那么看s1的數據是否為空,如果為空,那么用s2對應索引的數據替換,不為空則保留s1、也就是不替換。
當然大部分情況下我們處理的都是同一個DataFrame的兩列,對於同一個DataFrame中的兩列,它們的索引顯然是一致的,所以就是簡單的從上到下,不會有太多花里胡哨的。
combine
combine和combine_first類似,只是需要指定一個函數。
import pandas as pd
df = pd.DataFrame(
{"A": ["001", None, "003", None, "005"],
"B": ["1", "2", "3", "4", "5"]}
)
print(df)
"""
A B
0 001 1
1 None 2
2 003 3
3 None 4
4 005 5
"""
df["A"] = df["A"].combine(df["B"], lambda a, b: a if pd.notna(a) else b)
print(df)
"""
A B
0 001 1
1 2 2
2 003 3
3 4 4
4 005 5
"""
我們指定了一個匿名函數,參數a、b就代表df["A"]和df["B"]中對應的每一個數據。如果a不為空,那么返回a,否則返回b。
所以我們看到,我們使用combine實現了和combine_first的效果。combine_first是專門對空值進行替換的,但是combine則是可以讓我們自己指定邏輯。我們可以實現combine_first的功能,也可以實現其它的功能
import pandas as pd
s1 = pd.Series([1, 22, 3, 44])
s2 = pd.Series([11, 2, 33, 4])
# 哪個元素大就保留哪一個
print(s1.combine(s2, lambda a, b: a if a > b else b))
"""
0 11
1 22
2 33
3 44
dtype: int64
"""
# 兩個元素進行相乘
print(s1.combine(s2, lambda a, b: a * b))
"""
0 11
1 44
2 99
3 176
dtype: int64
"""
combine的功能還是很強大的,當然它同樣是針對索引來操作的。事實上combine和combine_first內部會先對索引進行處理,如果兩個Series對象的索引不一樣,那么會先將它們索引變得一致。
import pandas as pd
s1 = pd.Series([1, 22, 3, 44], index=['a', 'b', 'c', 'd'])
s2 = pd.Series([11, 2, 33, 4], index=['c', 'd', 'e', 'f'])
# 先對兩個索引取並集
index = s1.index.union(s2.index)
print(index) # Index(['a', 'b', 'c', 'd', 'e', 'f'], dtype='object')
# 然后通過reindex,獲取指定索引的元素,當然索引不存在就用NaN代替
s1 = s1.reindex(index)
s2 = s2.reindex(index)
print(s1)
"""
a 1.0
b 22.0
c 3.0
d 44.0
e NaN
f NaN
dtype: float64
"""
print(s2)
"""
a NaN
b NaN
c 11.0
d 2.0
e 33.0
f 4.0
dtype: float64
"""
# 在將s1和s2的索引變得一致之后,依次進行操作。
再回過頭看一下combine_first
import pandas as pd
s1 = pd.Series([1, 22, 3, 44], index=['a', 'b', 'c', 'd'])
s2 = pd.Series([11, 2, 33, 4], index=['a', 'b', 'c', 'e'])
print(s1.combine_first(s2))
"""
a 1.0
b 22.0
c 3.0
d 44.0
e 4.0
dtype: float64
"""
# 一開始的話可能有人會好奇為什么類型變了,但是現在顯然不會有疑問了
# 因為s1和s2的索引不一致,index='e'在s1中不存在,index='d'在s2中不存在
# 而reindex如果指定不存在索引,則用NaN代替
# 而如果出現了NaN,那么類型就由整型變成了浮點型。
# 但兩個Series對象的index如果一樣,那么reindex的結果也還是和原來一樣,由於沒有NaN,那么類型就不會變化
# 所以我們可以自己實現一個combine_first,當然pandas內部也是這么做的
s1 = s1.reindex(['a', 'b', 'c', 'd', 'e'])
s2 = s2.reindex(['a', 'b', 'c', 'd', 'e'])
print(s1)
"""
a 1.0
b 22.0
c 3.0
d 44.0
e NaN
dtype: float64
"""
print(s2)
"""
a 11.0
b 2.0
c 33.0
d NaN
e 4.0
dtype: float64
"""
# s1不為空,否則用s2替換
print(s1.where(pd.notna(s1), s2))
"""
a 1.0
b 22.0
c 3.0
d 44.0
e 4.0
dtype: float64
"""
再重新回過頭看一下combine
import pandas as pd
s1 = pd.Series([1, 22, 3, 44], index=['a', 'b', 'c', 'd'])
s2 = pd.Series([11, 2, 33, 4], index=['c', 'd', 'e', 'f'])
print(s1.combine(s2, lambda a, b: a if a > b else b))
"""
a NaN
b NaN
c 11.0
d 44.0
e 33.0
f 4.0
dtype: float64
"""
# 為什么出現這個結果,相信你很容易就分析出來
# reindex之后:
# s1的數據變成[1.0, 22.0, 33.0, 44.0, NaN, NaN]
# s2的數據變成[NaN, NaN, 11.0, 2.0, 33.0, 4.0]
# 然后依次比較,1.0 > NaN為False,那么保留b,而b為NaN, 所以結果的第一個元素為NaN,同理第二個也是如此。
# 同理最后兩個元素,和NaN比較也是False,還是保留b,那么最后兩個元素則是33.0和4.0
# 至於index為c、d的元素就沒有必要分析了,顯然是保留大的那個
所以還是那句話,對於combine和combine_first來說,它們是對相同索引的元素進行比較,如果兩個Series對象的索引不一樣,那么會先取並集,然后通過reindex,再進行比較。再舉個栗子:
import pandas as pd
s1 = pd.Series([1, 2, 3, 4])
s2 = pd.Series([1, 2, 3, 4])
# 兩個元素是否相等,相等返回True,否則返回False
print(s1.combine(s2, lambda a, b: True if a == b else False))
"""
0 True
1 True
2 True
3 True
dtype: bool
"""
s2.index = [0, 1, 3, 2]
print(s1.combine(s2, lambda a, b: True if a == b else False))
"""
0 True
1 True
2 False
3 False
dtype: bool
"""
# 當我們將s2的索引變成了[0, 1, 3, 2]結果就不對了
print(s1.index.union(s2.index)) # Int64Index([0, 1, 2, 3], dtype='int64')
# 此時reindex的結果,s1還是1、2、3、4,但是s2則變成了1、2、4、3
所以在使用combine和combine_first這兩個方法的時候,一定要記住索引,否則可能會造成陷阱。事實上,包括pandas很多的其它操作也是,它們都是基於索引來的,並不是簡單的依次從左到右或者從上到下
但還是那句話,我們很多時候都是對DataFrame中的兩列進行操作,而它們索引是一樣的,所以不需要想太多。
當然這兩個方法除了針對Series對象,還可以針對DataFrame對象,比如:df1.combine(df2, func),對相同的column進行替換,但不是很常用,有興趣可以自己研究。我們主要還是作用於Series對象
update
update比較野蠻,我們來看一下。
import pandas as pd
s1 = pd.Series([1, 2, 3, 4])
s2 = pd.Series([11, 22, 33, 44])
s1.update(s2)
print(s1)
"""
0 11
1 22
2 33
3 44
dtype: int64
"""
首先我們看到這個方法是在本地進行操作的,功能還是用s2的元素替換s1的元素,並且只要s2中的元素不為空,那么就進行替換。
import pandas as pd
s1 = pd.Series([1, 2, 3, 4])
s2 = pd.Series([11, 22, None, 44])
s1.update(s2)
print(s1)
"""
0 11
1 22
2 3
3 44
dtype: int64
"""
所以這個函數叫update,意思就是更新。用s2中的元素換掉s1中的元素,但如果s2中的元素為空,那么可以認為'新版本'還沒出來,那么還是使用老版本,所以s1中的3沒有被換掉。
所以update和combine_first比較類似,但它們的區別在於:
combine_first:如果s1中的值為空,用s2的值替換,否則保留s1的值
update:如果s2中的值不為空,那么替換s1,否則保留s1的值
另外在combine_first的時候,我們反復強調了索引的問題,如果s1和s2索引不一樣,那么生成的結果的元素個數會多。但是update不一樣,因為它是在本地進行操作的,也就是直接本地修改s1。所以最終s1的元素個數是不為發生變化的。
import pandas as pd
s1 = pd.Series([1, 2, 3, 4], index=['a', 'b', 'c', 'd'])
s2 = pd.Series([11, 22, 33, 44], index=['c', 'd', 'e', 'f'])
s1.update(s2)
print(s1)
"""
a 1
b 2
c 11
d 22
dtype: int64
"""
s2中不存在index為'a'、'b'的元素,那么可以認為'新版本'沒有出現,因此不更新、保留原來的值。但是s2中存在index為'c'、'd'的元素,所以有'新版本',那么就更新。所以s1由[1 2 3 4]變成了[1 2 11 22],至於s2中index為'e'、'f'的元素,它們和s1沒有關系,因為s1中壓根沒有index為'e'、'f'的元素,s2中提供了'新版本'也是沒用的。所以使用update,是在s1本地操作的,操作前后s1的索引以及元素個數不會改變。
當然update也適用於對兩個DataFrame進行操作,有興趣可以自己去了解。