楔子
我们在用 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 上面。
以后当我们在对 DataFrame 的两列数据进行对比处理的时候,不妨使用上面三个方法。