Python中的滑动平均值

13

有没有一种Pythonic的方法来构建一个包含某个函数的移动平均值的列表?

在阅读了关于火星人、黑匣子和柯西分布的有趣小文章后,我想自己计算柯西分布的移动平均值:

import math 
import random

def cauchy(location, scale):
    p = 0.0
    while p == 0.0:
        p = random.random()
    return location + scale*math.tan(math.pi*(p - 0.5))

# is this next block of code a good way to populate running_avg?
sum = 0
count = 0
max = 10
running_avg = []
while count < max:
    num = cauchy(3,1)
    sum += num
    count += 1
    running_avg.append(sum/count)

print running_avg     # or do something else with it, besides printing

我认为这种方法可行,但我想知道是否有更优雅的方法来构建running_avg列表,而不是使用循环和计数器(例如列表推导)。
有一些相关问题,但它们涉及更复杂的问题(小窗口大小,指数加权)或不特定于Python:

1
我很好奇为什么这被标记为移动平均,因为你实际上对一个运行平均(增长窗口)感兴趣,而不是移动平均(固定窗口)?如果你想做一个移动平均,可以在http://docs.python.org/library/collections.html#deque-recipes中找到一个不错的方法 :) - Jeffrey Harris
@Jeffrey:你说得对--这主要是为了帮助那些不知道正确短语的人。但是,有了你发布的有用链接,也许把移动平均标签留在那里是有意义的? :) - Nate Kohl
@JeffreyHarris,移动平均和滑动平均不是同一回事吗? - Prof. Falken
3个回答

15
你可以编写一个生成器:
def running_average():
  sum = 0
  count = 0
  while True:
    sum += cauchy(3,1)
    count += 1
    yield sum/count

或者,如果你有一个生成 Cauchy 数的生成器和一个用于累计求和的实用函数,你可以使用一个简洁的生成器表达式:

# Cauchy numbers generator
def cauchy_numbers():
  while True:
    yield cauchy(3,1)

# running sum utility function
def running_sum(iterable):
  sum = 0
  for x in iterable:
    sum += x
    yield sum

# Running averages generator expression (** the neat part **)
running_avgs = (sum/(i+1) for (i,sum) in enumerate(running_sum(cauchy_numbers())))

# goes on forever
for avg in running_avgs:
  print avg

# alternatively, take just the first 10
import itertools
for avg in itertools.islice(running_avgs, 10):
  print avg

太好了。只是为了明确,你的第一个例子会像这样使用:running_avg = [running_average().next() for i in range(10)]吗? - Nate Kohl
是的,你可以像那样使用它,也可以像第二个例子中那样使用itertools.islice:for avg in itertools.islice(running_average(), 10): - orip
这个解决方案中生成器的使用很巧妙,但是由于你同时使用了很多生成器,所以它似乎比一个更简单的列表推导式解决方案慢了大约两倍,尽管这可能只是因为你的解决方案能够处理生成器,而列表推导式需要一个列表。 - Bryan McLemore
我计时了更简单的LC解决方案,包括生成Cauchy数(这就是这两个生成器解决方案所做的),但得到的速度比生成器慢 - 请参见我的下面评论。 - orip
我明白了,你说的时间差确实是因为我从计时中删除了那部分内容所导致的,但我没有想到它会影响你的。不过,请查看我刚刚发布的新解决方案。 - Bryan McLemore

6

你可以使用协程。它们类似于生成器,但允许您发送值。协程是在Python 2.5中添加的,因此在此之前的版本中无法使用。

def running_average():
    sum = 0.0
    count = 0
    value = yield(float('nan'))
    while True:
        sum += value
        count += 1
        value = yield(sum/count)

ravg = running_average()
next(ravg)   # advance the corutine to the first yield

for i in xrange(10):
    avg = ravg.send(cauchy(3,1))
    print 'Running average: %.6f' % (avg,)

作为列表推导式:
ravg = running_average()
next(ravg)
ravg_list = [ravg.send(cauchy(3,1)) for i in xrange(10)]

编辑:

  • 使用next()函数代替it.next()方法,以便在Python 3中也可以正常工作。 next()函数也已被向后移植到Python 2.6+。
    在Python 2.5中,您可以将调用替换为it.next(),或者自己定义一个next函数。
    (感谢Adam Parkin)

哇,真的很灵活。我从没想过可以使用yield将东西“送入”函数。 - Nate Kohl
请注意,在Python 3中,下一个语法有点不同,请使用next(ravg)而不是ravg.next() - Adam Parkin

4
我为你准备了两个可能的解决方案。它们都是通用的运行平均函数,适用于任何数字列表(也可以适用于任何可迭代对象)。
基于生成器的:
nums = [cauchy(3,1) for x in xrange(10)]

def running_avg(numbers):
    for count in xrange(1, len(nums)+1):
        yield sum(numbers[:count])/count

print list(running_avg(nums))

列表推导式(实际上与先前的代码完全相同):
nums = [cauchy(3,1) for x in xrange(10)]

print [sum(nums[:count])/count for count in xrange(1, len(nums)+1)]

生成器兼容的基于生成器的:

Generator-compatabile Generator based:

编辑:我刚刚测试了一下,看看是否可以轻松地使我的解决方案与生成器兼容,并且它的性能如何。这就是我想出来的。

def running_avg(numbers):
    sum = 0
    for count, number in enumerate(numbers):
        sum += number
        yield sum/(count+1)

请看下面的性能统计数据,非常值得。 性能特征: 编辑:我还决定测试Orip有趣的多个生成器使用方式,以了解其对性能的影响。
使用timeit和以下代码(1,000,000次迭代3次):
print "Generator based:", ', '.join(str(x) for x in Timer('list(running_avg(nums))', 'from __main__ import nums, running_avg').repeat())
print "LC based:", ', '.join(str(x) for x in Timer('[sum(nums[:count])/count for count in xrange(1, len(nums)+1)]', 'from __main__ import nums').repeat())
print "Orip's:", ', '.join(str(x) for x in Timer('list(itertools.islice(running_avgs, 10))', 'from __main__ import itertools, running_avgs').repeat())

print "Generator-compatabile Generator based:", ', '.join(str(x) for x in Timer('list(running_avg(nums))', 'from __main__ import nums, running_avg').repeat())

我得到了以下的结果:
Generator based: 17.653908968, 17.8027219772, 18.0342400074
LC based: 14.3925321102, 14.4613749981, 14.4277560711
Orip's: 30.8035550117, 30.3142540455, 30.5146529675

Generator-compatabile Generator based: 3.55352187157, 3.54164409637, 3.59098005295

请参见代码的注释:
Orip's genEx based: 4.31488609314, 4.29926609993, 4.30518198013 

结果以秒为单位,展示了新的兼容生成器方法相比于原来的方法持续更快。尽管您的结果可能会有所不同。我预期原始生成器和新生成器之间的巨大差异在于它们对总数的计算方式不同。

有意思。就性能而言,这些方法和orip的第一个生成器示例相比如何? - Nate Kohl
你是否像上面的解决方案一样每次重新生成柯西数?如果没有,那么你正在计时数字的生成以及运行平均值。 - orip
没有Orip我的代码不会每次计算数字所需的时间,除非我测试你的代码,所以这肯定会影响你的速度慢2倍。尽管我刚刚发布的新解决方案的统计数据显示了显着的改进。 - Bryan McLemore
为了与生成器表达式解决方案进行良好的比较,请运行 list((sum/(i+1) for (i,sum) in enumerate(running_sum(nums))))(使用 nums 而不是 cauchy_numbers())。 - orip
Orip的基于genEx的:4.31488609314,4.29926609993,4.30518198013。 - Bryan McLemore
显示剩余4条评论

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