Kapitel 4. Zeitreihendaten simulieren
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
Bis zu diesem Punkt haben wir besprochen, wo man Zeitreihendaten findet und wie man sie verarbeitet. Jetzt schauen wir uns an, wie man Zeitreihendaten durch Simulation erzeugt.
Unsere Diskussion gliedert sich in drei Teile. Zunächst vergleichen wir Simulationen von Zeitreihendaten mit anderen Arten von Datensimulationen und stellen fest, welche neuen Bereiche von besonderem Interesse sind, wenn wir den Zeitablauf berücksichtigen müssen. Zweitens werfen wir einen Blick auf einige codebasierte Simulationen. Drittens diskutieren wir einige allgemeine Trends bei der Simulation von Zeitreihen.
Der Großteil dieses Kapitels befasst sich mit konkreten Codebeispielen für die Erzeugung verschiedener Arten von Zeitreihendaten. Wir werden die folgenden Beispiele durchgehen:
Wir simulieren das E-Mail-Öffnungs- und Spendenverhalten von Mitgliedern einer gemeinnützigen Organisation im Laufe mehrerer Jahre. Dies steht im Zusammenhang mit den Daten, die wir in "Nachrüsten einer Zeitreihendatenerhebung aus einer Tabellensammlung" untersucht haben .
Wir simulieren die Ereignisse in einer Taxiflotte von 1.000 Fahrzeugen mit verschiedenen Schichtbeginnzeiten und tageszeitabhängigen Fahrgastabholfrequenzen im Laufe eines Tages.
Wir simulieren die schrittweise Zustandsentwicklung eines magnetischen Festkörpers bei einer bestimmten Temperatur und Größe unter Anwendung der relevanten physikalischen Gesetze.
Diese drei Codebeispiele entsprechen drei Klassen von Zeitreihensimulationen:
- Heuristische Simulationen
Wir entscheiden, wie die Welt funktionieren soll, stellen sicher, dass sie Sinn macht, und programmieren sie, eine Regel nach der anderen.
- Diskrete Ereignissimulationen
Wir bauen einzelne Akteure mit bestimmten Regeln in unserem Universum und lassen diese Akteure dann laufen, um zu sehen, wie sich das Universum mit der Zeit entwickelt.
- Physikbasierte Simulationen
Wir wenden physikalische Gesetze an, um zu sehen, wie sich ein System im Laufe der Zeit entwickelt.
Die Simulation von Zeitreihen kann eine wertvolle analytische Übung sein, die wir in späteren Kapiteln auch in Bezug auf bestimmte Modelle zeigen werden.
Was ist das Besondere an der Simulation von Zeitreihen?
Das Simulieren von Daten ist ein Bereich der Datenwissenschaft, der nur selten gelehrt wird, der aber für Zeitreihendaten besonders nützlich ist. Das ergibt sich aus einem der Nachteile von Zeitreihendaten: Keine zwei Datenpunkte in derselben Zeitreihe sind genau vergleichbar, da sie zu unterschiedlichen Zeiten geschehen. Wenn wir darüber nachdenken wollen, was zu einem bestimmten Zeitpunkt passiert sein könnte, begeben wir uns in die Welt der Simulation.
Simulationen können einfach oder komplex sein. Auf der einfacheren Seite wirst du in jedem Statistik-Lehrbuch über Zeitreihen synthetische Daten finden, z. B. in Form eines Random Walk. Diese werden in der Regel als kumulative Summen eines Zufallsprozesses (wie z. B. R's rnorm
) oder durch eine periodische Funktion (wie z. B. eine Sinuskurve) erzeugt. Auf der komplexeren Seite machen viele Wissenschaftler und Ingenieure mit der Simulation von Zeitreihen Karriere. Zeitreihensimulationen sind nach wie vor ein aktives Forschungsgebiet - und ein rechenintensives - in vielen Bereichen, darunter:
-
Meteorologie
-
Finanzen
-
Epidemiologie
-
Quantenchemie
-
Plasmaphysik
In einigen dieser Fälle sind die grundlegenden Verhaltensregeln gut verstanden, aber aufgrund der Komplexität der Gleichungen kann es trotzdem schwierig sein, alles zu berücksichtigen, was passieren kann (Meteorologie, Quantenchemie, Plasmaphysik). In anderen Fällen sind nicht alle Vorhersagevariablen bekannt, und Experten sind sich nicht einmal sicher, ob aufgrund der stochastischen, nichtlinearen Natur der untersuchten Systeme (Finanzen, Epidemiologie) perfekte Vorhersagen gemacht werden können.
Simulation vs. Vorhersage
Simulationen und Prognosen sind ähnliche Aufgaben. In beiden Fällen musst du Hypothesen über die zugrunde liegende Systemdynamik und die Parameter aufstellen und dann aus diesen Hypothesen Datenpunkte extrapolieren.
Dennoch gibt es wichtige Unterschiede, die du beachten solltest, wenn du dich mit Simulationen statt mit Prognosen befasst und sie entwickelst:
-
Es kann einfacher sein, qualitative Beobachtungen in eine Simulation zu integrieren als in eine Prognose.
-
Simulationen werden in großem Maßstab durchgeführt, damit du viele alternative Szenarien (Tausende oder mehr) sehen kannst, während Prognosen sorgfältiger erstellt werden sollten.
-
Bei Simulationen steht weniger auf dem Spiel als bei Prognosen; es stehen keine Menschenleben und keine Ressourcen auf dem Spiel, so dass du in deinen ersten Simulationsrunden kreativer und experimentierfreudiger sein kannst. Natürlich musst du am Ende sicherstellen, dass du deine Simulationen begründen kannst, so wie du auch deine Prognosen begründen musst.
Simulationen im Code
Als Nächstes sehen wir uns drei Beispiele für die Codierung von Simulationen von Zeitreihen an. Während du diese Beispiele liest, solltest du bedenken, wie viele verschiedene Daten simuliert werden können, um eine "Zeitreihe" zu erstellen, und wie das zeitliche Element sehr spezifisch und menschengesteuert sein kann, z. B. Wochentage und Tageszeiten von Spenden, aber auch sehr unspezifisch und im Wesentlichen unbeschriftet sein kann, z. B. der"n-teSchritt" einer Physiksimulation.
Die drei Beispiele für Simulationen, die wir in diesem Abschnitt besprechen werden, sind:
Wir simulieren einen synthetischen Datensatz, um unsere Hypothesen darüber zu testen, wie das Verhalten der Mitglieder einer Organisation zwischen der Empfänglichkeit für E-Mails der Organisation und der Spendenbereitschaft korreliert (oder auch nicht). Dies ist das einfachste Beispiel, da wir die Beziehungen hart kodieren und tabellarische Daten mit
for
Schleifen und Ähnlichem erzeugen.Wir simulieren den synthetischen Datensatz, um das Gesamtverhalten einer Taxiflotte mit Schichtzeiten und tageszeitabhängiger Fahrgastfrequenz zu untersuchen. In diesem Datensatz nutzen wir die objektorientierten Eigenschaften von Python sowie die Generatoren, die sehr hilfreich sind, wenn wir ein System in Gang setzen und sehen wollen, was es tut.
Simulation des physikalischen Prozesses, bei dem ein magnetisches Material nach und nach seine einzelnen magnetischen Elemente ausrichtet, die zunächst durcheinander sind, sich aber schließlich zu einem geordneten System zusammenfügen. In diesem Beispiel sehen wir, wie physikalische Gesetze eine Zeitreihensimulation steuern und eine natürliche zeitliche Skalierung in einen Prozess einfügen können.
Die Arbeit selbst machen
Wenn du Simulationen programmierst, musst du die logischen Regeln, die für dein System gelten, im Auge behalten. Hier gehen wir ein Beispiel durch, bei dem der Programmierer den Großteil der Arbeit übernimmt, um sicherzustellen, dass die Daten einen Sinn ergeben (indem er zum Beispiel keine Ereignisse angibt, die in einer unlogischen Reihenfolge stattfinden).
Wir beginnen mit der Definition des Mitgliederuniversums, d.h. wie viele Mitglieder wir haben und wann sie der Organisation beigetreten sind. Außerdem ordnen wir jedem Mitglied einen Mitgliedsstatus zu:
## python
>>>
## membership status
>>>
years
=
[
'2014'
,
'2015'
,
'2016'
,
'2017'
,
'2018'
]
>>>
memberStatus
=
[
'bronze'
,
'silver'
,
'gold'
,
'inactive'
]
>>>
memberYears
=
np
.
random
.
choice
(
years
,
1000
,
>>>
p
=
[
0.1
,
0.1
,
0.15
,
0.30
,
0.35
])
>>>
memberStats
=
np
.
random
.
choice
(
memberStatus
,
1000
,
>>>
p
=
[
0.5
,
0.3
,
0.1
,
0.1
])
>>>
yearJoined
=
pd
.
DataFrame
({
'yearJoined'
:
memberYears
,
>>>
'memberStats'
:
memberStats
})
Beachte, dass allein durch diese Codezeilen bereits viele Regeln/Annahmen in die Simulation eingebaut sind. Wir legen bestimmte Wahrscheinlichkeiten für die Jahre fest, in denen die Mitglieder beigetreten sind. Außerdem machen wir den Status eines Mitglieds völlig unabhängig von seinem Eintrittsjahr. In der realen Welt können wir das wahrscheinlich schon besser machen, denn diese beiden Variablen sollten in einem gewissen Zusammenhang stehen, vor allem, wenn wir Anreize schaffen wollen, dass die Leute Mitglieder bleiben.
Wir erstellen eine Tabelle, die anzeigt, wann die Mitglieder jede Woche E-Mails geöffnet haben. In diesem Fall legen wir das Verhalten unserer Organisation fest: Wir verschicken drei E-Mails pro Woche. Wir legen auch verschiedene Verhaltensmuster der Mitglieder in Bezug auf E-Mails fest:
Niemals E-Mails öffnen
Konstantes Niveau des Engagements/der Öffnungsrate der E-Mail
Steigendes oder sinkendes Engagement
Wir können uns vorstellen, wie wir dies komplexer und nuancierter gestalten können, je nach anekdotischen Beobachtungen von Veteranen oder neuen Hypothesen, die wir über unbeobachtbare Prozesse haben, die die Daten beeinflussen:
## python
>>>
NUM_EMAILS_SENT_WEEKLY
=
3
>>>
## we define several functions for different patterns
>>>
def
never_opens
(
period_rng
):
>>>
return
[]
>>>
def
constant_open_rate
(
period_rng
):
>>>
n
,
p
=
NUM_EMAILS_SENT_WEEKLY
,
np
.
random
.
uniform
(
0
,
1
)
>>>
num_opened
=
np
.
random
.
binomial
(
n
,
p
,
len
(
period_rng
))
>>>
return
num_opened
>>>
def
increasing_open_rate
(
period_rng
):
>>>
return
open_rate_with_factor_change
(
period_rng
,
>>>
np
.
random
.
uniform
(
1.01
,
>>>
1.30
))
>>>
def
decreasing_open_rate
(
period_rng
):
>>>
return
open_rate_with_factor_change
(
period_rng
,
>>>
np
.
random
.
uniform
(
0.5
,
>>>
0.99
))
>>>
def
open_rate_with_factor_change
(
period_rng
,
fac
):
>>>
if
len
(
period_rng
)
<
1
:
>>>
return
[]
>>>
times
=
np
.
random
.
randint
(
0
,
len
(
period_rng
),
>>>
int
(
0.1
*
len
(
period_rng
)))
>>>
num_opened
=
np
.
zeros
(
len
(
period_rng
))
>>>
for
prd
in
range
(
0
,
len
(
period_rng
),
2
):
>>>
try
:
>>>
n
,
p
=
NUM_EMAILS_SENT_WEEKLY
,
np
.
random
.
uniform
(
0
,
>>>
1
)
>>>
num_opened
[
prd
:(
prd
+
2
)]
=
np
.
random
.
binomial
(
n
,
p
,
>>>
2
)
>>>
p
=
max
(
min
(
1
,
p
*
fac
),
0
)
>>>
except
:
>>>
num_opened
[
prd
]
=
np
.
random
.
binomial
(
n
,
p
,
1
)
>>>
for
t
in
range
(
len
(
times
)):
>>>
num_opened
[
times
[
t
]]
=
0
>>>
return
num_opened
Wir haben Funktionen definiert, die vier verschiedene Arten von Verhalten simulieren:
- Mitglieder, die die E-Mails, die wir ihnen schicken, nie öffnen
(
never_opens()
)- Mitglieder, die jede Woche etwa die gleiche Anzahl von E-Mails öffnen
(
constant_open_rate()
)- Mitglieder, die jede Woche eine abnehmende Anzahl von E-Mails öffnen
(
decreasing_open_rate()
)- Mitglieder, die jede Woche eine größere Anzahl von E-Mails öffnen
(
increasing_open_rate()
)
Wir stellen sicher, dass diejenigen, die sich im Laufe der Zeit immer mehr engagieren oder desengagieren, über die Funktionen increasing_open_rate()
und decreasing_open_rate()
mit der Funktion open_rate_with_factor_change()
auf die gleiche Weise simuliert werden.
Wir müssen auch ein System entwickeln, um das Spendenverhalten zu modellieren. Wir wollen nicht völlig naiv sein, sonst gibt uns unsere Simulation keinen Einblick in das, was wir erwarten sollten. Das heißt, wir wollen unsere aktuellen Hypothesen über das Verhalten der Mitglieder in das Modell einbauen und dann testen, ob die Simulationen, die auf diesen Hypothesen basieren, mit unseren realen Daten übereinstimmen. Hier setzen wir das Spendenverhalten in einen lockeren, aber nicht deterministischen Zusammenhang mit der Anzahl der E-Mails, die ein Mitglied geöffnet hat:
## python
>>>
## donation behavior
>>>
def
produce_donations
(
period_rng
,
member_behavior
,
num_emails
,
>>>
use_id
,
member_join_year
):
>>>
donation_amounts
=
np
.
array
([
0
,
25
,
50
,
75
,
100
,
250
,
500
,
>>>
1000
,
1500
,
2000
])
>>>
member_has
=
np
.
random
.
choice
(
donation_amounts
)
>>>
email_fraction
=
num_emails
/
>>>
(
NUM_EMAILS_SENT_WEEKLY
*
len
(
period_rng
))
>>>
member_gives
=
member_has
*
email_fraction
>>>
member_gives_idx
=
np
.
where
(
member_gives
>>>
>=
donation_amounts
)[
0
][
-
1
]
>>>
member_gives_idx
=
max
(
min
(
member_gives_idx
,
>>>
len
(
donation_amounts
)
-
2
),
>>>
1
)
>>>
num_times_gave
=
np
.
random
.
poisson
(
2
)
*
>>>
(
2018
-
member_join_year
)
>>>
times
=
np
.
random
.
randint
(
0
,
len
(
period_rng
),
num_times_gave
)
>>>
dons
=
pd
.
DataFrame
({
'member'
:
[],
>>>
'amount'
:
[],
>>>
'timestamp'
:
[]})
>>>
for
n
in
range
(
num_times_gave
):
>>>
donation
=
donation_amounts
[
member_gives_idx
>>>
+
np
.
random
.
binomial
(
1
,
.
3
)]
>>>
ts
=
str
(
period_rng
[
times
[
n
]]
.
start_time
>>>
+
random_weekly_time_delta
())
>>>
dons
=
dons
.
append
(
pd
.
DataFrame
(
>>>
{
'member'
:
[
use_id
],
>>>
'amount'
:
[
donation
],
>>>
'timestamp'
:
[
ts
]}))
>>>
>>>
if
dons
.
shape
[
0
]
>
0
:
>>>
dons
=
dons
[
dons
.
amount
!=
0
]
>>>
## we don't report zero donation events as this would not
>>>
## be recorded in a real world database
>>>
>>>
return
dons
Wir haben ein paar Schritte unternommen, um sicherzustellen, dass der Code ein realistisches Verhalten zeigt:
Wir machen die Gesamtzahl der Spenden davon abhängig, wie lange jemand bereits Mitglied ist.
Wir erstellen einen Vermögensstatus pro Mitglied und bauen eine Hypothese über das Verhalten ein, die besagt, dass der Spendenbetrag mit einem stabilen Betrag zusammenhängt, den eine Person für Spenden vorgesehen hat.
Da das Verhalten unserer Mitglieder an einen bestimmten Zeitstempel gebunden ist, müssen wir entscheiden, in welchen Wochen jedes Mitglied gespendet hat und wann in dieser Woche die Spende getätigt wurde. Wir schreiben eine Hilfsfunktion, die einen zufälligen Zeitpunkt in der Woche auswählt:
## python
>>>
def
random_weekly_time_delta
():
>>>
days_of_week
=
[
d
for
d
in
range
(
7
)]
>>>
hours_of_day
=
[
h
for
h
in
range
(
11
,
23
)]
>>>
minute_of_hour
=
[
m
for
m
in
range
(
60
)]
>>>
second_of_minute
=
[
s
for
s
in
range
(
60
)]
>>>
return
pd
.
Timedelta
(
str
(
np
.
random
.
choice
(
days_of_week
))
>>>
+
" days"
)
+
>>>
pd
.
Timedelta
(
str
(
np
.
random
.
choice
(
hours_of_day
))
>>>
+
" hours"
)
+
>>>
pd
.
Timedelta
(
str
(
np
.
random
.
choice
(
minute_of_hour
))
>>>
+
" minutes"
)
+
>>>
pd
.
Timedelta
(
str
(
np
.
random
.
choice
(
second_of_minute
))
>>>
+
" seconds"
)
Du hast vielleicht bemerkt, dass wir die Stunde des Zeitstempels nur aus dem Bereich von 11 bis 23 ziehen (hours_of_day = [h for h in range(11, 23)]
). Wir gehen von einem Universum aus, in dem sich Menschen in einer sehr begrenzten Anzahl von Zeitzonen oder sogar nur in einer einzigen Zeitzone aufhalten, da wir keine Stunden außerhalb des angegebenen Bereichs zulassen. Hier bauen wir unser zugrunde liegendes Modell für das Verhalten der Nutzer/innen weiter aus.
Wir erwarten daher ein einheitliches Verhalten von unseren Nutzern, als ob sie sich alle in einer oder mehreren benachbarten Zeitzonen befinden würden, und wir gehen davon aus, dass das Spendenverhalten der Menschen vom späten Morgen bis zum späten Abend angemessen ist, aber nicht über Nacht und nicht gleich nach dem Aufwachen.
Schließlich setzen wir alle soeben entwickelten Komponenten zusammen, um eine bestimmte Anzahl von Mitgliedern und damit verbundenen Ereignissen so zu simulieren, dass sichergestellt ist, dass Ereignisse nur dann eintreten, wenn ein Mitglied beigetreten ist, und dass die E-Mail-Ereignisse eines Mitglieds in einem gewissen Verhältnis zu seinen Spendenereignissen stehen (aber nicht in einem unrealistisch kleinen Verhältnis):
## python
>>>
behaviors
=
[
never_opens
,
>>>
constant_open_rate
,
>>>
increasing_open_rate
,
>>>
decreasing_open_rate
]
>>>
member_behaviors
=
np
.
random
.
choice
(
behaviors
,
1000
,
>>>
[
0.2
,
0.5
,
0.1
,
0.2
])
>>>
rng
=
pd
.
period_range
(
'2015-02-14'
,
'2018-06-01'
,
freq
=
'W'
)
>>>
emails
=
pd
.
DataFrame
({
'member'
:
[],
>>>
'week'
:
[],
>>>
'emailsOpened'
:
[]})
>>>
donations
=
pd
.
DataFrame
({
'member'
:
[],
>>>
'amount'
:
[],
>>>
'timestamp'
:
[]})
>>>
for
idx
in
range
(
yearJoined
.
shape
[
0
]):
>>>
## randomly generate the date when a member would have joined
>>>
join_date
=
pd
.
Timestamp
(
yearJoined
.
iloc
[
idx
]
.
yearJoined
)
+
>>>
pd
.
Timedelta
(
str
(
np
.
random
.
randint
(
0
,
365
))
+
>>>
' days'
)
>>>
join_date
=
min
(
join_date
,
pd
.
Timestamp
(
'2018-06-01'
))
>>>
>>>
## member should not have action timestamps before joining
>>>
member_rng
=
rng
[
rng
>
join_date
]
>>>
>>>
if
len
(
member_rng
)
<
1
:
>>>
continue
>>>
>>>
info
=
member_behaviors
[
idx
](
member_rng
)
>>>
if
len
(
info
)
==
len
(
member_rng
):
>>>
emails
=
emails
.
append
(
pd
.
DataFrame
(
>>>
{
'member'
:
[
idx
]
*
len
(
info
),
>>>
'week'
:
[
str
(
r
.
start_time
)
for
r
in
member_rng
],
>>>
'emailsOpened'
:
info
}))
>>>
donations
=
donations
.
append
(
>>>
produce_donations
(
member_rng
,
member_behaviors
[
idx
],
>>>
sum
(
info
),
idx
,
join_date
.
year
))
Dann schauen wir uns das zeitliche Verhalten der Spenden an, um ein Gefühl dafür zu bekommen, wie wir dies für weitere Analysen oder Prognosen nutzen können. Wir stellen die Gesamtsumme der Spenden dar, die wir für jeden Monat des Datensatzes erhalten haben (siehe Abbildung 4-1):
## python
>>>
df
.
set_index
(
pd
.
to_datetime
(
df
.
timestamp
),
inplace
=
True
)
>>>
df
.
sort_index
(
inplace
=
True
)
>>>
df
.
groupby
(
pd
.
Grouper
(
freq
=
'M'
))
.
amount
.
sum
()
.
plot
()
Es sieht so aus, als ob die Zahl der Spenden und der geöffneten E-Mails im Zeitraum von 2015 bis 2018 gestiegen ist. Das ist nicht überraschend, denn auch die Zahl der Mitglieder ist im Laufe der Zeit gestiegen, wie die kumulierte Summe der Mitglieder und das Jahr ihres Beitritts zeigen. Eine der Grundannahmen unseres Modells war, dass wir ein Mitglied nach seinem Beitritt auf unbestimmte Zeit behalten können. Wir haben keine anderen Vorkehrungen getroffen als die, dass Mitglieder eine abnehmende Anzahl von E-Mails öffnen. Aber selbst in diesem Fall haben wir uns die Möglichkeit offen gelassen, weiter zu spenden. Wir sehen diese Annahme einer unbegrenzt andauernden Mitgliedschaft (und das damit verbundene Spendenverhalten) in Abbildung 4-1. Wir sollten wahrscheinlich zurückgehen und unseren Code verfeinern, denn eine unbegrenzte Mitgliedschaft und Spenden sind kein realistisches Szenario.
Dies ist keine klassische Zeitreihensimulation, daher mag es sich eher wie eine Übung zur Erstellung von Tabellendaten anfühlen. Das ist sie auch, aber wir mussten uns mit Zeitreihen beschäftigen:
-
Wir mussten Entscheidungen darüber treffen, in wie vielen Zeitreihen sich unsere Nutzer befanden.
-
Wir mussten Entscheidungen darüber treffen, welche Trends wir im Laufe der Zeit modellieren wollten:
-
Im Fall von E-Mails haben wir uns für drei Trends entschieden: stabile, steigende und sinkende Öffnungsraten von E-Mails.
-
Im Falle der Spenden machten wir die Spenden zu einem stabilen Verhaltensmuster, das davon abhing, wie viele E-Mails das Mitglied in seinem Leben geöffnet hatte. Dies beinhaltete eine Vorausschau, aber da wir Daten generierten, war dies ein Weg, um zu entscheiden, dass die allgemeine Affinität eines Mitglieds zur Organisation, die zu mehr geöffneten E-Mails führt, auch die Häufigkeit der Spenden erhöht.
-
-
Wir mussten aufpassen, dass wir keine E-Mails geöffnet oder Spenden getätigt haben, bevor das Mitglied der Organisation beigetreten ist.
-
Wir mussten sicherstellen, dass unsere Daten nicht in die Zukunft reichen, damit es für die Nutzer der Daten realistischer wird. Beachte, dass es für eine Simulation in Ordnung ist, wenn unsere Daten in die Zukunft gehen.
Aber es ist nicht perfekt. Der hier vorgestellte Code ist unhandlich und erzeugt kein realistisches Universum. Und da nur der Programmierer die Logik überprüft hat, könnten Kanten übersehen worden sein, so dass die Ereignisse in einer unlogischen Reihenfolge ablaufen. Zum Schutz vor solchen Fehlern wäre es gut, vor der Simulation externe Maßstäbe und Standards für die Gültigkeit festzulegen.
Wir brauchen eine Software, die ein logisches und konsistentes Universum erzwingt. Im nächsten Abschnitt werden wir uns die Python-Generatoren als eine bessere Option ansehen.
Ein Simulationsuniversum bauen, das sich selbst steuert
Manchmal hast du ein bestimmtes System und möchtest die Regeln für dieses System aufstellen und sehen, wie es funktioniert. Vielleicht willst du dir vorstellen, wie ein Universum unabhängiger Mitglieder, die auf deine Anwendung zugreifen, diese nutzen wird, oder du willst versuchen, eine interne Theorie der Entscheidungsfindung auf der Grundlage eines angenommenen externen Verhaltens zu validieren. In diesen Fällen willst du herausfinden, wie die einzelnen Agenten im Laufe der Zeit zu deinen Gesamtkennzahlen beitragen. Python eignet sich dank der Verfügbarkeit von Generatoren besonders gut für diese Aufgabe. Wenn du anfängst, Software zu entwickeln, anstatt dich auf die Analyse zu beschränken, ist es sinnvoll, zu Python zu wechseln, auch wenn du dich in R wohler fühlst.
Mit Generatoren können wir eine Reihe von unabhängigen (oder abhängigen!) Akteuren erstellen und sie aufziehen, um zu sehen, was sie tun, ohne dass wir zu viel Boilerplate-Code brauchen, um alles im Auge zu behalten.
Im nächsten Codebeispiel untersuchen wir eine Taxisimulation.1 Wir wollen uns vorstellen, wie sich eine Flotte von Taxis, die ihre Schichten zu unterschiedlichen Zeiten antreten, insgesamt verhalten könnte. Dazu wollen wir viele einzelne Taxis erstellen, sie in einer Cyberstadt loslassen und sie ihre Aktivitäten zurückmelden lassen.
Eine solche Simulation könnte außerordentlich kompliziert sein. Zu Demonstrationszwecken nehmen wir in Kauf, dass wir eine einfachere Welt bauen, als wir sie uns vorstellen ("Alle Modelle sind falsch..."). Zunächst versuchen wir zu verstehen, was ein Python-Generator ist.
Betrachten wir zunächst eine Methode, die ich geschrieben habe, um eine Taxi-Identifikationsnummer abzurufen:
## python
>>>
import
numpy
as
np
>>>
def
taxi_id_number
(
num_taxis
):
>>>
arr
=
np
.
arange
(
num_taxis
)
>>>
np
.
random
.
shuffle
(
arr
)
>>>
for
i
in
range
(
num_taxis
):
>>>
yield
arr
[
i
]
Für diejenigen, die mit Generatoren nicht vertraut sind, ist hier der vorangegangene Code in Aktion:
## python
>>>
ids
=
taxi_id_number
(
10
)
>>>
(
next
(
ids
))
>>>
(
next
(
ids
))
>>>
(
next
(
ids
))
die ausgedruckt werden könnten:
7 2 5
Er durchläuft die Schleife, bis er 10 Zahlen ausgegeben hat. Dann verlässt er die for
Schleife innerhalb des Generators und gibt eine StopIteration
Ausnahme aus.
Die taxi_id_number()
produziert Einweg-Objekte, die alle unabhängig voneinander sind und ihren eigenen Zustand behalten. Dies ist eine Generatorfunktion. Du kannst dir Generatoren als winzige Objekte vorstellen, die ihr eigenes kleines Bündel von Zustandsvariablen verwalten. Das ist nützlich, wenn du viele Objekte parallel zueinander haben willst, von denen jedes seine eigenen Variablen verwaltet.
Bei dieser einfachen Taxisimulation teilen wir unsere Taxis in verschiedene Schichten ein und verwenden einen Generator, um die Schichten anzugeben. Wir planen mehr Taxis in der Tagesmitte als in den Abend- oder Nachtschichten ein, indem wir unterschiedliche Wahrscheinlichkeiten für den Beginn einer Schicht zu einer bestimmten Zeit festlegen:
## python
>>>
def
shift_info
():
>>>
start_times_and_freqs
=
[(
0
,
8
),
(
8
,
30
),
(
16
,
15
)]
>>>
indices
=
np
.
arange
(
len
(
start_times_and_freqs
))
>>>
while
True
:
>>>
idx
=
np
.
random
.
choice
(
indices
,
p
=
[
0.25
,
0.5
,
0.25
])
>>>
start
=
start_times_and_freqs
[
idx
]
>>>
yield
(
start
[
0
],
start
[
0
]
+
7.5
,
start
[
1
])
Achte auf start_times_and_freqs
. Dies ist unser erster Teil des Codes, der dazu beiträgt, dass dies eine Zeitreihensimulation wird. Wir geben an, dass es zu verschiedenen Tageszeiten unterschiedlich wahrscheinlich ist, dass der Schicht ein Taxi zugewiesen wird. Außerdem gibt es zu verschiedenen Tageszeiten eine unterschiedliche durchschnittliche Anzahl von Fahrten.
Jetzt erstellen wir einen komplexeren Generator, der die vorangegangenen Generatoren nutzt, um individuelle Taxiparameter festzulegen und individuelle Taxi-Fahrpläne zu erstellen:
## python
>>>
def
taxi_process
(
taxi_id_generator
,
shift_info_generator
):
>>>
taxi_id
=
next
(
taxi_id_generator
)
>>>
shift_start
,
shift_end
,
shift_mean_trips
=
>>>
next
(
shift_info_generator
)
>>>
actual_trips
=
round
(
np
.
random
.
normal
(
loc
=
shift_mean_trips
,
>>>
scale
=
2
))
>>>
average_trip_time
=
6.5
/
shift_mean_trips
*
60
>>>
# convert mean trip time to minutes
>>>
between_events_time
=
1.0
/
(
shift_mean_trips
-
1
)
*
60
>>>
# this is an efficient city where cabs are seldom unused
>>>
time
=
shift_start
>>>
yield
TimePoint
(
taxi_id
,
'start shift'
,
time
)
>>>
deltaT
=
np
.
random
.
poisson
(
between_events_time
)
/
60
>>>
time
+=
deltaT
>>>
for
i
in
range
(
actual_trips
):
>>>
yield
TimePoint
(
taxi_id
,
'pick up '
,
time
)
>>>
deltaT
=
np
.
random
.
poisson
(
average_trip_time
)
/
60
>>>
time
+=
deltaT
>>>
yield
TimePoint
(
taxi_id
,
'drop off '
,
time
)
>>>
deltaT
=
np
.
random
.
poisson
(
between_events_time
)
/
60
>>>
time
+=
deltaT
>>>
deltaT
=
np
.
random
.
poisson
(
between_events_time
)
/
60
>>>
time
+=
deltaT
>>>
yield
TimePoint
(
taxi_id
,
'end shift '
,
time
)
Hier greift das Taxi auf Generatoren zu, um seine ID-Nummer, die Startzeiten der Schicht und die durchschnittliche Anzahl der Fahrten für seine Startzeit zu ermitteln. Von dort aus macht es sich auf seine individuelle Fahrt, indem es eine bestimmte Anzahl von Fahrten auf seiner eigenen Zeitachse durchläuft und diese an den Kunden sendet, der next()
über diesen Generator aufruft. Dieser Generator erzeugt also eine Zeitreihe von Punkten für ein einzelnes Taxi.
Der Taxigenerator liefert TimePoint
s, die wie folgt definiert sind:
## python
>>>
from
dataclasses
import
dataclass
>>>
@dataclass
>>>
class
TimePoint
:
>>>
taxi_id
:
int
>>>
name
:
str
>>>
time
:
float
>>>
def
__lt__
(
self
,
other
):
>>>
return
self
.
time
<
other
.
time
Wir verwenden den relativ neuen dataclass
Dekorator, um den Code zu vereinfachen (dies erfordert Python 3.7). Ich empfehle allen Datenwissenschaftlern, die Python verwenden, sich mit dieser neuen und datenfreundlichen Erweiterung von Python vertraut zu machen.
Pythons Dunder-Methoden
Die Dunder-Methoden von Python, deren Namen mit zwei Unterstrichen beginnen und enden, sind eine Reihe von eingebauten Methoden für jede Klasse. Dunder-Methoden werden automatisch aufgerufen, wenn ein bestimmtes Objekt verwendet wird. Es gibt vordefinierte Implementierungen, die außer Kraft gesetzt werden können, wenn du sie für deine Klasse selbst definierst. Es gibt viele Gründe, warum du das tun möchtest, z. B. im Fall des vorangegangenen Codes, in dem wir TimePoint
s nur auf der Grundlage ihrer Zeit und nicht auf der Grundlage ihrer taxi_id
oder name
Attribute vergleichen wollen.
Dunder ist ursprünglich eine Abkürzung für "double under".
Neben dem automatisch erzeugten Initialisierer für TimePoint
brauchen wir nur noch zwei weitere Dunder-Methoden, __lt__
(zum Vergleichen von TimePoint
s) und __str__
(zum Ausdrucken von TimePoint
s, hier nicht gezeigt). Wir brauchen den Vergleich, weil wir alle erzeugten TimePoint
s in eine Datenstruktur aufnehmen werden, die sie in der richtigen Reihenfolge hält: eine Prioritätswarteschlange. Eine Prioritätswarteschlange ist ein abstrakter Datentyp, in den Objekte in beliebiger Reihenfolge eingefügt werden können, der aber Objekte in einer bestimmten Reihenfolge auf der Grundlage ihrer Priorität ausgibt.
Abstrakter Datentyp
Ein abstrakter Datentyp ist ein Berechnungsmodell, das durch sein Verhalten definiert ist. Es besteht aus einer Aufzählung möglicher Aktionen und Eingabedaten sowie den Ergebnissen dieser Aktionen für bestimmte Datensätze.
Ein allgemein bekannter abstrakter Datentyp ist ein FIFO-Datentyp (First-in-First-out). Dies bedeutet, dass die Objekte in der gleichen Reihenfolge aus der Datenstruktur ausgegeben werden, in der sie in die Datenstruktur eingegeben wurden. Wie der Programmierer dies erreichen will, ist eine Frage der Implementierung und nicht der Definition.
Wir haben eine Simulationsklasse, die diese Taxi-Generatoren ausführt und sie zusammenhält. Dies ist nicht nur eine dataclass
, denn sie hat eine ganze Reihe von Funktionen, sogar im Initialisierer, um die Eingaben in eine sinnvolle Anordnung von Informationen und Verarbeitung zu bringen. Beachte, dass die einzige öffentliche Funktion die run()
Funktion ist:
## python
>>>
import
queue
>>>
class
Simulator
:
>>>
def
__init__
(
self
,
num_taxis
):
>>>
self
.
_time_points
=
queue
.
PriorityQueue
()
>>>
taxi_id_generator
=
taxi_id_number
(
num_taxis
)
>>>
shift_info_generator
=
shift_info
()
>>>
self
.
_taxis
=
[
taxi_process
(
taxi_id_generator
,
>>>
shift_info_generator
)
for
>>>
i
in
range
(
num_taxis
)]
>>>
self
.
_prepare_run
()
>>>
def
_prepare_run
(
self
):
>>>
for
t
in
self
.
_taxis
:
>>>
while
True
:
>>>
try
:
>>>
e
=
next
(
t
)
>>>
self
.
_time_points
.
put
(
e
)
>>>
except
:
>>>
break
>>>
def
run
(
self
):
>>>
sim_time
=
0
>>>
while
sim_time
<
24
:
>>>
if
self
.
_time_points
.
empty
():
>>>
break
>>>
p
=
self
.
_time_points
.
get
()
>>>
sim_time
=
p
.
time
>>>
(
p
)
Zuerst erstellen wir die Anzahl von Taxigeneratoren, die wir brauchen, um die richtige Anzahl von Taxis zu repräsentieren. Dann durchlaufen wir jedes dieser Taxis, solange es noch TimePoint
s hat, und schieben alle diese TimePoint
s in eine Prioritätswarteschlange. Die Priorität des Objekts wird für eine benutzerdefinierte Klasse wie TimePoint
durch unsere Implementierung von TimePoint
's __lt__
bestimmt, wobei wir die Startzeit vergleichen. Wenn die TimePoint
s in die Prioritäts-Warteschlange geschoben werden, werden sie also in zeitlicher Reihenfolge ausgegeben.
Wir führen die Simulation durch:
## python
>>>
sim
=
Simulator
(
1000
)
>>>
sim
.
run
()
So sieht die Ausgabe aus (deine Ausgabe wird anders aussehen, da wir keinen Seed gesetzt haben - und jedes Mal, wenn du den Code ausführst, wird sie anders sein als bei der letzten Iteration):
id: 0539 name: drop off time: 23:58 id: 0318 name: pick up time: 23:58 id: 0759 name: end shift time: 23:58 id: 0977 name: pick up time: 23:58 id: 0693 name: end shift time: 23:59 id: 0085 name: end shift time: 23:59 id: 0351 name: end shift time: 23:59 id: 0036 name: end shift time: 23:59 id: 0314 name: drop off time: 23:59
Setzen eines Seeds beim Generieren von Zufallszahlen
Wenn du einen Code schreibst, der Zufallszahlen generiert, möchtest du vielleicht sicherstellen, dass er reproduzierbar ist (z. B. wenn du Unit-Tests für einen Code einrichten willst, der normalerweise zufällig ist, oder wenn du versuchst, die Fehlerquellen einzugrenzen, um die Fehlersuche zu erleichtern). Um sicherzustellen, dass die Zufallszahlen in der gleichen, nicht zufälligen Reihenfolge ausgegeben werden, legst du einen Seed fest. Das ist ein gängiger Vorgang, daher gibt es für jede Computersprache Anleitungen, wie man einen Seed setzt.
Der Einfachheit halber haben wir auf die nächste Minute gerundet, obwohl wir auch feinere Daten zur Verfügung haben. Welche zeitliche Auflösung wir verwenden, hängt von unseren Zielen ab:
Wenn wir den Menschen in unserer Stadt zeigen wollen, wie sich die Taxiflotte auf den Verkehr auswirkt, können wir die stündlichen Aggregate anzeigen.
Wenn wir eine Taxi-App sind und die Auslastung unseres Servers verstehen müssen, wollen wir wahrscheinlich Daten im Minutentakt oder sogar hochaufgelöste Daten betrachten, um über unser Infrastrukturdesign und unsere Kapazität nachzudenken.
Wir haben uns entschieden, die Taxifahrten TimePoint
so zu melden, wie sie "stattfinden". Das heißt, wir berichten über den Beginn einer Taxifahrt ("Abholung") ohne den Zeitpunkt, zu dem die Fahrt endet, obwohl wir das leicht hätten kürzen können. Das ist eine Möglichkeit, die Zeitreihe realistischer zu gestalten, denn bei einem Live-Stream hättest du die Ereignisse wahrscheinlich auf diese Weise aufgezeichnet.
Beachte, dass unsere Zeitreihensimulation, wie im vorherigen Fall, noch keine Zeitreihe erzeugt hat. Wir haben jedoch ein Protokoll erstellt und können uns auf verschiedene Weise zu einer Zeitreihe durchringen:
Ausgabe in eine CSV-Datei oder eine Zeitreihendatenbank, während wir die Simulation ausführen.
Führe eine Art Online-Modell aus, das mit unserer Simulation verbunden ist, um zu lernen, wie man eine Echtzeit-Pipeline zur Streaming-Datenverarbeitung entwickelt.
Speichere die Ausgabe in einer Datei oder Datenbank und verpacke die Daten dann in einer praktischen (aber möglicherweise riskanten) Form, z. B. indem du die Start- und Endzeiten einer bestimmten Fahrt miteinander verbindest, um zu untersuchen, wie sich die Länge einer Taxifahrt zu verschiedenen Tageszeiten verhält.
Neben der Möglichkeit, Hypothesen über die Dynamik eines Taxisystems zu testen, hat die Simulation dieser Daten mehrere Vorteile. Hier sind ein paar Situationen, in denen diese synthetischen Zeitreihendaten nützlich sein könnten:.
Prüfung der Vorzüge verschiedener Prognosemodelle in Bezug auf die bekannte zugrunde liegende Dynamik der Simulation.
Baue eine Pipeline für Daten auf, die du irgendwann auf der Grundlage deiner synthetischen Daten erwartest, während du auf die echten Daten wartest.
Als Zeitreihenanalyst bist du gut beraten, wenn du Generatoren und objektorientierte Programmierung nutzen kannst. Dieses Beispiel ist nur ein Beispiel dafür, wie dieses Wissen dein Leben vereinfachen und die Qualität deines Codes verbessern kann.
Ziehen Sie für umfangreiche Simulationen die agentenbasierte Modellierung in Betracht
Die Lösung, die wir hier kodiert haben, war in Ordnung, aber es war eine ganze Menge Boilerplate, um sicherzustellen, dass die logischen Bedingungen beachtet werden. Wenn eine Simulation von diskreten Ereignissen, die auf den Handlungen diskreter Akteure basieren, eine nützliche Quelle für simulierte Zeitreihendaten wäre, solltest du ein simulationsorientiertes Modul in Betracht ziehen. Das SimPy-Modul ist eine hilfreiche Option, denn es verfügt über eine leicht zugängliche API und bietet viel Flexibilität für die Art von Simulationsaufgaben, die wir in diesem Abschnitt behandelt haben.
Eine Physiksimulation
In einer anderen Art von Simulationsszenario bist du vielleicht im vollen Besitz der physikalischen Gesetze, die ein System definieren. Dabei muss es sich aber nicht um Physik an sich handeln, sondern kann auch auf eine Reihe anderer Bereiche zutreffen:
Quantitative Finanzforscher stellen oft Hypothesen über die "physikalischen" Regeln des Marktes auf. Das tun auch Wirtschaftswissenschaftler, wenn auch auf anderen Zeitskalen.
Psychologen stellen die "psychophysischen" Regeln auf, wie Menschen Entscheidungen treffen. Diese können genutzt werden, um "physikalische" Regeln für die erwarteten menschlichen Reaktionen auf eine Vielzahl von Optionen im Laufe der Zeit zu erstellen.
Biologen erforschen Regeln darüber, wie sich ein System im Laufe der Zeit als Reaktion auf verschiedene Reize verhält.
Ein Beispiel für die Kenntnis einiger Regeln für ein einfaches physikalisches System ist die Modellierung eines Magneten. An diesem Fall werden wir arbeiten, und zwar anhand eines oft gelehrten Modells der statistischen Mechanik, dem Ising-Modell.2 Wir werden uns eine vereinfachte Version ansehen, mit der wir sein Verhalten im Laufe der Zeit simulieren können. Wir werden ein magnetisches Material so initialisieren, dass seine einzelnen magnetischen Komponenten in zufällige Richtungen zeigen. Dann werden wir beobachten, wie sich dieses System unter der Wirkung bekannter physikalischer Gesetze und einiger Codezeilen in eine Ordnung verwandelt, in der alle magnetischen Komponenten in dieselbe Richtung zeigen.
Als Nächstes erörtern wir, wie eine solche Simulation mit Hilfe der Markov Chain Monte Carlo (MCMC)-Methode durchgeführt wird, wobei wir sowohl die Funktionsweise dieser Methode im Allgemeinen als auch ihre Anwendung auf dieses spezielle System erläutern.
In der Physik kann eine MCMC-Simulation zum Beispiel verwendet werden, um zu verstehen, wie Quantenübergänge in einzelnen Molekülen die Gesamtmessungen des Systems im Laufe der Zeit beeinflussen können. In diesem Fall müssen wir ein paar besondere Regeln anwenden:
In einem Markov-Prozess hängt die Wahrscheinlichkeit eines Übergangs zu einem Zustand in der Zukunft nur vom aktuellen Zustand ab (nicht von vergangenen Informationen).
Wir werden eine physikspezifische Bedingung aufstellen, die eine Boltzmann-Verteilung für die Energie erfordert, das heißt, . Für die meisten von uns ist das nur ein Implementierungsdetail und nichts, worüber sich Nicht-Physiker Gedanken machen müssen.
Wir führen eine MCMC-Simulation wie folgt durch:
-
Wähle den Ausgangszustand jeder einzelnen Gitterstelle zufällig.
-
Wähle für jeden einzelnen Zeitschritt eine einzelne Gitterstelle und drehe ihre Richtung um.
-
Berechne die Energieänderung, die sich aus dieser Umdrehung ergeben würde, wenn du die physikalischen Gesetze berücksichtigst, mit denen du arbeitest. In diesem Fall bedeutet das:
-
Wenn die Energieänderung negativ ist, gehst du in einen Zustand mit niedrigerer Energie über, der immer bevorzugt wird, also behältst du den Schalter und gehst zum nächsten Zeitschritt über.
-
Wenn die Energieänderung nicht negativ ist, akzeptierst du sie mit der Akzeptanzwahrscheinlichkeit von . Dies entspricht der Regel 2.
-
Setze die Schritte 2 und 3 unbegrenzt fort, bis du den wahrscheinlichsten Zustand für die jeweilige Gesamtmessung ermittelt hast.
Werfen wir einen Blick auf die Details des Ising-Modells. Stell dir vor, wir haben ein zweidimensionales Material, das aus einem Gitter von Objekten besteht, von denen jedes einen Mini-Magneten hat, der nach oben oder unten zeigen kann. Wir versetzen diese Mini-Magnete zum Zeitpunkt Null zufällig in einen nach oben oder unten gerichteten Spin und zeichnen dann auf, wie sich das System von einem zufälligen Zustand zu einem geordneten Zustand bei niedriger Temperatur entwickelt.3
Zuerst konfigurieren wir unser System wie folgt:
## python
>>>
### CONFIGURATION
>>>
## physical layout
>>>
N
=
5
# width of lattice
>>>
M
=
5
# height of lattice
>>>
## temperature settings
>>>
temperature
=
0.5
>>>
BETA
=
1
/
temperature
Dann gibt es noch einige Hilfsmethoden, wie zum Beispiel die zufällige Initialisierung unseres Startblocks:
>>> def initRandState(N, M): >>> block = np.random.choice([-1, 1], size = (N, M)) >>> return block
Wir berechnen auch die Energie für eine bestimmte Ausrichtung des mittleren Zustands im Verhältnis zu seinen Nachbarn:
## python
>>>
def
costForCenterState
(
state
,
i
,
j
,
n
,
m
):
>>>
centerS
=
state
[
i
,
j
]
>>>
neighbors
=
[((
i
+
1
)
%
n
,
j
),
((
i
-
1
)
%
n
,
j
),
>>>
(
i
,
(
j
+
1
)
%
m
),
(
i
,
(
j
-
1
)
%
m
)]
>>>
## notice the % n because we impose periodic boundary cond
>>>
## ignore this if it doesn't make sense - it's merely a
>>>
## physical constraint on the system saying 2D system is like
>>>
## the surface of a donut
>>>
interactionE
=
[
state
[
x
,
y
]
*
centerS
for
(
x
,
y
)
in
neighbors
]
>>>
return
np
.
sum
(
interactionE
)
Und wir wollen die Magnetisierung des gesamten Blocks für einen bestimmten Zustand bestimmen:
## python
>>>
def
magnetizationForState
(
state
):
>>>
return
np
.
sum
(
state
)
An dieser Stelle führen wir die MCMC-Schritte ein, die wir bereits besprochen haben:
## python
>>>
def
mcmcAdjust
(
state
):
>>>
n
=
state
.
shape
[
0
]
>>>
m
=
state
.
shape
[
1
]
>>>
x
,
y
=
np
.
random
.
randint
(
0
,
n
),
np
.
random
.
randint
(
0
,
m
)
>>>
centerS
=
state
[
x
,
y
]
>>>
cost
=
costForCenterState
(
state
,
x
,
y
,
n
,
m
)
>>>
if
cost
<
0
:
>>>
centerS
*=
-
1
>>>
elif
np
.
random
.
random
()
<
np
.
exp
(
-
cost
*
BETA
):
>>>
centerS
*=
-
1
>>>
state
[
x
,
y
]
=
centerS
>>>
return
state
Um nun eine Simulation durchzuführen, brauchen wir einige Aufzeichnungen sowie wiederholte Aufrufe der MCMC-Anpassung:
## python
>>>
def
runState
(
state
,
n_steps
,
snapsteps
=
None
):
>>>
if
snapsteps
is
None
:
>>>
snapsteps
=
np
.
linspace
(
0
,
n_steps
,
num
=
round
(
n_steps
/
(
M
*
N
*
100
)),
>>>
dtype
=
np
.
int32
)
>>>
saved_states
=
[]
>>>
sp
=
0
>>>
magnet_hist
=
[]
>>>
for
i
in
range
(
n_steps
):
>>>
state
=
mcmcAdjust
(
state
)
>>>
magnet_hist
.
append
(
magnetizationForState
(
state
))
>>>
if
sp
<
len
(
snapsteps
)
and
i
==
snapsteps
[
sp
]:
>>>
saved_states
.
append
(
np
.
copy
(
state
))
>>>
sp
+=
1
>>>
return
state
,
saved_states
,
magnet_hist
Und wir lassen die Simulation laufen:
## python
>>>
### RUN A SIMULATION
>>>
init_state
=
initRandState
(
N
,
M
)
>>>
(
init_state
)
>>>
final_state
=
runState
(
np
.
copy
(
init_state
),
1000
)
Wir können einige Erkenntnisse aus dieser Simulation gewinnen, indem wir uns den Anfangs- und Endzustand ansehen (siehe Abbildung 4-2).
In Abbildung 4-2 sehen wir uns einen zufällig erzeugten Ausgangszustand an. Du könntest zwar erwarten, dass die beiden Zustände stärker vermischt sind, aber erinnere dich daran, dass ein perfekter Schachbretteffekt nicht so wahrscheinlich ist. Wenn du versuchst, den Ausgangszustand mehrmals zu erzeugen, wirst du sehen, dass der scheinbar "zufällige" oder "50/50"-Schachbrettzustand gar nicht so wahrscheinlich ist. Beachte jedoch, dass sich zu Beginn etwa die Hälfte unserer Standorte in jedem Zustand befindet. Mach dir außerdem bewusst, dass alle Muster, die du in den Ausgangszuständen findest, wahrscheinlich darauf zurückzuführen sind, dass dein Gehirn der sehr menschlichen Tendenz folgt, Muster zu sehen, auch wenn es keine gibt.
Dann geben wir den Anfangszustand in die Funktion runState()
ein, lassen 1.000 Zeitschritte verstreichen und untersuchen dann das Ergebnis in Abbildung 4-3.
Dies ist eine Momentaufnahme des Zustands bei Schritt 1.000. An diesem Punkt gibt es mindestens zwei interessante Beobachtungen. Erstens hat sich der dominante Zustand im Vergleich zu Schritt 1.000 umgekehrt. Zweitens ist der dominante Zustand numerisch nicht dominanter als der andere dominante Zustand bei Schritt 1.000. Das deutet darauf hin, dass die Temperatur weiterhin Standorte aus dem vorherrschenden Zustand herauslösen kann, auch wenn dieser sonst favorisiert wird. Um diese Dynamik besser zu verstehen, sollten wir uns überlegen, ob wir nicht die gesamten Messwerte, wie z. B. die Magnetisierung, aufzeichnen oder Filme erstellen, in denen wir unsere zweidimensionalen Daten in einem Zeitreihenformat betrachten können.
Wir machen das mit der Magnetisierung über die Zeit für viele unabhängige Durchläufe der vorherigen Simulation, wie in Abbildung 4-4 dargestellt:
## python
>>>
we
collect
each
time
series
as
a
separate
element
in
results
list
>>>
results
=
[]
>>>
for
i
in
range
(
100
):
>>>
init_state
=
initRandState
(
N
,
M
)
>>>
final_state
,
states
,
magnet_hist
=
runState
(
init_state
,
1000
)
>>>
results
.
append
(
magnet_hist
)
>>>
>>>
## we plot each curve with some transparency so we can see
>>>
## curves that overlap one another
>>>
for
mh
in
results
:
>>>
plt
.
plot
(
mh
,
'r'
,
alpha
=
0.2
)
Die Magnetisierungskurven sind nur ein Beispiel dafür, wie wir uns vorstellen können, wie sich das System im Laufe der Zeit entwickelt. Wir könnten auch die Aufzeichnung von 2D-Zeitreihen als Momentaufnahme des Gesamtzustands zu jedem Zeitpunkt in Betracht ziehen. Es könnte auch andere interessante aggregierte Variablen geben, die wir bei jedem Schritt messen könnten, wie z. B. ein Maß für die Layout-Entropie oder ein Maß für die Gesamtenergie. Größen wie die Magnetisierung oder die Entropie sind verwandte Größen, da sie von der geometrischen Anordnung des Zustands an jedem Gitterplatz abhängen, aber jede Größe ist ein etwas anderes Maß.
Wir können diese Daten auf ähnliche Weise nutzen, wie wir es mit den Taxidaten besprochen haben, auch wenn das zugrunde liegende System ganz anders ist. Wir könnten zum Beispiel:
Nutze die simulierten Daten als Anstoß, um eine Pipeline einzurichten.
Teste die Methoden des maschinellen Lernens an diesen synthetischen Daten, um herauszufinden, ob sie auch bei realen Daten hilfreich sein können, bevor wir uns die Mühe machen, reale Daten für eine solche Modellierung zu bereinigen.
Sieh dir die filmähnlichen Bilder wichtiger Messgrößen an, um ein besseres physikalisches Gespür für das System zu entwickeln .
Abschließende Anmerkungen zu Simulationen
Wir haben uns eine Reihe von sehr unterschiedlichen Beispielen für die Simulation von Messungen angesehen, die das Verhalten im Laufe der Zeit beschreiben. Wir haben uns die Simulation von Daten zum Verbraucherverhalten (Mitgliedschaft in einer NGO und Spenden), zur städtischen Infrastruktur (Abholverhalten von Taxis) und zu physikalischen Gesetzen (die allmähliche Ordnung eines magnetischen Materials nach dem Zufallsprinzip) angesehen. Mit diesen Beispielen solltest du dich wohl genug fühlen, um mit dem Lesen von Codebeispielen für simulierte Daten zu beginnen und auch Ideen zu entwickeln, wie deine eigene Arbeit von Simulationen profitieren könnte.
Wahrscheinlich hast du in der Vergangenheit Annahmen über deine Daten getroffen, ohne zu wissen, wie du diese oder alternative Möglichkeiten testen kannst. Simulationen bieten dir einen Weg, dies zu tun. Das bedeutet, dass deine Gespräche über Daten um hypothetische Beispiele gepaart mit quantitativen Metriken aus Simulationen erweitert werden können. Das schafft eine Grundlage für deine Diskussionen und eröffnet dir neue Möglichkeiten, sowohl im Bereich der Zeitreihen als auch in anderen Bereichen der Datenwissenschaft.
Statistische Simulationen
Statistische Simulationen sind der traditionellste Weg zu simulierten Zeitreihendaten. Sie sind besonders nützlich, wenn wir die zugrundeliegende Dynamik eines stochastischen Systems kennen und einige unbekannte Parameter schätzen oder sehen wollen, wie sich verschiedene Annahmen auf den Parameterschätzungsprozess auswirken würden (wir werden später im Buch ein Beispiel dafür sehen). Auch für physikalische Systeme ist die statistische Simulation manchmal besser.
Statistische Simulationen von Zeitreihendaten sind auch dann sehr wertvoll, wenn wir einen definitiven quantitativen Maßstab brauchen, um unsere eigene Unsicherheit über die Genauigkeit unserer Simulationen zu definieren. Bei traditionellen statistischen Simulationen, wie z. B. einem ARIMA-Modell (das in Kapitel 6 besprochen wird), sind die Formeln für den Fehler gut etabliert, d. h., um ein System mit einem angenommenen zugrunde liegenden statistischen Modell zu verstehen, musst du nicht viele Simulationen durchführen, um numerische Aussagen über Fehler und Varianz zu treffen.
Deep Learning-Simulationen
Deep Learning-Simulationen für Zeitreihen sind ein junges, aber vielversprechendes Feld. Die Vorteile von Deep Learning sind, dass sehr komplizierte, nichtlineare Dynamiken in Zeitreihendaten erfasst werden können, ohne dass der Praktiker die Dynamik vollständig versteht. Dies ist jedoch auch ein Nachteil, da der Praktiker keine prinzipielle Grundlage für das Verständnis der Dynamik des Systems hat.
Deep-Learning-Simulationen sind auch dann vielversprechend, wenn der Schutz der Privatsphäre eine Rolle spielt. Deep Learning wurde zum Beispiel eingesetzt, um synthetische heterogene Zeitreihendaten für medizinische Anwendungen zu erzeugen, die auf realen Zeitreihendaten basieren, aber keine privaten Informationen preisgeben können. Ein solcher Datensatz wäre, wenn er wirklich ohne Datenschutzlecks erzeugt werden kann, von unschätzbarem Wert, weil Forscher/innen dann Zugang zu einer Vielzahl von (ansonsten teuren und die Privatsphäre verletzenden) medizinischen Daten hätten.
Mehr Ressourcen
- Cristóbal Esteban, Stephanie L. Hyland und Gunnar Rätsch, "Real-Valued (Medical) Time Series Generation with Recurrent Conditional GANs", unveröffentlichtes Manuskript, zuletzt überarbeitet am 4. Dezember 2017, https://perma.cc/Q69W-L44Z.
Die Autoren zeigen, wie generative adversarische Netzwerke verwendet werden können, um realistisch aussehende heterogene medizinische Zeitreihendaten zu erzeugen. Dies ist ein Beispiel dafür, wie Deep-Learning-Simulationen genutzt werden können, um ethische, rechtliche und (hoffentlich) datenschutzkonforme medizinische Datensätze zu erstellen, die einen breiteren Zugang zu nützlichen Daten für maschinelles Lernen und Deep Learning im Gesundheitswesen ermöglichen.
- Gordon Reikard und W. Erick Rogers, "Forecasting Ocean Waves: Comparing a Physics-based Model with Statistical Models", Coastal Engineering 58 (2011): 409-16, https://perma.cc/89DJ-ZENZ.
Dieser Artikel bietet einen verständlichen und praxisnahen Vergleich zweier drastisch unterschiedlicher Methoden zur Modellierung eines Systems, nämlich mit Physik oder mit Statistik. Die Forscher kommen zu dem Schluss, dass für das jeweilige Problem die Zeitskala, die für den Prognostiker von Interesse ist, ausschlaggebend dafür sein sollte, welches Paradigma er anwendet. In diesem Artikel geht es zwar um Prognosen, aber die Simulation ist eng damit verbunden und die gleichen Erkenntnisse gelten auch hier.
- Wolfgang Härdle, Joel Horowitz und Jens-Peter Kreiss, "Bootstrap Methods for Time Series," International Statistical Review / Revue Internationale de Statistique 71, no. 2 (2003): 435-59, https://perma.cc/6CQA-EG2E.
Ein klassischer Bericht aus dem Jahr 2005 über die Schwierigkeiten der statistischen Simulation von Zeitreihendaten angesichts zeitlicher Abhängigkeiten. Die Autoren erklären in einer hochtechnischen Statistikzeitschrift, warum die Methoden zum Bootstrappen von Zeitreihendaten hinter den Methoden für andere Arten von Daten zurückbleiben und welche vielversprechenden Methoden zum Zeitpunkt der Abfassung verfügbar waren. Der Stand der Technik hat sich nicht allzu sehr verändert, daher ist dies eine nützliche, wenn auch anspruchsvolle Lektüre.
1 Dieses Beispiel ist stark von Luciano Ramalhos Buch " Fluent Python" (O'Reilly 2015) inspiriert. Ich empfehle dir dringend, das komplette Simulationskapitel in diesem Buch zu lesen, um deine Python-Programmierkenntnisse zu verbessern und mehr Möglichkeiten für agentenbasierte Simulationen zu sehen.
2 Das Ising-Modell ist ein bekanntes und häufig gelehrtes klassisches statistisch-mechanisches Modell von Magneten. Wenn du mehr darüber erfahren möchtest, findest du online viele Code-Beispiele und weitere Diskussionen zu diesem Modell, sowohl im Programmier- als auch im Physikkontext.
3 Das Ising-Modell wird häufiger verwendet, um zu verstehen, was der Gleichgewichtszustand eines Ferromagneten ist, als um den zeitlichen Aspekt zu betrachten, wie ein Ferromagnet seinen Weg in den Gleichgewichtszustand finden kann. Wir betrachten die Entwicklung im Laufe der Zeit jedoch als Zeitreihe.
Get Praktische Zeitreihenanalyse now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.