【问题标题】:Interpolate (or extrapolate) only small gaps in pandas dataframe仅插入(或外推)熊猫数据框中的小间隙
【发布时间】:2015-08-12 12:33:35
【问题描述】:

我有一个以时间为索引(1 分钟频率)和几列数据的 pandas DataFrame。有时数据包含 NaN。如果是这样,我只想在间隙不超过 5 分钟时进行插值。在这种情况下,这将是最多 5 个连续的 NaN。数据可能看起来像这样(几个测试用例,显示了问题):

import numpy as np
import pandas as pd
from datetime import datetime

start = datetime(2014,2,21,14,50)
data = pd.DataFrame(index=[start + timedelta(minutes=1*x) for x in range(0, 8)],
                         data={'a': [123.5, np.NaN, 136.3, 164.3, 213.0, 164.3, 213.0, 221.1],
                               'b': [433.5, 523.2, 536.3, 464.3, 413.0, 164.3, 213.0, 221.1],
                               'c': [123.5, 132.3, 136.3, 164.3] + [np.NaN]*4,
                               'd': [np.NaN]*8,
                               'e': [np.NaN]*7 + [2330.3],
                               'f': [np.NaN]*4 + [2763.0, 2142.3, 2127.3, 2330.3],
                               'g': [2330.3] + [np.NaN]*7,
                               'h': [2330.3] + [np.NaN]*6 + [2777.7]})

它是这样写的:

In [147]: data
Out[147]: 
                         a      b      c   d       e       f       g       h
2014-02-21 14:50:00  123.5  433.5  123.5 NaN     NaN     NaN  2330.3  2330.3
2014-02-21 14:51:00    NaN  523.2  132.3 NaN     NaN     NaN     NaN     NaN
2014-02-21 14:52:00  136.3  536.3  136.3 NaN     NaN     NaN     NaN     NaN
2014-02-21 14:53:00  164.3  464.3  164.3 NaN     NaN     NaN     NaN     NaN
2014-02-21 14:54:00  213.0  413.0    NaN NaN     NaN  2763.0     NaN     NaN
2014-02-21 14:55:00  164.3  164.3    NaN NaN     NaN  2142.3     NaN     NaN
2014-02-21 14:56:00  213.0  213.0    NaN NaN     NaN  2127.3     NaN     NaN
2014-02-21 14:57:00  221.1  221.1    NaN NaN  2330.3  2330.3     NaN  2777.7

我知道data.interpolate(),但它有几个缺陷,因为它会产生这样的结果,这对 a-e 列有好处,但对于 f-h 列,由于不同的原因它失败了::

                         a      b      c   d       e       f       g  \
2014-02-21 14:50:00  123.5  433.5  123.5 NaN     NaN     NaN  2330.3   
2014-02-21 14:51:00  129.9  523.2  132.3 NaN     NaN     NaN  2330.3   
2014-02-21 14:52:00  136.3  536.3  136.3 NaN     NaN     NaN  2330.3   
2014-02-21 14:53:00  164.3  464.3  164.3 NaN     NaN     NaN  2330.3   
2014-02-21 14:54:00  213.0  413.0  164.3 NaN     NaN  2763.0  2330.3   
2014-02-21 14:55:00  164.3  164.3  164.3 NaN     NaN  2142.3  2330.3   
2014-02-21 14:56:00  213.0  213.0  164.3 NaN     NaN  2127.3  2330.3   
2014-02-21 14:57:00  221.1  221.1  164.3 NaN  2330.3  2330.3  2330.3   

                               h  
2014-02-21 14:50:00  2330.300000  
2014-02-21 14:51:00  2394.214286  
2014-02-21 14:52:00  2458.128571  
2014-02-21 14:53:00  2522.042857  
2014-02-21 14:54:00  2585.957143  
2014-02-21 14:55:00  2649.871429  
2014-02-21 14:56:00  2713.785714  
2014-02-21 14:57:00  2777.700000 

f) 间隔由开始时 4 分钟的 NaN 组成,它们应替换为该值 2763.0(即及时向后推断)

g) 间隔超过 5 分钟,但仍然可以推断出

h) 间隔超过 5 分钟,但仍会插入间隔。

我理解这些原因,当然我没有指定它不应插入超过 5 分钟的间隔。我知道interpolate 只会及时向前推断,但我希望它也能及时向后推断。是否有任何已知的方法可以用来解决我的问题,而无需重新发明轮子?

编辑: 方法data.interpolate 接受输入参数limit,它定义了被插值替换的连续NaN 的最大数量。但这仍然会插入到极限,但在这种情况下,我想继续使用所有 NaN。

【问题讨论】:

  • 插值后可以使用bfill()向后填充方法吗?
  • @JohnGalt bfill() 在插值后看起来不错。遗憾的是,它会在超过 5 分钟的间隙中遇到同样的问题,因为它会简单地填充它们。
  • 那你期待什么?如果它只有一个值,则不能插值。
  • @JohnGalt 我希望它可以推断,但前提是间隔不超过 5 分钟,即 5 个连续的 NaN。 IE。 bfill 修复了案例 f,但没有修复案例 g。
  • @JohnE 是的,我刚刚添加了我对limit 的认识。它几乎完成了工作,因为它在第 5 个值之后停止插值。但如果间隙长于 5 的限制,则根本不应该进行插值。

标签: python pandas interpolation extrapolation


【解决方案1】:

所以这里有一个应该可以解决问题的面具。只需interpolate,然后应用掩码将适当的值重置为 NaN。老实说,这比我意识到的要多一些工作,因为我必须遍历每一列,但是如果没有我提供一些像“ones”这样的虚拟列,groupby 就无法正常工作。

无论如何,如果有什么不清楚的地方,我可以解释,但实际上只有几行有点难以理解。有关df['new'] 行上的技巧的更多解释,请参阅here,或者打印出单独的行以更好地了解发生了什么。

mask = data.copy()
for i in list('abcdefgh'):
    df = pd.DataFrame( data[i] )
    df['new'] = ((df.notnull() != df.shift().notnull()).cumsum())
    df['ones'] = 1
    mask[i] = (df.groupby('new')['ones'].transform('count') < 5) | data[i].notnull()

In [7]: data
Out[7]: 
                         a      b      c   d       e       f       g       h
2014-02-21 14:50:00  123.5  433.5  123.5 NaN     NaN     NaN  2330.3  2330.3
2014-02-21 14:51:00    NaN  523.2  132.3 NaN     NaN     NaN     NaN     NaN
2014-02-21 14:52:00  136.3  536.3  136.3 NaN     NaN     NaN     NaN     NaN
2014-02-21 14:53:00  164.3  464.3  164.3 NaN     NaN     NaN     NaN     NaN
2014-02-21 14:54:00  213.0  413.0    NaN NaN     NaN  2763.0     NaN     NaN
2014-02-21 14:55:00  164.3  164.3    NaN NaN     NaN  2142.3     NaN     NaN
2014-02-21 14:56:00  213.0  213.0    NaN NaN     NaN  2127.3     NaN     NaN
2014-02-21 14:57:00  221.1  221.1    NaN NaN  2330.3  2330.3     NaN  2777.7

In [8]: mask
Out[8]: 
                        a     b     c      d      e     f      g      h
2014-02-21 14:50:00  True  True  True  False  False  True   True   True
2014-02-21 14:51:00  True  True  True  False  False  True  False  False
2014-02-21 14:52:00  True  True  True  False  False  True  False  False
2014-02-21 14:53:00  True  True  True  False  False  True  False  False
2014-02-21 14:54:00  True  True  True  False  False  True  False  False
2014-02-21 14:55:00  True  True  True  False  False  True  False  False
2014-02-21 14:56:00  True  True  True  False  False  True  False  False
2014-02-21 14:57:00  True  True  True  False   True  True  False   True

如果您在外推方面不做任何更花哨的事情,从那里很容易:

In [9]: data.interpolate().bfill()[mask]
Out[9]: 
                         a      b      c   d       e       f       g       h
2014-02-21 14:50:00  123.5  433.5  123.5 NaN     NaN  2763.0  2330.3  2330.3
2014-02-21 14:51:00  129.9  523.2  132.3 NaN     NaN  2763.0     NaN     NaN
2014-02-21 14:52:00  136.3  536.3  136.3 NaN     NaN  2763.0     NaN     NaN
2014-02-21 14:53:00  164.3  464.3  164.3 NaN     NaN  2763.0     NaN     NaN
2014-02-21 14:54:00  213.0  413.0  164.3 NaN     NaN  2763.0     NaN     NaN
2014-02-21 14:55:00  164.3  164.3  164.3 NaN     NaN  2142.3     NaN     NaN
2014-02-21 14:56:00  213.0  213.0  164.3 NaN     NaN  2127.3     NaN     NaN
2014-02-21 14:57:00  221.1  221.1  164.3 NaN  2330.3  2330.3     NaN  2777.7

编辑添加:通过将一些内容移出循环,这是一种更快(在此示例数据上大约是 2 倍)且稍微简单的方法:

mask = data.copy()
grp = ((mask.notnull() != mask.shift().notnull()).cumsum())
grp['ones'] = 1
for i in list('abcdefgh'):
    mask[i] = (grp.groupby(i)['ones'].transform('count') < 5) | data[i].notnull()

【讨论】:

  • 嘿 JohnE,对所有值进行插值/bfill 非常好,只需在先前定义的位置使用结果即可。运行时是一个问题,有些情况大约有 1000 列。我会检查运行时,如果需要的话,我是否可以通过一些深思熟虑更有效地创建掩码。
  • 仅供参考:我接受了你的想法,添加案例,因为一些 NaN 的情况应该被插值,这是非常罕见的:if df.isnull().all() - 掩码都是 False,elif df.isnull().any() 做 groubpy -您提供的东西和else 将掩码设置为全部True。此外,当不运行循环但将掩码创建放入方法中并用它提供data.apply() 时,它会更快一些。
  • 好吧,老实说,当我让代码能够正常工作时,我真的很高兴!我没有花时间优化它,所以我相信你可以改进它。我只是添加了一个编辑,它仅通过将一些代码移到循环之外来提供帮助,但听起来您已经有了一些提高性能的好主意。 FWIW,我怀疑 groupby/transform 现在占用了大部分时间,但没有看到明显的解决方法。
  • 谢谢!我认为你有“
【解决方案2】:

在找到上面的答案之前,我必须解决类似的问题并提出基于numpy 的解决方案。因为我的代码大约是。快十倍,我在这里提供它是为了将来对某人有用。它在系列末尾处理 NaN 的方式与 the solution of JohnE above 不同。如果一个系列以 NaN 结尾,它会将最后一个间隙标记为无效。

代码如下:


def bfill_nan(arr):
    """ Backward-fill NaNs """
    mask = np.isnan(arr)
    idx = np.where(~mask, np.arange(mask.shape[0]), mask.shape[0]-1)
    idx = np.minimum.accumulate(idx[::-1], axis=0)[::-1]
    out = arr[idx]
    return out

def calc_mask(arr, maxgap):
    """ Mask NaN gaps longer than `maxgap` """
    isnan = np.isnan(arr)
    cumsum = np.cumsum(isnan).astype('float')
    diff = np.zeros_like(arr)
    diff[~isnan] = np.diff(cumsum[~isnan], prepend=0)
    diff[isnan] = np.nan
    diff = bfill_nan(diff)
    return (diff < maxgap) | ~isnan


mask = data.copy()

for column_name in data:
    x = data[column_name].values
    mask[column_name] = calc_mask(x, 5)

print('data:')
print(data)

print('\nmask:')
print mask

输出:

data:
                         a      b      c   d       e       f       g       h
2014-02-21 14:50:00  123.5  433.5  123.5 NaN     NaN     NaN  2330.3  2330.3
2014-02-21 14:51:00    NaN  523.2  132.3 NaN     NaN     NaN     NaN     NaN
2014-02-21 14:52:00  136.3  536.3  136.3 NaN     NaN     NaN     NaN     NaN
2014-02-21 14:53:00  164.3  464.3  164.3 NaN     NaN     NaN     NaN     NaN
2014-02-21 14:54:00  213.0  413.0    NaN NaN     NaN  2763.0     NaN     NaN
2014-02-21 14:55:00  164.3  164.3    NaN NaN     NaN  2142.3     NaN     NaN
2014-02-21 14:56:00  213.0  213.0    NaN NaN     NaN  2127.3     NaN     NaN
2014-02-21 14:57:00  221.1  221.1    NaN NaN  2330.3  2330.3     NaN  2777.7

mask:
                        a     b      c      d      e     f      g      h
2014-02-21 14:50:00  True  True   True  False  False  True   True   True
2014-02-21 14:51:00  True  True   True  False  False  True  False  False
2014-02-21 14:52:00  True  True   True  False  False  True  False  False
2014-02-21 14:53:00  True  True   True  False  False  True  False  False
2014-02-21 14:54:00  True  True  False  False  False  True  False  False
2014-02-21 14:55:00  True  True  False  False  False  True  False  False
2014-02-21 14:56:00  True  True  False  False  False  True  False  False
2014-02-21 14:57:00  True  True  False  False   True  True  False   True

【讨论】:

    【解决方案3】:

    根据interpolate documentation limit_area 下面使用的是 0.23.0 版中的新内容。我不确定这是否是 e 和 g 列的所需输出,因为您没有详细指定所需的输出。

    import numpy as np
    import pandas as pd
    from datetime import datetime
    from datetime import timedelta
    
    start = datetime(2014,2,21,14,50)
    df = data = pd.DataFrame(index=[start + timedelta(minutes=1*x) for x in range(0, 8)],
                             data={'a': [123.5, np.NaN, 136.3, 164.3, 213.0, 164.3, 213.0, 221.1],
                                   'b': [433.5, 523.2, 536.3, 464.3, 413.0, 164.3, 213.0, 221.1],
                                   'c': [123.5, 132.3, 136.3, 164.3] + [np.NaN]*4,
                                   'd': [np.NaN]*8,
                                   'e': [np.NaN]*7 + [2330.3],
                                   'f': [np.NaN]*4 + [2763.0, 2142.3, 2127.3, 2330.3],
                                   'g': [2330.3] + [np.NaN]*7,
                                   'h': [2330.3] + [np.NaN]*6 + [2777.7]})
    
    df.interpolate(
        limit=5,
        inplace=True,
        limit_direction='both',
        limit_area='outside',
        )
    
    print(df)
    

    输出:

                             a      b      c   d       e       f       g       h
    2014-02-21 14:50:00  123.5  433.5  123.5 NaN     NaN  2763.0  2330.3  2330.3
    2014-02-21 14:51:00    NaN  523.2  132.3 NaN     NaN  2763.0  2330.3     NaN
    2014-02-21 14:52:00  136.3  536.3  136.3 NaN  2330.3  2763.0  2330.3     NaN
    2014-02-21 14:53:00  164.3  464.3  164.3 NaN  2330.3  2763.0  2330.3     NaN
    2014-02-21 14:54:00  213.0  413.0  164.3 NaN  2330.3  2763.0  2330.3     NaN
    2014-02-21 14:55:00  164.3  164.3  164.3 NaN  2330.3  2142.3  2330.3     NaN
    2014-02-21 14:56:00  213.0  213.0  164.3 NaN  2330.3  2127.3     NaN     NaN
    2014-02-21 14:57:00  221.1  221.1  164.3 NaN  2330.3  2330.3     NaN  2777.7
    

    【讨论】:

      【解决方案4】:

      我继续将@JohnE 的solution 改编成一个函数(进行了一些调整/改进)。我使用的是 Python 3.8,我相信 3.9 的类型提示已更改,因此您可能需要适应。

      from typing import Union
      
      def fill_with_hard_limit(
              df_or_series: Union[pd.DataFrame, pd.Series], limit: int,
              fill_method='interpolate',
              **fill_method_kwargs) -> Union[pd.DataFrame, pd.Series]:
          """The fill methods from Pandas such as ``interpolate`` or ``bfill``
          will fill ``limit`` number of NaNs, even if the total number of
          consecutive NaNs is larger than ``limit``. This function instead
          does not fill any data when the number of consecutive NaNs
          is > ``limit``.
      
          Adapted from: https://stackoverflow.com/a/30538371/11052174
      
          :param df_or_series: DataFrame or Series to perform interpolation
              on.
          :param limit: Maximum number of consecutive NaNs to allow. Any
              occurrences of more consecutive NaNs than ``limit`` will have no
              filling performed.
          :param fill_method: Filling method to use, e.g. 'interpolate',
              'bfill', etc.
          :param fill_method_kwargs: Keyword arguments to pass to the
              fill_method, in addition to the given limit.
      
          :returns: A filled version of the given df_or_series according
              to the given inputs.
          """
      
          # Keep things simple, ensure we have a DataFrame.
          try:
              df = df_or_series.to_frame()
          except AttributeError:
              df = df_or_series
      
          # Initialize our mask.
          mask = pd.DataFrame(True, index=df.index, columns=df.columns)
      
          # Get cumulative sums of consecutive NaNs.
          grp = (df.notnull() != df.shift().notnull()).cumsum()
      
          # Add columns of ones.
          grp['ones'] = 1
      
          # Loop through columns and update the mask.
          for col in df.columns:
      
              mask.loc[:, col] = (
                      (grp.groupby(col)['ones'].transform('count') <= limit)
                      | df[col].notnull()
              )
      
          # Now, interpolate and use the mask to create NaNs for the larger
          # gaps.
          method = getattr(df, fill_method)
          out = method(limit=limit, **fill_method_kwargs)[mask]
      
          # Be nice to the caller and return a Series if that's what they
          # provided.
          if isinstance(df_or_series, pd.Series):
              # Return a Series.
              return out.loc[:, out.columns[0]]
      
          return out
      

      用法:

      >>> data_filled = fill_with_hard_limit(data, 5)
      >>> data_filled
                               a      b      c   d       e       f       g       h
      2014-02-21 14:50:00  123.5  433.5  123.5 NaN     NaN     NaN  2330.3  2330.3
      2014-02-21 14:51:00  129.9  523.2  132.3 NaN     NaN     NaN     NaN     NaN
      2014-02-21 14:52:00  136.3  536.3  136.3 NaN     NaN     NaN     NaN     NaN
      2014-02-21 14:53:00  164.3  464.3  164.3 NaN     NaN     NaN     NaN     NaN
      2014-02-21 14:54:00  213.0  413.0  164.3 NaN     NaN  2763.0     NaN     NaN
      2014-02-21 14:55:00  164.3  164.3  164.3 NaN     NaN  2142.3     NaN     NaN
      2014-02-21 14:56:00  213.0  213.0  164.3 NaN     NaN  2127.3     NaN     NaN
      2014-02-21 14:57:00  221.1  221.1  164.3 NaN  2330.3  2330.3     NaN  2777.7
      

      【讨论】:

        猜你喜欢
        • 2019-01-13
        • 1970-01-01
        • 2022-06-10
        • 1970-01-01
        • 2021-10-06
        • 2019-05-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多