使用combine_first、combine、update高效的處理DataFrame中的列


楔子

我們在用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進行操作,有興趣可以自己去了解。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM