详解Pandas如何高效对比处理DataFrame的两列数据

 

楔子

我们在用 pandas 处理数据的时候,经常会遇到用其中一列数据替换另一列数据的场景。比如 A 列和 B 列,对 A 列中不为空的数据不作处理,对 A 列中为空的数据使用 B 列对应索引的数据进行替换。这一类的需求估计很多人都遇到,当然还有其它更复杂的。

解决这类需求的办法有很多,这里我们来推荐几个。

 

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 对象的索引是一致的,因为替换是根据索引来指定位置的,举个例子。

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 没有,那么会直接加上去。

注意:pandas 的很多操作都是基于自带的索引进行的,并不是简单的从上往下一一对应。即便是很多 pandas 老手,偶尔也会犯这个错误。

当然大部分情况下我们处理的都是同一个 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
"""

# 两个元素进行相乘
# 当然,对于目前这个需求,最好的办法是 s1 * s2
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
"""

combine 和 combine_first 都是先让 s1 和 s2 的索引变得一致之后,再进行操作。

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_first(s2))
"""
a     1.0
b    22.0
c     3.0
d    44.0
e    33.0
f     4.0
dtype: float64
"""

所以你会发现,s1 和 s2 里面都没有空值,返回的结果也没有空值,但是类型却从整型变成了浮点型。就是因为 s1 和 s2 在 reindex 的过程中出现了 NaN,所以类型变成了浮点型。

所以在使用 combine 和 combine_first 这两个方法的时候,一定要记住索引,否则可能会造成陷阱。事实上,包括 pandas 很多的其它操作也是,它们都是基于索引来的,并不是简单的依次从左到右或者从上到下。

 

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 进行操作,有兴趣可以自己去了解,但大部分时候我们都用在 Series 上面。

关于详解Pandas如何高效对比处理DataFrame的两列数据的文章就介绍至此,更多相关Pandas处理DataFrame数据内容请搜索编程宝库以前的文章,希望以后支持编程宝库

 处理json中不带双引号key问题在解析网页json数据的时候,我发现python标准库json模块无法加载数据。如下面数据import jsons='{symbol:"sh60006 ...