在表格或 DataFrame 中出现前导值和尾随值的情况相当常见NaN
。在连接之后和时间序列数据中尤其如此。
import numpy as np
import pandas as pd
df1 = pd.DataFrame({
'a': [1, 2, 3, 4, 5, 6, 7],
'b': [np.NaN, 2, np.NaN, 4, 5, np.NaN, np.NaN],
})
Out[0]:
a b
0 1 NaN
1 2 2.0
2 3 NaN
3 4 4.0
4 5 5.0
5 6 NaN
6 7 NaN
让我们用 删除它们dropna
。
df1.dropna()
Out[1]:
a b
1 2 2.0
3 4 4.0
4 5 5.0
哦不!我们丢失了列中显示的所有缺失值b
。我想保留中间(内部)的值。
NaN
如何快速、干净、高效地删除带有前导值和尾随值的行?结果应如下所示:
df1.stripna()
# obviously I'm not asking you to create a new pandas method...
# I just thought it was a good name.
Out[3]:
a b
1 2 2.0
2 3 NaN
3 4 4.0
4 5 5.0
到目前为止,有些答案都很不错,但我认为这个功能非常重要,如果有人感兴趣的话,我向 Pandas 提出了一个功能请求。让我们看看进展如何!
最佳答案
4
另一种可能更具可读性的方法是使用并使用索引切片loc
:
df1.loc[df1['b'].first_valid_index():df1['b'].last_valid_index()]
输出:
a b
1 2 2.0
2 3 NaN
3 4 4.0
4 5 5.0
而且,这应该真的很快。使用@LittleBobbyTables。
%timeit df1.loc[df1['b'].ffill().notna()&df1['b'].bfill().notna()]
每循环 24.2 毫秒 ± 610 微秒(7 次运行的平均值 ± 标准差,每次 10 次循环)
对比:
%timeit df1.loc[df1['b'].first_valid_index():df1['b'].last_valid_index()]
每循环 1.43 毫秒 ± 34.3 微秒(7 次运行的平均值 ± 标准差,每次 1,000 次循环)
4
-
3非常好。但是,这种方法的缺点是,如果中的所有值
df1['b']
都是 ,它会给出错误的结果np.nan
。这将计算为df1.loc[None:None]
,从而返回整个df1
,而预期结果应该是一个空的 DataFrame。
– -
3@ouroboros1 您可以先添加一个检查,例如
assert df1['b'].first_valid_index() is not None
。(不需要检查最后一个,因为至少一个有效的第一个索引意味着至少一个有效的最后一个索引。)然后您可以将其打包在一个函数中以方便使用。
–
-
1对此解决方案的观察和评论都非常棒。谢谢!
– -
1@ouroboros1 太棒了。对于我的用例来说,这几乎肯定不是问题,但这是一个值得注意的有趣边缘情况。
–
|
您可以使用/ +来构建的掩码:
out = df1.loc[df1['b'].ffill().notna()&df1['b'].bfill().notna()]
或者,使用:
out = df1.loc[df1['b'].interpolate(limit_area='inside').notna()]
或者使用:
m = df1['b'].notna()
out = df1.loc[m.cummax() & m[::-1].cummax()]
输出:
a b
1 2 2.0
2 3 NaN
3 4 4.0
4 5 5.0
中间体:
# bfill/fill
a b bfill ffill bfill+notna ffill+notna &
0 1 NaN 2.0 NaN True False False
1 2 2.0 2.0 2.0 True True True
2 3 NaN 4.0 2.0 True True True
3 4 4.0 4.0 4.0 True True True
4 5 5.0 5.0 5.0 True True True
5 6 NaN NaN 5.0 False True False
# cummax
a b m cummax reverse_cummax &
0 1 NaN False False True False
1 2 2.0 True True True True
2 3 NaN False True True True
3 4 4.0 True True True True
4 5 5.0 True True True True
5 6 NaN False True False False
2
-
3很好地配合使用
interpolate
,更好地进行插值,notna
以便它适用于所有数据类型。
– -
@QuangHoang 我不确定该如何进行。
(m:=df1['b'].notna().convert_dtypes()).where(m).interpolate('pad', limit_area='inside').fillna(False)
有点麻烦。
–
|
如果您使用 ,以下是 mozwaypandas>=2.2
的轻微改进。您可以使用witharea_limit
代替interpolate
。
interpolate
由于默认为,这应该会更快一些linear
。
out = df1.loc[df1['b'].ffill(limit_area='inside').notna()]
但就纯粹的速度和对旧版熊猫的支持而言,你无法击败斯科特波士顿(Scott Boston)的极快(>10 倍)的first/last_valid_index
答案。
df1.loc[df1['b'].first_valid_index():df1['b'].last_valid_index()]
速度测试!
import numpy as np
import pandas as pd
df1 = pd.DataFrame({
'a': range(1_000_000),
'b': [
*([np.NaN] * 100_000),
*range(100_000, 200_000),
*([np.NaN] * 100_000),
*range(300_000, 900_000),
*([np.NaN] * 100_000),
],
})
%timeit
df1.loc[df1['b'].ffill().notna()&df1['b'].bfill().notna()]
Out[1]:
12.3 ms ± 258 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%%timeit
df1.loc[df1['b'].interpolate(limit_area='inside').notna()]
Out[2]:
55.3 ms ± 1.8 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%%timeit
m = df1['b'].notna()
out = df1.loc[m.cummax() & m[::-1].cummax()]
Out[3]:
128 ms ± 3.97 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%%timeit
df1.loc[df1['b'].ffill(limit_area='inside').notna()]
Out[4]:
15.4 ms ± 681 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
ffill
在这种情况下,如果使用而不是,pandas 2.2 的速度会提高 3.5 倍interpolate
。与最快的第一个示例相比,它更容易阅读。
first/last_valid_index
速度极快,击败了竞争对手,而且非常易读。
%%timeit
df1.loc[df1['b'].first_valid_index():df1['b'].last_valid_index()]
Out[5]:
798 µs ± 55.4 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
这里的轻微缺点是,如果所有行b
都是 NaN,它可能会返回意外结果(完整的 DataFrame),并且也很难扩展到更广泛的用例(例如多个缺失的列)。
4
-
2说得好,我忘记了 的这个附加功能。实际上,这与 的/行为被弃用的
ffill
事实相吻合;)ffill
pad
interpolate
– -
1@ScottBoston 补充道。它在我的计算机上运行速度也很快!!
– -
1仅供参考,有和会检查是否有任何列具有非 nan 值并返回第一个/最后一个索引。
– -
1知道这些很有用。这会使你的案例更具扩展性!我认为
df1.loc[df1['b', 'c'].ffill(limit_area='inside').notna().any()]
将逻辑调整到多列可能更容易,例如any
用替换all
。
–
|
解决方案 1:.isna().cumprod()
作为布尔值
此解决方案:
-
首先创建一个
nans
用于识别NaN
条目的布尔掩码。 -
掩码
start
使用 的累积乘积nans
将所有行标记为 ,直到遇到True
第一个非值。NaN
-
掩码
end
反转数据框,执行类似的累积乘积来识别尾随NaN
值。 -
最后,通过对组合掩码取反来过滤数据框
(~(start | end))
,仅保留既不是前导也不是尾随的行NaN
,从而保留任何内部NaN
值。
nans = df1['b'].isna()
start = nans.cumprod().astype(bool)
end = nans[::-1].cumprod().astype(bool)
df1[~(start | end)]
解决方案 2:.notna().cumsum().gt(0)
此解决方案:
-
首先创建一个布尔掩码,用于标识列中的
notnans
非条目。NaN
b
-
该表达式
notnans.cumsum().gt(0)
生成一个掩码,将所有行标记为True
从第一个非NaN
值开始。 -
notnans[::-1].cumsum().gt(0)
反转布尔掩码以从末尾执行累积和,将行标记为直到遇到True
最后一个非值。NaN
-
最后的过滤保留了同时满足两个条件的行
True
。
notnans = df1['b'].notna()
df1[(notnans.cumsum().gt(0)) & (notnans[::-1].cumsum().gt(0))]
注意: @wjandrea 在下面的评论中观察到,非常有趣的是,反转的系列不需要反转回来,因为索引保证了系列的正确对齐。
输出:
a b
1 2 2.0
2 3 NaN
3 4 4.0
4 5 5.0
11
-
是的,这确实解决了我上面给出的非常具体的例子所显示的问题,但概念更多的是“如何去除多个前导值和尾随
NaN
值?”
– -
这似乎很难扩展到未知数量的外部
NaN
。
– -
1调整我的方法以适应该设置并不困难,@LittleBobbyTables。我稍后会这样做,因为我现在必须离开电脑。
– -
1你实际上不需要反转,因为 Pandas 会根据索引进行对齐,而索引是通过第一次反转保留下来的
–
-
1就我个人而言,我
.gt()
更喜欢>
使用 来进行链接:df1[notnans.cumsum().gt(0) & notnans[::-1].cumsum().gt(0)]
。它更易于阅读和编写,因为您不需要用括号括住整个表达式。
–
|
|