Gleiche Eingaben, gleiche Antwort
Ein Ergebnis ist reproduzierbar, wenn Sie, ein Kollege oder ein Prüfer dasselbe Projekt später ausführen und dieselben Zahlen erhalten. Ein Backtest, der sich nicht neu erzeugen lässt, kann weder geprüft noch debuggt noch geglaubt werden. Ergebnisse driften aus wenigen Gründen: Zufallszahlen, unterschiedliche Bibliotheksversionen, geänderte oder von Hand bearbeitete Daten, im Code verstreute Einstellungen und versteckter Zustand, etwa ein in falscher Reihenfolge ausgeführtes Notebook. Ein reproduzierbarer Ablauf beseitigt jede Driftquelle und hält fest, was verwendet wurde.
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.324Seeds und welcher Generator sie besitzt
Simulationen und Bootstraps nutzen Zufallszahlen, die in Wahrheit eine deterministische Folge sind, die von einem Seed startet. Legen Sie den Seed fest, und der Lauf wiederholt sich exakt. random.seed(42) initialisiert den einen globalen Generator, den das ganze Programm teilt; anderer Code, der Zahlen zieht, verschiebt daher Ihre Folge. Sicherer ist ein eigener Generator, rng = random.Random(42) (oder numpy.random.default_rng(42)), den Sie weiterreichen und aus dem Sie ziehen. Dann hängen Ihre Ergebnisse nur von Ihrem Seed ab, und Ihr Seeding stört niemandes Zufall.
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 itFingerabdrücke für Daten und Einstellungen
Woher wissen Sie, dass die Datendatei dieselbe ist wie letzten Monat? Berechnen Sie eine Prüfsumme: Ein Hash wie SHA-256 macht jede Datei zu einem kurzen String fester Länge, der sich ändert, sobald sich nur ein Byte ändert. hashlib.sha256(data).hexdigest() liefert ihn; große Dateien lesen Sie blockweise. Einstellungen verdienen dieselbe Behandlung: Serialisieren Sie die Konfiguration mit sortierten Schlüsseln (json.dumps(config, sort_keys=True)) und hashen Sie den Text; der Fingerabdruck ist für zwei gleichbedeutende Konfigurationen unabhängig von der Schlüsselreihenfolge identisch und unterscheidet sich, sobald sich ein Wert ändert.
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})) # FalseHalten Sie fest, was Sie ausgeführt haben
Jeder Lauf sollte neben seinen Ergebnissen ein kleines Manifest hinterlassen: den Seed, den Konfigurations-Fingerabdruck, die Python- und Bibliotheksversionen, die Prüfsumme jeder Eingabedatei und den Git-Commit des Codes. Mit dieser Aufzeichnung können Sie genau sagen, was eine Zahl erzeugt hat, und sie später neu aufbauen. Kombinieren Sie sie mit den früheren Gewohnheiten: eine in requirements.txt festgelegte Umgebung, Code in Git, Einstellungen in einer Datei, ein Befehl, der alles von den Rohdaten bis zu den Endtabellen ausführt, und keine manuellen Datenänderungen dazwischen.
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
# }- Nutzen Sie ein eigenes
random.Random(seed), damit die Ergebnisse nur von Ihrem Seed abhängen. - Fingerabdruck für Daten (Datei-Prüfsumme) und Einstellungen (sortiertes JSON, dann SHA-256).
- Speichern Sie bei jedem Lauf ein Manifest; fixierte Umgebung + Git-Commit + ein Befehl zum Neuaufbau.
Als Nächstes: Klassen und Objekte, um Daten und die Funktionen dazu in einer sauberen Einheit zu bündeln.
Lab: Was bewertet wird
- Mit
seeded_returnswiederholbare Zufallsrenditen erzeugen25 Pkt. - Mit
file_checksumeiner Datendatei einen Fingerabdruck geben25 Pkt. - Mit
config_fingerprinteiner Konfiguration einen Fingerabdruck geben25 Pkt. - Mit
run_manifestfesthalten, was ein Lauf verwendet hat25 Pkt.