You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python Itertools蒙特卡洛模拟结果异常问题求助

slothrop评论后编辑:

看起来takewhile会消耗迭代器中最后一个被检查的元素,这意味着无法判断下一个元素是否也满足条件,导致计算出现问题。在无限随机实现的迭代器上使用takewhile似乎不是好主意(如果需要保留这些实现的话)。


问题

我编写了两次蒙特卡洛模拟,一次用itertools实现,一次用传统方式。前者出现异常行为,结果不正确,而后者运行正常,想请教原因,抱歉附上了较多代码。

猜想

我猜测takewhile函数结合随机变量(RVs)时存在异常行为。注意simulate_freqs_iter函数中有一些被注释的写法(如zip(takewhile(lambda x: x < t, honest_arrival_times), attack_arrival_times)),它们的结果差异很大,我无法解释。

说明:我知道n=1e5太小,原本计划用n=1e8或1e9,但不会让您运行20小时。n=1e5只需约10秒,且结果差异已很明显!

非常感谢您的解答,我非常喜欢itertools(即便这个实现既不快也无其他效率优势)。

代码

from random import expovariate, seed
from itertools import accumulate, takewhile, count, islice, chain

seed("31.05.2023")

def simulate_freqs_iter(k:int, mu:float, lam:float, n:int, conditional_on_finite:bool=True, verbose:bool=False) -> dict:
    """Simulates n monte-carlo runs of a slow poisson process catching up with a fast one
    returns a dictionary containing the empirical cdf for t={10, 11, ..., 50}"""
    abs_freqs = {t: 0 for t in range(10, 51)}
    for i in range(n):
        # Waiting times for each poisson process
        honest_arrival_times = accumulate(((expovariate(mu)) for _ in count(start=0, step=1)))
        attack_arrival_times = islice( # attack_arrival_times is k blocks behind, but only needs to catch up, not surpass
            accumulate(((expovariate((lam))) for _ in count(start=0, step=1))), k-1, None)

        # Optional printing function
        if verbose and (i%50000)==0:
            first_10_honest = list(islice(honest_arrival_times, 10))
            first_10_attack = list(islice(attack_arrival_times, 10))
            print(list(zip(first_10_honest, first_10_attack)))
            honest_arrival_times = chain(first_10_honest, honest_arrival_times)
            attack_arrival_times = chain(first_10_attack, attack_arrival_times)

        # Simulates poisson waiting times and counts number of hits where 1 catches up with 2
        for t in abs_freqs.keys():
            # Every time N_2 surpasses N_1 we have a hit
            hits = list((t, honest, attack) for honest, attack in 
                    # Take all jump times of N_1 and see if N_2 was there before N_1 within t
                        # takewhile(lambda x: min(x[0], x[1]) < t, zip(honest_arrival_times, attack_arrival_times))
                        # zip(takewhile(lambda x: x < t, honest_arrival_times), attack_arrival_times)
                        zip(honest_arrival_times, takewhile(lambda x: x < t, attack_arrival_times))
                        # zip(takewhile(lambda x: x < t, honest_arrival_times), takewhile(lambda x: x < t, attack_arrival_times))
                        if honest > attack)
            if hits:
                if verbose: print(hits[0])
                abs_freqs[t]+=1
                # Don't continue this path once found a hit
                break
    
    
    # Condition on the probability that it ever catches up
    prob_finite = min(((lam/mu)**k), 1) if conditional_on_finite else 1
    
    # Accumulate relative frequencies to get cdf    
    results = {key:((value/n) / prob_finite) for key, value in dict(
        accumulate(abs_freqs.items(), lambda prev,curr: (curr[0], (curr[1]+prev[1])))).items()}
    return results


def simulate_freqs_orthodox(k:int, mu:float, lam:float, n:int, conditional_on_finite:bool=True, verbose:bool=False) -> dict:
    """Simulates n monte-carlo runs of a slow poisson process catching up with a fast one
    returns a dictionary containing the empirical cdf for t={10, 11, ..., 50}"""
    abs_freqs = {t: 0 for t in range(10, 51)}

    # We run the monte carlo n times
    for _ in range(n):
        # Initialise first block arrival
        next_honest_arrival = expovariate(mu)
        next_attack_arrival = expovariate(lam)

        # the attacker chain is behind k blocks
        for _ in range(k-1):
            next_attack_arrival += expovariate(lam)

        for t in abs_freqs.keys():
            while next_attack_arrival < t:
                # count when a double spend attack is successful
                if next_honest_arrival > next_attack_arrival:
                    abs_freqs[t] += 1
                    if verbose: print(next_honest_arrival, next_attack_arrival)
                    # stop searching for all t on this path once found a hit
                    break

                # Add another block to each chain
                next_honest_arrival += expovariate(mu)
                next_attack_arrival += expovariate(lam)
            else:
                # If while-loop terminates without a hit, continue with greater t
                continue
            # Terminate search for this path after a successful hit
            break
    
    # Condition on the probability that it ever catches up
    prob_finite = min(((lam/mu)**k), 1) if conditional_on_finite else 1
    
    # Accumulate relative frequencies to get cdf    
    results = {key:((value/n) / prob_finite) for key, value in dict(
        accumulate(abs_freqs.items(), lambda prev,curr: (curr[0], (curr[1]+prev[1])))).items()}
    return results

# Monte Carlo simulation of probabilities for T_k
k=10
mu=7/10
lam=3/10
n=int(1e5)

print(simulate_freqs_iter(k=k, mu=mu, lam=lam, n=n, conditional_on_finite=True, verbose=False))
print(simulate_freqs_orthodox(k=k, mu=mu, lam=lam, n=n, conditional_on_finite=True, verbose=False))

解答

核心问题出在takewhile对迭代器的消耗方式上,和随机变量本身无关,是itertools版本的逻辑和正统版本完全不一致:

  1. 迭代器的一次性消耗特性:honest_arrival_times和attack_arrival_times都是无限迭代器,每次取元素都会消耗掉该元素,无法复用。你在循环每个t时,每次用takewhile截取迭代器,会导致前一个t处理时已经消耗了部分元素,后一个t只能拿到剩余元素,完全违背了正统版本中“每个t都从初始状态开始模拟”的逻辑。

  2. takewhile的截断逻辑:以你当前使用的zip(honest_arrival_times, takewhile(lambda x: x < t, attack_arrival_times))为例,takewhile会一直取attack_arrival_times的元素直到第一个不满足x < t的元素,并且这个不满足条件的元素也会被消耗掉,不会放回迭代器。下一次循环更大的t时,attack_arrival_times已经跳过了这个元素,模拟状态直接断片,和正统版本逐步累积到t的逻辑完全不符。

  3. 版本逻辑差异:正统版本中,每个蒙特卡洛样本从初始状态开始,对每个t逐步模拟到该时间点;而itertools版本中,每个样本的迭代器是共享的,处理完t=10后,迭代器已经走到t=10之后的状态,处理t=11时是从中间状态继续,而非重新从初始状态模拟到t=11,这直接导致统计结果错误。

修复建议

如果想保留itertools的写法,需要为每个t重新生成独立的迭代器(但无限迭代器无法保存状态)。更合理的做法是,对每个蒙特卡洛样本,先生成到足够大的t的所有到达时间,再针对每个t统计:

# 在simulate_freqs_iter的每个样本循环内
honest_times = []
attack_times = []
# 先生成到t=50之后的足够多的点
current_honest = 0.0
current_attack = 0.0
for _ in range(k-1):
    current_attack += expovariate(lam)
while current_honest < 50 or current_attack < 50:
    current_honest += expovariate(mu)
    honest_times.append(current_honest)
    current_attack += expovariate(lam)
    attack_times.append(current_attack)

# 然后对每个t统计
for t in abs_freqs.keys():
    hits = [(t, h, a) for h, a in zip(honest_times, attack_times) if a < t and h > a]
    if hits:
        abs_freqs[t] +=1
        break

这样既保留了迭代器的思想,又保证每个t的统计基于完整的初始状态模拟。


内容的提问来源于stack exchange,提问作者WonderfulWonder

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 15:40:04