在Pandas时间序列数据框中使用自定义条件填充缺失数据

3
以下是我的数据框的一部分,其中有许多缺失值。
            A                                       B
S           a       b       c       d       e       a       b       c       d       e
date                                        
2020-10-15  1.0     2.0     NaN     NaN     NaN     10.0    11.0    NaN     NaN     NaN
2020-10-16  NaN     NaN     NaN     NaN     NaN     NaN     NaN     NaN     NaN     NaN
2020-10-17  NaN     NaN     NaN     4.0     NaN     NaN     NaN     NaN     13.0    NaN
2020-10-18  NaN     NaN     NaN     NaN     NaN     NaN     NaN     NaN     NaN     NaN
2020-10-19  NaN     NaN     NaN     NaN     NaN     NaN     NaN     NaN     NaN     NaN
2020-10-20  4.0     6.0     4.0     1.0     9.0     10.0    2.0     13.0    4.0     13.0

我希望使用特定的向后填充条件,替换每一列中的NANs。例如,在(A,a)列中,缺失值出现在16号、17号、18号和19号这几天。下一个值是20日的'4'。我希望这个值(该列中下一个非缺失值)以逐步增加10%的比例分布在包括20日在内的所有日期上。也就是说,对于所有行中所有缺失值,都应采用这种方法填充它们。

我尝试使用bfill()函数,但无法理解如何将所需的公式作为选项纳入其中。

我已经查看了链接Pandas: filling missing values in time series forward using a formula和其他一些关于stackoverflow的链接。这与我的情况有些相似,但在我的情况下,给定列中NANs的数量性质是可变的,并跨越多行。相比较(A,a)列与(A,d)或(B,d)列。鉴于此,我发现很难采用该解决方案解决我的问题。

感谢任何帮助。

2个回答

3
这里有一种完全向量化的方法来完成这个任务。它非常高效快速:在一个 1000 x 1000 的矩阵上只需 130 毫秒。这是一个展示使用 numpy 的一些有趣技巧的好机会。
首先,让我们深入了解要求,具体来说,每个单元格的值到底是什么。
给出的例子是 [nan, nan, nan, nan, 4.0] --> [.66, .72, .79, .87, .96],这被解释为“逐渐增加 10% 的价值”(以这样一种方式,使得总和为“要分配的值”:4.0)。
这是一个等比数列,其公比为 r = 1 + 0.1:[r^1, r^2, r^3, ...],然后归一化为 1。例如:
r = 1.1
a = 4.0
n = 5
q = np.cumprod(np.repeat(r, n))
a * q / q.sum()
# array([0.65518992, 0.72070892, 0.79277981, 0.87205779, 0.95926357])

我们希望进行直接计算(避免调用Python函数和显式循环,这将会非常慢),因此我们需要以闭合形式表达归一化因子q.sum()。这是一个已经被广泛接受的量,其表达式为: 概括而言,我们需要3个量来计算每个单元格的值:
- a:要分配的值 - i:运行的索引(0..n-1) - n:运行长度
然后,该值为v = a * r ** i * (r - 1) / (r ** n - 1)。
以OP示例中的第一列为例,其中输入为:[1, nan, nan, nan, nan, 4],我们希望:
- a = [1, 4, 4, 4, 4, 4] - i = [0, 0, 1, 2, 3, 4] - n = [1, 5, 5, 5, 5, 5]
然后,该值v将为(保留2位小数):[1.,0.66,0.72,0.79,0.87,0.96]。
现在我们开始将这三个量作为numpy数组获取。
a最简单,只需使用df.bfill().values即可。但是对于i和n,我们需要做一些工作,首先将值分配给一个numpy数组:
z = df.values
nrows, ncols = z.shape

对于变量i,我们从累计的NaN数量开始计算,当值不是NaN时重置计数。这受到SO answer“在NumPy中进行累积计数而不进行迭代”的强烈启发。但我们要为一个二维数组执行此操作,并且还想添加第一行为0,并且舍弃最后一行以完全满足我们的需求:
def rcount(z):
    na = np.isnan(z)
    without_reset = na.cumsum(axis=0)
    reset_at = ~na
    overcount = np.maximum.accumulate(without_reset * reset_at)
    result = without_reset - overcount
    return result

i = np.vstack((np.zeros(ncols, dtype=bool), rcount(z)))[:-1]

对于n,我们需要使用numpy的基本原理自己跳一些舞蹈(如果我有时间,我会分解步骤):

runlen = np.diff(np.hstack((-1, np.flatnonzero(~np.isnan(np.vstack((z, np.ones(ncols))).T)))))
n = np.reshape(np.repeat(runlen, runlen), (nrows + 1, ncols), order='F')[:-1]

因此,把所有东西都放在一起:

def spread_bfill(df, r=1.1):
    z = df.values
    nrows, ncols = z.shape

    a = df.bfill().values
    i = np.vstack((np.zeros(ncols, dtype=bool), rcount(z)))[:-1]
    runlen = np.diff(np.hstack((-1, np.flatnonzero(~np.isnan(np.vstack((z, np.ones(ncols))).T)))))
    n = np.reshape(np.repeat(runlen, runlen), (nrows + 1, ncols), order='F')[:-1]
    v = a * r**i * (r - 1) / (r**n - 1)
    return pd.DataFrame(v, columns=df.columns, index=df.index)

在您的示例数据上,我们得到:

>>> spread_bfill(df).round(2)  # round(2) for printing purposes
               A                              B                         
               a     b     c     d     e      a      b     c     d     e
S                                                                       
2020-10-15  1.00  2.00  0.52  1.21  1.17  10.00  11.00  1.68  3.93  1.68
2020-10-16  0.66  0.98  0.57  1.33  1.28   1.64   0.33  1.85  4.32  1.85
2020-10-17  0.72  1.08  0.63  1.46  1.41   1.80   0.36  2.04  4.75  2.04
2020-10-18  0.79  1.19  0.69  0.30  1.55   1.98   0.40  2.24  1.21  2.24
2020-10-19  0.87  1.31  0.76  0.33  1.71   2.18   0.44  2.47  1.33  2.47
2020-10-20  0.96  1.44  0.83  0.37  1.88   2.40   0.48  2.71  1.46  2.71

为了检查,让我们看一下该示例中的每个数量:

>>> a
[[ 1  2  4  4  9 10 11 13 13 13]
 [ 4  6  4  4  9 10  2 13 13 13]
 [ 4  6  4  4  9 10  2 13 13 13]
 [ 4  6  4  1  9 10  2 13  4 13]
 [ 4  6  4  1  9 10  2 13  4 13]
 [ 4  6  4  1  9 10  2 13  4 13]]

>>> i
[[0 0 0 0 0 0 0 0 0 0]
 [0 0 1 1 1 0 0 1 1 1]
 [1 1 2 2 2 1 1 2 2 2]
 [2 2 3 0 3 2 2 3 0 3]
 [3 3 4 1 4 3 3 4 1 4]
 [4 4 5 2 5 4 4 5 2 5]]

>>> n
[[1 1 6 3 6 1 1 6 3 6]
 [5 5 6 3 6 5 5 6 3 6]
 [5 5 6 3 6 5 5 6 3 6]
 [5 5 6 3 6 5 5 6 3 6]
 [5 5 6 3 6 5 5 6 3 6]
 [5 5 6 3 6 5 5 6 3 6]]

以下是最后一个例子,用来说明如果一列以1个或多个NaN结尾的情况会发生什么(它们仍然保持为NaN):
np.random.seed(10)
a = np.random.randint(0, 10, (6, 6)).astype(float)
a *= np.random.choice([1.0, np.nan], a.shape, p=[.3, .7])
df = pd.DataFrame(a)
>>> df
    0    1    2    3    4    5
0 NaN  NaN  NaN  NaN  NaN  0.0
1 NaN  NaN  9.0  NaN  8.0  NaN
2 NaN  NaN  NaN  NaN  NaN  NaN
3 NaN  8.0  4.0  NaN  NaN  NaN
4 NaN  NaN  NaN  6.0  9.0  NaN
5 NaN  NaN  2.0  NaN  7.0  8.0

然后:

>>> spread_bfill(df).round(2)  # round(2) for printing
    0     1     2     3     4     5
0 NaN  1.72  4.29  0.98  3.81  0.00
1 NaN  1.90  4.71  1.08  4.19  1.31
2 NaN  2.09  1.90  1.19  2.72  1.44
3 NaN  2.29  2.10  1.31  2.99  1.59
4 NaN   NaN  0.95  1.44  3.29  1.74
5 NaN   NaN  1.05   NaN  7.00  1.92

速度

a = np.random.randint(0, 10, (1000, 1000)).astype(float)
a *= np.random.choice([1.0, np.nan], a.shape, p=[.3, .7])
df = pd.DataFrame(a)

%timeit spread_bfill(df)
# 130 ms ± 142 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

太棒了。非常感谢详细的解释。幸运的是,数据将始终在最后一行具有值。:):)如果最后一行有NANs,我可能会在“r”处执行ffill。非常感谢你。 - Srinivas
解决方案在我的实际数据框中执行得非常快,瞬间完成了331行和156列。非常感谢。 - Srinivas

1

初始数据:

>>> df
              A                         B
              a    b    c    d    e     a     b     c     d     e
date
2020-10-15  1.0  2.0  NaN  NaN  NaN  10.0  11.0   NaN   NaN   NaN
2020-10-16  NaN  NaN  NaN  NaN  NaN   NaN   NaN   NaN   NaN   NaN
2020-10-17  NaN  NaN  NaN  4.0  NaN   NaN   NaN   NaN  13.0   NaN
2020-10-18  NaN  NaN  NaN  NaN  NaN   NaN   NaN   NaN   NaN   NaN
2020-10-19  NaN  NaN  NaN  NaN  NaN   NaN   NaN   NaN   NaN   NaN
2020-10-20  4.0  6.0  4.0  1.0  9.0  10.0   2.0  13.0   4.0  13.0

定义你的几何序列:

def geomseq(seq):
    q = 1.1
    n = len(seq)
    S = seq.max()
    Uo = S * (1-q) / (1-q**n)
    Un = [Uo * q**i for i in range(0, n)]
    return Un

TL;DR的意思是“太长不看”,是一种简化版的摘要或总结。
>>> df.unstack().groupby(df.unstack().sort_index(ascending=False).notna().cumsum().sort_index()).transform(geomseq).unstack(level=[0, 1])
                   A                                                  B
                   a         b         c         d         e          a          b         c         d         e
date
2020-10-15  1.000000  2.000000  0.518430  1.208459  1.166466  10.000000  11.000000  1.684896  3.927492  1.684896
2020-10-16  0.655190  0.982785  0.570272  1.329305  1.283113   1.637975   0.327595  1.853386  4.320242  1.853386
2020-10-17  0.720709  1.081063  0.627300  1.462236  1.411424   1.801772   0.360354  2.038724  4.752266  2.038724
2020-10-18  0.792780  1.189170  0.690030  0.302115  1.552567   1.981950   0.396390  2.242597  1.208459  2.242597
2020-10-19  0.872058  1.308087  0.759033  0.332326  1.707823   2.180144   0.436029  2.466856  1.329305  2.466856
2020-10-20  0.959264  1.438895  0.834936  0.365559  1.878606   2.398159   0.479632  2.713542  1.462236  2.713542

细节:
将您的数据框转换为系列:
>>> sr = df.unstack()
>>> sr.head(10)
      date
A  a  2020-10-15    1.0
      2020-10-16    NaN  # <= group X (final value: .655)
      2020-10-17    NaN  # <= group X (final value: .720)
      2020-10-18    NaN  # <= group X (final value: .793)
      2020-10-19    NaN  # <= group X (final value: .872)
      2020-10-20    4.0  # <= group X (final value: .960)
   b  2020-10-15    2.0
      2020-10-16    NaN
      2020-10-17    NaN
      2020-10-18    NaN
dtype: float64

现在您可以建立群组:
>>> groups = sr.sort_index(ascending=False).notna().cumsum().sort_index()
>>> groups.head(10)
      date
A  a  2020-10-15    16
      2020-10-16    15  # <= group X15
      2020-10-17    15  # <= group X15
      2020-10-18    15  # <= group X15
      2020-10-19    15  # <= group X15
      2020-10-20    15  # <= group X15
   b  2020-10-15    14
      2020-10-16    13
      2020-10-17    13
      2020-10-18    13
dtype: int64

应用你的等比数列:
>>> sr = sr.groupby(groups).transform(geomseq)
>>> sr.head(10)
      date
A  a  2020-10-15    1.000000
      2020-10-16    0.655190  # <= group X15
      2020-10-17    0.720709  # <= group X15
      2020-10-18    0.792780  # <= group X15
      2020-10-19    0.872058  # <= group X15
      2020-10-20    0.959264  # <= group X15
   b  2020-10-15    2.000000
      2020-10-16    0.982785
      2020-10-17    1.081063
      2020-10-18    1.189170
dtype: float64

最后,根据您的初始 dataframe 重新塑造 series

>>> df = sr.unstack(level=[0, 1])
>>> df
                   A                                                  B
                   a         b         c         d         e          a          b         c         d         e
date
2020-10-15  1.000000  2.000000  0.518430  1.208459  1.166466  10.000000  11.000000  1.684896  3.927492  1.684896
2020-10-16  0.655190  0.982785  0.570272  1.329305  1.283113   1.637975   0.327595  1.853386  4.320242  1.853386
2020-10-17  0.720709  1.081063  0.627300  1.462236  1.411424   1.801772   0.360354  2.038724  4.752266  2.038724
2020-10-18  0.792780  1.189170  0.690030  0.302115  1.552567   1.981950   0.396390  2.242597  1.208459  2.242597
2020-10-19  0.872058  1.308087  0.759033  0.332326  1.707823   2.180144   0.436029  2.466856  1.329305  2.466856
2020-10-20  0.959264  1.438895  0.834936  0.365559  1.878606   2.398159   0.479632  2.713542  1.462236  2.713542

这真是太酷了。太棒了。它需要的代码要少得多。再次感谢您提供的出色解释。这段代码运行速度也很快。然而,虽然这个解决方案在我的实际数据框中有331行和156列时需要几秒钟才能执行,但@Pierre D提供的解决方案是瞬间完成的,超级快。 - Srinivas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接