paint-brush
Umgang mit fehlenden Daten in Finanzzeitreihen – Rezepte und Fallstrickevon@vkirilin
12,436 Lesungen
12,436 Lesungen

Umgang mit fehlenden Daten in Finanzzeitreihen – Rezepte und Fallstricke

von Vladimir Kirilin10m2024/04/03
Read on Terminal Reader

Zu lang; Lesen

Ich konzentriere mich auf Methoden zum Umgang mit fehlenden Daten in Finanzzeitreihen. Anhand einiger Beispieldaten zeige ich, dass LOCF im Vergleich zu Dropping und Imputation normalerweise eine gute Methode ist, aber seine Fehler hat – d. h. es kann künstliche, unerwünschte Sprünge in den Daten erzeugen. Alternativen wie Interpolation haben jedoch ihre eigenen Probleme, insbesondere im Zusammenhang mit Live-Vorhersagen/-Prognosen.
featured image - Umgang mit fehlenden Daten in Finanzzeitreihen – Rezepte und Fallstricke
Vladimir Kirilin HackerNoon profile picture
0-item

Wenn Sie wie ich sind, haben Sie mindestens einmal mit fehlenden Daten in Ihren Datensätzen zu tun gehabt. Oder zweimal. Oder einmal zu oft ...


Um mit diesen lästigen NAs fertig zu werden, reicht es manchmal aus, sie zu löschen, also die Zeilen mit fehlenden Daten zu entfernen. Dies ist jedoch möglicherweise nicht immer optimal, insbesondere für Zeitreihendaten und noch mehr für Finanzdaten. Natürlich ist dieses Problem gut untersucht, so dass es viele Alternativen zum Wegwerfen gibt.


Ich werde mir einige davon ansehen (unten aufgeführt) und die Vor- und Nachteile besprechen:


  1. Fallenlassen

  2. LOCF (letzte übertragene Beobachtung)

  3. Mittlere (oder ähnliche) Imputation

  4. Interpolation


Spoiler-Alarm: Es gibt keinen einheitlichen Ansatz! Ich werde argumentieren, dass LOCF normalerweise eine gute Wahl für die Finanzierung ist, aber auch nicht ohne Nachteile. Lassen Sie mich vor diesem Hintergrund die Methoden und Daten beschreiben, die ich verwenden werde, um sie vorzustellen.


Hinweis: Wenn man pedantisch sein möchte, sind alle Methoden 2–4 Beispiele für eine gewisse Unterstellung.


Modelldaten

Beginnen wir mit einigen Beispielen, warum man sich überhaupt für einen Abbruch interessieren würde. Zur Veranschaulichung habe ich einige stark vereinfachte tägliche Aktienkursdaten erstellt, unter der Annahme, dass sie einem Zufallspfad ohne Drift folgen (dh der durchschnittliche langfristige Preis sollte konstant bleiben) – nicht die genaueste, aber dennoch eine harmlose Annahme.


 np.random.seed(10) # needed for reproducibility price_moves = 3*pd.Series(np.random.randn(100)) # generating random "steps" with 0 mean price_vec = 100 + price_moves.cumsum() # generating brownian motion price_vec.plot() 


unveränderte Preisreihe


Nun, die Handlung sieht ziemlich gut gemeint aus.


Angenommen, wir wollen nun den empirischen Mittelwert der täglichen Preisunterschiede ermitteln –

 price_vec.diff().mean() #sample mean >0.20030544816842052

Offensichtlich ungleich Null, im Gegensatz zur Erzeugungsreihe – aber das ist nur Beispielrauschen. So weit, ist es gut.


Daten werden gelöscht

Lassen Sie uns diese Daten nun ein wenig verformen, indem wir einige Datenpunkte löschen:

 price_vec_na_simple = price_vec.copy() price_vec_na_simple.iloc[90:95] = np.array([np.NaN, np.NaN, np.NaN, np.NaN, np.NaN]) # price_vec_na_simple.diff().mean() >0.1433356258183252


Ein paar Dinge fallen uns sofort auf -

  1. Der Mittelwert ist irgendwie nicht NA, obwohl der diff Vektor eindeutig NAs enthält

  2. Der Mittelwert unterscheidet sich von dem, den wir zuvor erhalten haben


Nun ist Nr. 1 ganz einfach: pd.mean entfernt NA standardmäßig automatisch.

Aber was ist mit #2? Überdenken wir, was wir berechnen.


Es lässt sich leicht zeigen, dass der mittlere Preisunterschied zumindest ohne NAs einfach (price_vec[99]-price_vec[0])/99 betragen sollte – tatsächlich heben sich bei der Summierung der Preisunterschiede alle „mittleren“ Teile auf, sozusagen (price_vec[1] - price_vec[0]) + (price_vec[2] - price_vec[1]) + .. !


Nachdem wir nun die fehlenden Daten eingefügt haben und wir zunächst die Differenzen berücksichtigen und dann NA -Werte weglassen, wird diese Aufhebung unterbrochen. Eine einfache Rechnung zeigt, dass Sie jetzt (price_vec[99] - price_vec[0] - price_vec[95] + price_vec[89])/93 berechnen (price_vec[99] - price_vec[0] - price_vec[95] + price_vec[89])/93 .


Um dies zu veranschaulichen, beachten Sie, dass die folgenden beiden Begriffe jetzt weggelassen wurden – price_vec[95] - price_vec[94] und price_vec[90] - price_vec[89] , da (NA - any number) zu NA ausgewertet wird und dann gelöscht wird.


Lassen Sie uns dies überprüfen:

 (price_vec[99] - price_vec[0])/99 >0.20030544816842052 (price_vec[99] - price_vec[0] - price_vec[95] + price_vec[89])/93 >0.1433356258183252


Jetzt wird klarer, wie wir die Dinge in Ordnung bringen können – wir müssen zuerst NAs entfernen und dann diff

 price_vec_na_simple.dropna().diff().mean() >0.21095999328376203


Der Mittelwert ist fast wieder da, wo er sein sollte – eine kleine Abweichung entsteht, weil wir jetzt weniger Terme im Mittel haben – 94 statt 99.


Ok, sieht es so aus, als ob wir dropna verwenden können, wenn uns nur der Mittelwert am Herzen liegt (solange wir es richtig machen)? Schließlich liegt der Unterschied zwischen 0.2 und 0.21 eindeutig innerhalb unserer Lärmtoleranz. Na ja, nicht ganz – mal sehen, warum.


LOCF

Was ist LOCF?

LOCF steht für Last Observation Carried Forward. Die Idee dahinter ist super einfach: Wenn ich Daten in bestimmten Zeitintervallen aufzeichne, die regelmäßig sein können oder auch nicht, und wenn die Beobachtung eines bestimmten Intervalls fehlt, gehen wir einfach davon aus, dass sich an unserer Variablen nichts geändert hat, und ersetzen sie durch die letzte Nicht-Variable. fehlender Wert (zum Beispiel - [3, 5, NA, 8] → [3, 5, 5, 8]). Man könnte sich fragen: Warum sollte man sich überhaupt um ein Intervall mit einer fehlenden Beobachtung kümmern, es also nicht einfach löschen, wie bei der „Dropping“-Methode? Nun, die Antwort liegt in dem inhärenten Defekt des „Ablegens“, den ich oben nicht erwähnt habe.


Angenommen, Sie zeichnen mehrere Mengen gleichzeitig auf, insbesondere solche, die sich normalerweise nicht allzu schnell ändern – wie etwa stündliche Aufzeichnungen von Temperatur und Luftfeuchtigkeit. Angenommen, Sie haben beide Werte für 10:00, 11:00 und 12:00 Uhr, aber nur die Luftfeuchtigkeit um 13:00 Uhr. Löschen Sie einfach diese „Zeile“ – tun Sie also so, als hätten Sie für 13:00 Uhr keine Lesung? Nun, das ist in Ordnung, wenn Sie nur zwei Variablen haben – auch wenn Sie gerade einige potenziell wertvolle Informationen entfernt haben (die Luftfeuchtigkeit um 13:00 Uhr). Wenn Sie jedoch über viele solcher Vorkommnisse oder viele Variablen gleichzeitig verfügen, führt das Weglassen möglicherweise dazu, dass Sie praktisch überhaupt keine Daten mehr haben!


Eine sehr attraktive Alternative besteht darin, einfach anzunehmen, dass sich an der Temperatur zwischen 12:00 und 13:00 Uhr nichts geändert hat. Denn wenn jemand um 12:30 Uhr zu uns käme und uns fragte: „Wie hoch ist die aktuelle Temperatur“, hätten wir zu Recht mit dem Messwert von 12:00 Uhr geantwortet (wenn wir nicht sofort einen neuen Messwert erhalten könnten). ). Warum nicht die gleiche Logik für den 13:00-Wert verwenden?


Warum LOCF verwenden (im Finanzwesen)?

Lassen Sie uns zunächst unseren neuen Ansatz anhand der Daten von früher testen:

 price_vec_na_simple.ffill().diff().mean() # ffill performs LOCF by default >0.20030544816842052


Sieht so aus, als hätten wir unseren alten Wert genau wiedererlangt! Wenn Sie außerdem weitere Untersuchungen zu den Preisunterschiedsdaten durchführen möchten, sehen diese jetzt „geordneter“ aus, da für jeden Tag ein Eintrag vorhanden ist, obwohl fünf dieser Einträge jetzt 0 sind (warum? Versuchen Sie, price_vec_na_simple.ffill().diff().iloc[90:95] “ auszuführen price_vec_na_simple.ffill().diff().iloc[90:95] um es selbst zu sehen).


Darüber hinaus kommen im Finanzwesen häufig fehlende Daten und Ausreißerdaten zusammen. Lassen Sie mich das veranschaulichen:

 #inflate two observations, delete three next ones price_moves_na[90] += 20 price_moves_na[91] += 30 price_moves_na[92] -= 50 # to "deflate" the price shock back price_vec_na = (100 + price_moves_na.cumsum()) price_vec_na[92:95] = [np.NaN, np.NaN, np.NaN] price_vec_na.tail(20).plot() price_vec_na.diff().dropna().mean() >0.7093365245831178 

Spitze + fehlende Daten


Wir können sehen, dass nach einem starken Preisanstieg die Daten einfach drei Tage lang nicht verfügbar sind. Dies ist kein so „künstliches“ Beispiel, wie es klingen mag! Stellen Sie sich vor, der Handel würde nach dem Anstieg gestoppt, zumindest an dieser bestimmten Börse. Dann beruhigten sich die Dinge etwas, sodass der Preis wieder in den normalen Bereich zurückkehrte. Vielleicht war es etwas Allmähliches Was sich hinter den Kulissen abspielt, das tatsächlich die Verbindung zwischen der Spitze und der Zeit nach der Spitze herstellt, beruhigt sich. Aber Sie wissen das nicht und haben keine Daten dafür!


Was ist die natürlichste Annahme, wenn wir keine neuen Daten haben? Denken Sie daran, dass unser Datengenerierungsmodell im Wesentlichen auf Preisänderungen basierte. Wenn es also keine neuen Daten gibt, ändert sich der Preis vielleicht überhaupt nicht? Genau davon geht LOCF (Last Observation Carried Forward) aus.

Etwas Mathematik für den Kontext

Eine Randbemerkung für einen neugierigen Leser: Eine vielleicht grundlegendere Ansicht darüber, warum LOCF für Aktienkursdaten besonders geeignet ist, besteht darin, dass es normalerweise als Martingal modelliert wird. Grob gesagt ist ein Martingal etwas, bei dem unsere beste Schätzung für morgen das ist, was wir heute sehen, oder E[x_{t+1} | x_t] = x_t


Ok, zurück zu den tatsächlichen Daten! Lassen Sie uns die Konsequenzen von LOCF sowohl visuell als auch numerisch untersuchen:

 price_vec_na.ffill().tail(20).plot() price_vec_na.ffill().diff().mean() >0.20030544816842052 

LOCF-Imputation


Wir sehen sofort die Vor- und Nachteile von LOCF (im wahrsten Sinne des Wortes)! Zum einen liegt der Mittelwert wieder dort, wo wir ihn „erwarten“ – also beim unveränderten Erfahrungswert. Allerdings führen wir eine ziemlich hässliche Phase ein, in der der Preis nicht mehr dem „typischen“ entspricht, und einen künstlichen Preisverfall zwischen den Tagen 94 und 95.

Was ist mit der Anrechnung?

Vergleichen wir die Ergebnisse, die wir von LOCF erhalten, mit der (mittleren) Imputation. Dies ist eine sehr häufige Wahl für die NA-Verarbeitung, insbesondere für Nicht-Zeitreihendaten. Wenn es jedoch naiv durchgeführt wird, hat es bei der Verwendung für Finanzdaten viele Nachteile.


  • Wenn Sie nur den Mittelwert aller Stichproben verwenden, führt dies zu einer offensichtlichen Vorausschau-Verzerrung, dh Sie verwenden zukünftige Daten, um vergangene Werte zu imputieren.

  • Die Verwendung eines Rückblicks oder eines gleitenden Mittelwerts ist sicherlich besser – allerdings kann es manchmal zu Konflikten mit der zuvor beschriebenen Martingal-„Grundlinien“-Ansicht kommen.


Schauen wir uns das etwas genauer an. Ich werde die Rückblickimputation auf unsere alten Daten anwenden –

 price_vec_na_impute = price_vec_na.copy() price_vec_na_impute[price_vec_na_impute.isna()] = price_vec_na.iloc[:90].mean() price_vec_na_impute.diff().mean() >0.20030544816842052 

mittlere Unterstellung

Wir ermitteln den „richtigen“ Preisänderungsmittelwert, genau wie LOCF. ABER wir führen zwischen den Tagen 91 und 92 einen künstlichen Preisverfall ein, der in mancher Hinsicht noch schlimmer ist als der, den wir zuvor hatten. Schließlich geschah das, als oder nachdem sich die Dinge beruhigt hatten, während dieses einfach davon ausgeht, dass sich alles sofort wieder normalisiert. Abgesehen davon kann es in der Praxis etwas schwierig sein, das Rückschaufenster so auszubalancieren, dass wir a) aktuelle Trends, aber auch b) langfristige Tendenzen erfassen (der übliche Kompromiss zwischen Bias und Varianz).


Hinzufügen einer zweiten Variablen

Angenommen, wir möchten nun eine komplexere Aufgabe ausführen – die Korrelation zwischen den Preisbewegungen zweier Vermögenswerte aus den empirischen Daten extrahieren, wenn für eine oder beide Preisreihen Daten fehlen. Natürlich können wir Dropping immer noch verwenden, aber:


  • Selbst wenn wir es nutzen können, ist es optimal?

  • Was wäre, wenn wir viele Variablen hätten? Dann könnte das Weglassen aller Zeilen mit mindestens einer NA dazu führen, dass wir überhaupt keine Daten mehr haben!


Es gibt viele Gründe, warum man die Korrelation berechnen möchte – es ist der erste Schritt der EDA in fast jedem Multivariablenmodell, es wird ziemlich häufig bei jeder Art von Portfoliokonstruktion verwendet und so weiter. Daher ist es unbedingt erforderlich, diese Zahl so genau wie möglich zu messen!


Zur Veranschaulichung generieren wir eine zweite Variable mit einer „eingebauten“ Korrelation von 0,4 zur ersten. Dazu verwenden wir eine Art Gaußsches Mischungsmodell . Das Bild, das man sich vorstellen kann, ist das von zwei korrelierten Aktien, die einen wichtigen Risikofaktor gemeinsam haben, aber die zweite Aktie ist auch einem großen Risikofaktor ausgesetzt, den die erste nicht hat. Denken Sie zum Beispiel an Google und Facebook – der erste Faktor könnte eine allgemeine Stimmung gegenüber dem Technologiesektor sein und der zweite könnte der Wettbewerb mit konkurrierenden sozialen Netzwerken sein.


 np.random.seed(2) # needed to ensure a fixed second series price_moves_2 = pd.Series(np.random.randn(100)) price_vec_2 = 50+(0.4*price_moves/3 + np.sqrt(1-0.4**2)*price_moves_2).cumsum() # all this math to ensure we get a 0.4 "theoretical" correlation with the first one


Lassen Sie uns die empirische „Grundkorrelation“ überprüfen, also ohne die NAs und Sprünge.

 pd.concat([price_vec, price_vec_2], axis = 1).diff().corr().iloc[0,1] >0.4866403018044526


Dies kommt der „theoretischen“ Korrelation einigermaßen nahe – es ist bekannt, dass die empirische Korrelationsmessung anfällig für ein ziemlich großes Rauschen ist.


NAs ohne Ausreißer

Als nächsten Schritt untersuchen wir den Fall mit NAs, aber ohne Ausreißer. Wir werden auch vergleichen, was passiert, wenn wir na vor und nach diff dropna

 pd.concat([price_vec_na_simple, price_vec_2], axis = 1).diff().corr().iloc[0,1] # implicit dropna after diff >0.5022675176281746 pd.concat([price_vec_na_simple, price_vec_2], axis = 1).dropna().diff().corr().iloc[0,1] >0.5287405341268966


Beide Ergebnisse liegen ziemlich nah beieinander und nicht allzu weit von dem „empirischen“ Wert entfernt, den wir zuvor erhalten haben. Lassen Sie uns überprüfen, ob LOCF und Imputation ebenfalls ordnungsgemäß funktionieren:

 pd.concat([price_vec_na_simple, price_vec_2], axis = 1).ffill().diff().corr().iloc[0,1] >0.5049380499525835 price_vec_na_simple_impute = price_vec_na_simple.copy() price_vec_na_simple_impute[price_vec_na_simple_impute.isna()] = price_vec_na_simple_impute.iloc[:90].mean() pd.concat([price_vec_na_simple_impute, price_vec_2], axis = 1).ffill().diff().corr().iloc[0,1] >0.4866728183859715


Beim Vergleich der oben genannten 4 Ergebnisse sehen wir, dass alle Methoden recht gut funktionieren. Vielleicht sollten wir dann dasselbe für den Ausreißerfall erwarten?


NAs mit Ausreißern

Denken Sie daran: Um konsistent zu bleiben, müssen wir die zweite Preisreihe denselben Preisschocks aussetzen, die die erste erlebte, jedoch ohne die folgenden NAs. Um auf das obige Beispiel zurückzukommen: Stellen Sie sich ein Großereignis vor, das einen Anstieg des ersten Risikofaktors verursacht, der schließlich den Handel mit dem ersten Vermögenswert stoppt. Der zweite Vermögenswert wird diese zwar auch erleben, aber vielleicht in einem geringeren Ausmaß, und daher würde kein Anhalten stattfinden und somit auch keine NAs.


 price_vec_na_2 = 50+(0.4*price_moves_na/3 + np.sqrt(1-0.4**2)*price_moves_2).cumsum()


Vergleichen wir noch einmal die Leistung aller unserer Methoden –

 pd.concat([price_vec_na, price_vec_na_2], axis = 1).diff().corr().iloc[0,1] >0.6527112906179914 pd.concat([price_vec_na, price_vec_na_2], axis = 1).dropna().diff().corr().iloc[0,1] >0.7122391279139506


Das ist sowohl vom theoretischen als auch vom empirischen Wert her ein ziemlicher Unterschied! Wie wäre es mit LOCF und Imputation?

 pd.concat([price_vec_na, price_vec_na_2], axis = 1).ffill().diff().corr().iloc[0,1] >0.33178239830519984 pd.concat([price_vec_na_impute, price_vec_na_2], axis = 1).dropna().diff().corr().iloc[0,1] >0.7280990594963112


Jetzt sehen wir endlich, was LOCF wert ist! Es übertrifft alle anderen Methoden deutlich!


Mögliche Fallstricke + Interpolation

Natürlich ist dies nicht 100 % robust. Zum einen führen wir durch LOCF zu einem starken Preisverfall, wenn die fehlenden Daten enden. Wenn es mit einigen Ausreißern auf dem zweiten Preisvektor übereinstimmt, könnten sich die Ergebnisse erheblich ändern. (*Eine Übung für den Leser: Drehen Sie das Vorzeichen der Preisbewegung von price_vec_na_2[95] um und prüfen Sie, wie sich dies auf die Ergebnisse auswirkt.) Es ist nicht ganz klar, ob es „sauber“ ist, einfach nur diesen Preisrückgang einzuführen, anstatt z. B. zwischen dem Preishöchstwert price_vec_na[91] und dem „normalen“ Wert danach price_vec_na[95] zu interpolieren. Insbesondere bei einer „Live“-Nutzung ist eine Interpolation jedoch nicht wirklich möglich! Denn wenn heute Tag Nr. 93 ist, wie können wir dann mit einem zukünftigen Wert interpolieren, der am Ende von Tag Nr. 95 aufgezeichnet wurde? Für eine historische Studie – klar, das bleibt eine Option, aber dann bleibt unklar, wie man es interpretieren und für tatsächliche Prognosen nutzen soll! Zusammenfassend lässt sich sagen, dass eine Interpolation über die Zeitdimension möglich, aber etwas fragwürdiger ist.


Schlussfolgerungen

Ich habe versucht, eine kleine Fallstudie zu geben, um vorzustellen und zu begründen, warum LOCF oft die attraktivste und einfachste Option für den Umgang mit fehlenden Daten in Finanzzeitreihen ist.


Um es noch einmal zusammenzufassen: Die Vorteile sind:

  • Attraktiv aus Martingal-/Informationsfluss-Perspektive
  • Super einfach umzusetzen
  • Keine Notwendigkeit zur Optimierung (im Gegensatz zur Imputation)
  • Behandelt Ausreißer zumindest anständig


Einige Nachteile:

  • Kann am Ende des fehlenden Zeitraums möglicherweise zu großen Sprüngen führen
  • Bei Verwendung für mehrere Variablen könnten einige nuancierte Gelenkdynamiken fehlen


Als Quant in einem Prop-Trading-Shop verwende ich es für fast alle meine Studien als effektive Grundlage. Manche Situationen erfordern natürlich differenziertere Maßnahmen, aber diese sind selten und werden in der Regel auch mit keiner der drei anderen genannten Methoden wirklich zu 100 % „gelöst“.