Python/pandas:map/lambda 中的公式计算 bug?

3
我在使用Python(2.7.6.2)/Pandas(0.13和0.18)时遇到了一个奇怪的问题,当我在DataFrame上应用公式时,使用map / lambda与直接应用数字的结果似乎会有所不同。对于我来说,这似乎是一个错误,并且我很想知道原因以及如何避免这种问题。...
现在,我已经准备好了一个案例,可以重现此问题,这使问题有些清晰:
data15min = [ 5.4753, 5.4863, 5.2497, 5.057, 5.0917, 5.3467, 5.7513, 5.6, 5.342 ]
index     = pd.date_range("2015-10-17 18:00:00", periods=9, freq='15T')
columns = ['v03']

df15 = pd.DataFrame(data15min, index=index, columns=columns)
df_h = df15.rolling(min_periods=4,window=4,center=False).mean()
df_m = df_h['v03'].map(lambda x: np.nan if np.isnan(x) else int(x*100.))

df_h的最后一个值是错误计算出来的。这个值本身看起来很好(5.3467、5.7513、5.6、5.342的平均值恰好为5.51):

In [99]: df_h
Out[99]: 
v03
2015-10-17 18:00:00 NaN
2015-10-17 18:15:00 NaN
2015-10-17 18:30:00 NaN
2015-10-17 18:45:00 5.317075
2015-10-17 19:00:00 5.221175
2015-10-17 19:15:00 5.186275
2015-10-17 19:30:00 5.311675
2015-10-17 19:45:00 5.447425
2015-10-17 20:00:00 5.510000

使用map函数后,我得到的结果是550:

In [100]: df_m
Out[100]: 
2015-10-17 18:00:00      NaN
2015-10-17 18:15:00      NaN
2015-10-17 18:30:00      NaN
2015-10-17 18:45:00    531.0
2015-10-17 19:00:00    522.0
2015-10-17 19:15:00    518.0
2015-10-17 19:30:00    531.0
2015-10-17 19:45:00    544.0
2015-10-17 20:00:00    550.0
Freq: 15T, Name: v03, dtype: float64

我猜测这是由于数字表示不准确,但是当我直接在数字上应用公式时,会得到不同的行为:
In [103]: int(np.mean([5.3467, 5.7513, 5.6, 5.342])*100.)
Out[103]: 551

为了让问题更加混乱,当我使用包含相同相关值的稍短数据框时,使用map也会得到不同结果:
data15min = [  5.3467, 5.7513, 5.6, 5.342 ]
index     = pd.date_range("2015-10-17 19:15:00", periods=4, freq='15T')
columns = ['v03']

df15 = pd.DataFrame(data15min, index=index, columns=columns)
df_h = df15.rolling(min_periods=4,window=4,center=False).mean()
df_m = df_h['v03'].map(lambda x: np.nan if np.isnan(x) else int(x*100.))

In [104]: df_m
Out[104]: 
2015-10-17 19:15:00 NaN
2015-10-17 19:30:00 NaN
2015-10-17 19:45:00 NaN
2015-10-17 20:00:00 551.0
Freq: 15T, Name: v03, dtype: float64

我很困惑,也很担心会得到错误的结果。如果这与内部数字表示不准确有关(如果这个问题在展示的情况下有不同行为,那就令人惊讶了),我真的很想知道如何避免从中得到错误的结果。


我无法重现问题(因为我没有你的数据)。另一方面,你的pandas版本相当老。 - Stop harming Monica
我修改了描述,以便每个人都能够轻松重现此问题。我非常好奇是否只有我从Python / Pandas得到错误结果,或者如何避免这种情况。 - emo-martin
1个回答

1
这是一个浮点精度问题。在df_h ['v03']中的最后一个值实际上比5.51略小:
x = df_h['v03'].iloc[-1]
print repr(x)
print repr(x * 100.)
print int(x * 100.)

会打印出以下内容:
5.5099999999999989
550.99999999999989
550

当然,这是错误的,因为您写下的数字的实际平均值是5.51,但这就是浮点算术的工作原理。
如果我没记错的话,您试图将前三个数字用作字典中的键。仅取值的100倍的整数部分是一种非常脆弱的方法,因为很小的误差可能会改变结果。更健壮的方法是先四舍五入到3位小数:
df_h['v03'].round(3).map(lambda x: np.nan if np.isnan(x) else int(x*100.))

2015-10-17 18:00:00      NaN
2015-10-17 18:15:00      NaN
2015-10-17 18:30:00      NaN
2015-10-17 18:45:00    531.0
2015-10-17 19:00:00    522.0
2015-10-17 19:15:00    518.0
2015-10-17 19:30:00    531.0
2015-10-17 19:45:00    544.0
2015-10-17 20:00:00    551.0
Freq: 15T, Name: v03, dtype: float64

我猜在某些边缘情况下也会失败。
关于所谓的不确定行为,有几种算法可以计算平均值,你不应该假设正在使用numpy.mean()。实际上,在你的情况下似乎并没有使用它。
print(x == np.mean([5.3467, 5.7513, 5.6, 5.342]))

False

但是你可以告诉Pandas使用它:
df_h = df15.rolling(min_periods=4, window=4, center=False).apply(np.mean)
x = df_h['v03'].iloc[-1]
print(repr(x))
print(x == np.mean([5.3467, 5.7513, 5.6, 5.342]))

5.5099999999999998
True

然而,Rolling.mean() 的结果实际上存在不一致性:

for i in range(6):
    df_h = df15[i:].rolling(min_periods=4, window=4, center=False).mean()
    x = df_h['v03'].iloc[-1]
    print(repr(x))

5.5099999999999989
5.5099999999999989
5.5099999999999989
5.5099999999999989
5.5100000000000007
5.5099999999999998

这种情况不会发生,如果您使用numpy.mean()
for i in range(6):
    df_h = df15[i:].rolling(min_periods=4, window=4, center=False).apply(np.mean)
    x = df_h['v03'].iloc[-1]
    print(repr(x))

5.5099999999999998
5.5099999999999998
5.5099999999999998
5.5099999999999998
5.5099999999999998
5.5099999999999998

我猜测Rolling.mean()使用了一些优化(可能是从一个窗口重复计算到下一个窗口),这引入了更多的舍入误差,并且不适用于.apply()。它实际上比应用numpy版本要快得多:
def test1(s):
    return s.rolling(min_periods=4, window=4, center=False).mean()

def test2(s):
    return s.rolling(min_periods=4, window=4, center=False).apply(np.mean)

s = pd.Series(np.random.randn(10000))

%timeit test1(s)

1000 loops, best of 3: 316 µs per loop

%timeit test2(s)

10 loops, best of 3: 84.9 ms per loop

这可能是使用.apply()的开销导致的。我并不太了解它的内部情况。
关于将浮点数(或由浮点数派生的值)用作查找键,请尽可能避免。测试它们的相等性容易出错。
如果你真的需要它,你可以四舍五入到最低的小数位数,以便区分不同的数字(在你的情况下是2个小数位数),并使用四舍五入后的值作为键。如果你保持误差很小,就不应该有虚假匹配/不匹配。
请记住,还有几个round函数,特别是math.round()在python2和python3中的行为不同。我认为这不会影响numpy或pandas中的round(),但无论如何,请确保在创建键和查找键时采用相同的舍入方式。

感谢您的回复。我认为这可能是由于不准确的数字表示导致的,但我无法理解为什么相同的值和相同的公式会因上下文而产生不同的结果(即我展示的三种情况)。 这就是我一开始无法解释问题的原因。这让我担心,因为在测试我的代码时,我通常假设这种问题是确定性的。 最后,我想要实现的是某种查找表/字典,它作用于浮点值(并且处理速度快)。有没有特定的方法可以做到这一点? - emo-martin
如果您将相同的函数应用于相同的对象,则它是确定性的,但是可能会有几个函数用于相同的目的并返回略有不同的值,请参见编辑。 - Stop harming Monica
谢谢。我没有想到计算简单平均值的两种方法可能会得出略微不同的值,但最终可以理解,因为操作数的应用顺序可能会影响数值误差。但这并不能解释第三个例子,在较短的数据帧上应用相同的映射函数时,为什么结果也可能不同? - emo-martin
@emo-martin 是的,对于给定窗口的滚动平均值会因数据的其余部分而略有不同。我更新了我的答案,并提供了可能的解释,但我并不能确定。 - Stop harming Monica
Goyo,谢谢你。你调查和解决这个问题的方式非常令人印象深刻。现在对我来说似乎非常清楚,以至于我甚至认为我应该自己发现它。我学到了很多,但我也有些敏感:如果这样的问题会对使用Python脚本计算的结果产生明显影响,那么对可能出现的陷阱和副作用有深入的了解似乎确实是必要的。 - emo-martin

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