在表格或 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事实相吻合;)ffillpadinterpolate


    – 

  • 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非条目NaNb

  • 该表达式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)]。它更易于阅读和编写,因为您不需要用括号括住整个表达式。


    –