相同的输入,相同的结果
当你、同事或评审者以后能运行同一个项目并得到相同的数字时,结果就是可复现的。无法重建的回测既不能审计、调试,也不值得信任。结果漂移通常只有几个原因:随机数、库版本不同、数据被改动或被手工编辑、设置散落在代码各处,以及隐藏状态(例如乱序运行的笔记本)。可复现的工作流会消除每一种漂移来源,并记录所用的内容。
import random
# Same seed, same "random" numbers
a = random.Random(42)
b = random.Random(42)
print([round(a.random(), 3) for _ in range(3)]) # [0.639, 0.025, 0.275]
print([round(b.random(), 3) for _ in range(3)]) # [0.639, 0.025, 0.275]
# A different seed gives a different but equally repeatable sequence
print(round(random.Random(7).random(), 3)) # 0.324随机种子,以及它属于哪个生成器
模拟和自助法会用到随机数,而随机数其实是从种子(seed)出发的确定性序列。固定种子,运行就会完全重复。random.seed(42) 设置的是整个程序共享的唯一全局生成器,所以别处的代码再取随机数,就会让你的序列错位。更稳妥的做法是创建你自己的生成器 rng = random.Random(42)(或 numpy.random.default_rng(42)),把它传递下去并从中取数。这样结果只取决于你的种子,你设置种子也不会干扰别人的随机性。
import random
# The global generator is shared: seeding it changes everyone's numbers
random.seed(1)
first = random.random()
random.seed(1)
print(random.random() == first) # True: global seeding repeats...
# ...but a private generator leaves the global one untouched
random.seed(1)
before = random.getstate()
mine = random.Random(99)
mine.random()
print(random.getstate() == before) # True: your draws did not disturb it数据与设置的指纹
你怎么知道数据文件就是上个月用的那个?计算校验和:SHA-256 之类的哈希会把任何文件变成一个定长的短字符串,只要有一个字节变化它就会变。hashlib.sha256(data).hexdigest() 可得到它;大文件要分块读取。设置也值得同样处理:用排序后的键序列化配置(json.dumps(config, sort_keys=True)),再对文本做哈希,这样两份含义相同的配置无论键的顺序如何,指纹都相同,而一旦某个值改变,指纹就不同。
import hashlib
import json
# A checksum of bytes
print(hashlib.sha256(b"AAPL,184.25").hexdigest()[:12]) # 06ea4aa73ec1 (12 characters of 64)
# A fingerprint of settings, independent of key order
def fingerprint(config):
text = json.dumps(config, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(text.encode("utf-8")).hexdigest()[:12]
one = {"fee": 0.001, "window": 20}
two = {"window": 20, "fee": 0.001}
print(fingerprint(one) == fingerprint(two)) # True
print(fingerprint(one) == fingerprint({"fee": 0.002, "window": 20})) # False记录你运行了什么
每次运行都应在结果旁留下一份小小的清单(manifest):种子、配置指纹、Python 与库的版本、每个输入文件的校验和,以及代码的 Git 提交。有了这份记录,你就能准确说出某个数字是由什么产生的,并在以后重建它。再结合前面的习惯:在 requirements.txt 中固定环境、用 Git 管理代码、把设置集中在一个文件里、用一条命令从原始数据一路运行到最终表格,中间不对数据做任何手工修改。
import json
import platform
# A manifest records what a run used
manifest = {
"seed": 42,
"config_fingerprint": "a1b2c3d4e5f6",
"python": platform.python_version().split(".")[0] + ".x",
"inputs": {"prices.csv": "9f86d081884c"},
}
print(json.dumps(manifest, indent=2, sort_keys=True))
# {
# "config_fingerprint": "a1b2c3d4e5f6",
# "inputs": {
# "prices.csv": "9f86d081884c"
# },
# "python": "3.x",
# "seed": 42
# }- 使用私有的
random.Random(seed),让结果只取决于你的种子。 - 为数据(文件校验和)和设置(排序后的 JSON 再做 SHA-256)生成指纹。
- 每次运行保存清单;固定环境 + Git 提交 + 一条重建命令。
下一节:类与对象,把数据和处理它的函数整合成一个整洁的单元。
实验:评分内容
- 用
seeded_returns生成可重复的随机收益率25 分 - 用
file_checksum为数据文件生成指纹25 分 - 用
config_fingerprint为配置生成指纹25 分 - 用
run_manifest记录一次运行所用的内容25 分