这是一种完全矢量化的方法。它非常高效且快速:在 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,我们从NaNs 的累积计数开始,当值不是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
为了检查,让我们看一下该示例中的 3 个数量中的每一个:
>>> 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 个或多个 NaNs 结尾(它们仍然是 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)