paint-brush
Gérer les données manquantes dans les séries temporelles financières – Recettes et piègespar@vkirilin
12,436 lectures
12,436 lectures

Gérer les données manquantes dans les séries temporelles financières – Recettes et pièges

par Vladimir Kirilin10m2024/04/03
Read on Terminal Reader

Trop long; Pour lire

Je me concentre sur les méthodes permettant de gérer les données manquantes dans les séries temporelles financières. À l'aide de quelques exemples de données, je montre que LOCF est généralement une méthode de choix par rapport à l'abandon et à l'imputation, mais qu'elle a ses défauts - c'est-à-dire qu'elle peut créer des sauts artificiels indésirables dans les données. Cependant, les alternatives telles que l'interpolation ont leurs propres problèmes, en particulier dans le contexte de la prédiction/prévision en direct.
featured image - Gérer les données manquantes dans les séries temporelles financières – Recettes et pièges
Vladimir Kirilin HackerNoon profile picture
0-item

Si vous êtes comme moi, vous avez au moins une fois traité des données manquantes dans vos ensembles de données. Ou deux fois. Ou une fois de trop…


Parfois, tout ce qu'il faut pour gérer ces NA embêtants, c'est de les supprimer, c'est-à-dire de supprimer les lignes contenant des données manquantes. Cependant, cela n’est pas toujours optimal, en particulier pour les données de séries chronologiques, et encore plus pour les données financières. Bien sûr, ce problème est bien étudié, il existe donc de nombreuses alternatives au abandon.


J'en examinerai quelques-uns (énumérés ci-dessous) et discuterai des avantages et des inconvénients :


  1. Goutte

  2. LOCF (dernière observation reportée)

  3. Imputation moyenne (ou similaire)

  4. Interpolation


Alerte spoiler : il n’existe pas d’approche universelle ! Je soutiendrai que LOCF est généralement un bon choix en matière de financement, mais non sans inconvénients. Dans cet esprit, permettez-moi de décrire les méthodes et les données que j'utiliserai pour les présenter.


Remarque : si l'on veut être pédant, toutes les méthodes 2 à 4 sont des exemples d' imputation.


Données du modèle

Commençons par quelques exemples expliquant pourquoi on voudrait abandonner en premier lieu. Pour illustrer, j'ai généré des données quotidiennes trop simplifiées sur le cours des actions en supposant qu'elles suivent une marche aléatoire sans dérive (c'est-à-dire que le prix moyen à long terme devrait rester constant) - ce n'est pas l'hypothèse la plus précise mais néanmoins bénigne.


 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() 


série de prix inchangée


Eh bien, l’intrigue semble plutôt bien intentionnée.


Supposons que nous voulions maintenant connaître la moyenne empirique des différences de prix quotidiennes :

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

Évidemment non nul, contrairement à la série génératrice - mais ce n'est qu'un bruit d'échantillon. Jusqu'ici, tout va bien.


Suppression de données

Déformons maintenant un peu ces données en supprimant quelques points de données :

 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


Nous remarquons tout de suite quelques choses -

  1. La moyenne est en quelque sorte non-NA même si le vecteur diff contiendra clairement des NA

  2. La moyenne est différente de celle obtenue précédemment


Maintenant, le n ° 1 est assez simple - pd.mean supprime automatiquement NA par défaut.

Mais qu'en est-il du numéro 2 ? Repensons ce que nous calculons.


Il est facile de montrer qu'au moins sans NA, la différence de prix moyenne devrait simplement être (price_vec[99]-price_vec[0])/99 - en effet, lorsque nous additionnons les différences de prix, toutes les pièces « intermédiaires » s'annulent, comme ceci. (price_vec[1] - price_vec[0]) + (price_vec[2] - price_vec[1]) + .. !


Maintenant, avec les données manquantes insérées, si nous prenons d'abord les différences puis supprimons NA , cette annulation est interrompue - quelques calculs simples montrent que vous êtes maintenant en train de calculer (price_vec[99] - price_vec[0] - price_vec[95] + price_vec[89])/93 .


Pour montrer cela, notez que les deux termes suivants sont désormais omis - price_vec[95] - price_vec[94] et price_vec[90] - price_vec[89] , puisque (NA - any number) est évalué à NA et est ensuite supprimé.


Vérifions ceci :

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


Maintenant, il devient plus clair comment nous pouvons arranger les choses - nous devons d'abord supprimer les NA, puis diff -

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


La moyenne est presque revenue à ce qu'elle devrait être - un léger écart se produit car nous avons maintenant moins de termes dans la moyenne - 94 au lieu de 99.


Ok, on dirait que si nous ne nous soucions que de la moyenne, tout va bien avec dropna (tant que nous le faisons correctement) ? Après tout, la différence entre 0.2 et 0.21 se situe clairement dans notre tolérance au bruit. Eh bien, pas tout à fait – voyons pourquoi.


LOCF

Qu’est-ce que le LOCF ?

LOCF signifie Dernière Observation reportée. L'idée derrière cela est très simple : si j'enregistre des données à certains intervalles de temps, qui peuvent être réguliers ou non, si une observation d'un intervalle particulier manque, nous supposons simplement que rien n'a changé avec notre variable et la remplaçons par la dernière non- valeur manquante (par exemple - [3, 5, NA, 8] → [3, 5, 5, 8]). On peut se demander : pourquoi se soucier d'un intervalle avec une observation manquante en premier lieu, c'est-à-dire ne pas simplement le supprimer comme dans la méthode du « dropping » ? Eh bien, la réponse réside dans le défaut inhérent au « dropping » que je n'ai pas mentionné ci-dessus.


Supposons que vous enregistriez plusieurs quantités à la fois, en particulier celles qui ne changent généralement pas trop et trop rapidement, comme les enregistrements horaires de la température et de l'humidité. Supposons que vous ayez les deux valeurs pour 10h00, 11h00 et 12h00, mais uniquement l'humidité à 13h00. Supprimez-vous simplement cette « ligne » - c'est-à-dire faites comme si vous n'aviez pas de lecture pour 13h00 ? Eh bien, ce n'est pas grave si vous n'avez que deux variables - même si vous venez de supprimer certaines informations potentiellement précieuses (l'humidité de 13h00). Mais si vous avez plusieurs occurrences de ce type ou plusieurs variables à la fois, la suppression pourrait vous laisser pratiquement aucune donnée !


Une alternative très intéressante consiste simplement à supposer que rien n’a changé pour la température entre 12h00 et 13h00. Après tout, si quelqu'un venait nous voir à 12h30 et nous demandait « quelle est la température actuelle », nous aurions légitimement répondu avec la lecture de 12h00 (si nous ne pouvons pas obtenir une nouvelle lecture immédiatement, bien sûr). ). Pourquoi ne pas utiliser la même logique pour la valeur 13h00 ?


Pourquoi utiliser LOCF (en finance) ?

Tout d’abord, testons notre nouvelle approche sur les données précédentes :

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


On dirait que nous avons récupéré notre ancienne valeur justement ! De plus, si vous souhaitez effectuer des recherches plus approfondies sur les données de différence de prix, elles semblent désormais plus « ordonnées » car elles comportent une entrée pour chaque jour, même si cinq de ces entrées sont désormais à 0 (pourquoi ? essayez d'exécuter price_vec_na_simple.ffill().diff().iloc[90:95] pour voir par vous-même).


En outre, en finance, les données manquantes et les données aberrantes se rejoignent souvent. Permettez-moi d'illustrer cela :

 #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 

pic + données manquantes


Nous pouvons constater qu’après une forte hausse des prix, les données ne sont tout simplement pas disponibles pendant 3 jours consécutifs. Ce n’est pas un exemple aussi « artificiel » que cela puisse paraître ! Imaginez que les échanges s’arrêtent après le pic, du moins sur cette bourse en particulier. Ensuite, les choses se sont un peu stabilisées, et le prix est donc revenu à son régime normal. ce qui se passe dans les coulisses qui a réellement « relié » les points entre le pic et le calme post-pic. Mais vous ne le savez pas et n'avez aucune donnée pour cela !


Quelle est l’hypothèse la plus naturelle si nous n’avons pas de nouvelles données ? Eh bien, rappelez-vous que notre modèle de génération de données était fondamentalement basé sur les changements de prix. Donc, s’il n’y a pas de nouvelles données, peut-être que le prix ne change pas du tout ? C’est exactement ce que suppose LOCF (Last Observation Carried Forward).

Quelques mathématiques pour le contexte

Une remarque pour un lecteur curieux - peut-être une vision plus fondamentale de la raison pour laquelle LOCF est particulièrement adapté aux données sur le cours des actions est qu'il est généralement modélisé comme une martingale . En gros, une martingale est quelque chose pour lequel notre meilleure estimation pour demain est ce que nous voyons aujourd'hui, ou E[x_{t+1} | x_t] = x_t


Ok, revenons aux données réelles ! Examinons les conséquences de LOCF à la fois visuellement et numériquement :

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

Imputation LOCF


Immédiatement, nous voyons les avantages et les inconvénients du LOCF (littéralement) ! D'une part, la moyenne est revenue là où nous « nous attendons » à ce qu'elle soit, c'est-à-dire la valeur empirique inchangée. Cependant, nous introduisons une période assez moche où le prix est hors ligne avec le « typique » et une baisse artificielle du prix entre les jours 94 et 95.

Qu’en est-il de l’imputation ?

Comparons les résultats que nous obtenons de LOCF avec l'imputation (moyenne). Il s'agit d'un choix très courant pour la gestion des NA, en particulier pour les données non chronologiques. Cependant, si elle est réalisée naïvement, elle présente de nombreux inconvénients lorsqu’elle est utilisée pour les données financières.


  • Si vous utilisez simplement la moyenne de tous les échantillons, vous introduisez un biais prospectif évident - c'est-à-dire que vous utilisez des données futures pour imputer des valeurs passées.

  • Il est certainement préférable d’utiliser une sorte de rétrospection ou de moyenne mobile – cependant, cela peut parfois entrer en conflit avec la vision de « référence » de la martingale que nous avons décrite précédemment.


Examinons cela un peu plus en détail. J'utiliserai l'imputation rétrospective sur nos anciennes données -

 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 

imputation moyenne

Nous récupérons la moyenne de variation de prix « correcte », identique à LOCF. MAIS nous introduisons une baisse de prix artificielle entre les jours 91 et 92 qui, à certains égards, est encore pire que celle que nous avions auparavant. Après tout, celui-ci s’est produit lorsque ou après que les choses se soient probablement calmées, alors que celui-ci suppose simplement que tout revient à la normale immédiatement. En dehors de cela, en pratique, il peut être quelque peu difficile d’équilibrer la fenêtre rétrospective de telle sorte que nous a) capturions les tendances récentes mais aussi b) capturions les tendances à long terme (le compromis habituel biais-variance).


Ajout d'une deuxième variable

Supposons que nous souhaitions maintenant effectuer une tâche plus complexe : extraire la corrélation entre les mouvements de prix de deux actifs à partir des données empiriques, lorsqu'il manque des données dans l'une ou les deux séries de prix. Bien sûr, nous pouvons toujours utiliser le drop, mais :


  • même si on peut l’utiliser, est-ce optimal ?

  • que se passe-t-il si nous avons de nombreuses variables - alors supprimer toutes les lignes avec au moins un NA pourrait nous laisser sans données du tout !


Il existe de nombreuses raisons pour lesquelles on peut vouloir calculer la corrélation : c'est la première étape de l'EDA dans presque tous les modèles multivariables, elle est assez largement utilisée dans tout type de construction de portefeuille , etc. Il est donc tout à fait nécessaire de mesurer ce nombre aussi précisément que possible !


Pour illustrer, générons une deuxième variable avec une corrélation « intégrée » de 0,4 avec la première. Pour ce faire, nous utiliserons une sorte de modèle de mélange gaussien . L’image que l’on peut avoir à l’esprit est celle de deux actions corrélées qui partagent un facteur de risque important, mais la seconde action est également exposée à un facteur de risque majeur que la première n’a pas. Pensez à Google et Facebook par exemple : le premier facteur pourrait être un sentiment générique à l'égard du secteur technologique et le second pourrait être la concurrence avec les réseaux sociaux concurrents.


 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


Vérifions la corrélation empirique « de base », c'est-à-dire sans les NA et les sauts.

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


Ceci est raisonnablement proche de la corrélation « théorique » – il est bien connu que la mesure empirique de la corrélation est sujette à un bruit assez important.


NA sans valeurs aberrantes

Dans la prochaine étape, nous examinerons le cas des NA, mais sans valeurs aberrantes. Nous comparerons également ce qui se passe si nous dropna avant et après diff

 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


Les deux résultats sont assez proches et pas trop éloignés de la valeur « empirique » que nous avions obtenue auparavant. Vérifions que LOCF et l'imputation fonctionnent également correctement :

 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


En comparant les 4 résultats ci-dessus, nous constatons que toutes les méthodes fonctionnent plutôt bien. Peut-être devrions-nous alors nous attendre à la même chose pour le cas aberrant ?


NA avec valeurs aberrantes

N'oubliez pas que pour rester cohérents, nous devons exposer la deuxième série de prix aux mêmes chocs de prix que la première, mais sans les NA suivantes. Pour revenir à l'exemple ci-dessus, imaginez un événement majeur provoquant une hausse du premier facteur de risque qui finit par interrompre la négociation du premier actif. Le deuxième actif en fera également l'expérience, bien sûr, mais peut-être dans une moindre mesure, et donc aucun arrêt n'aurait lieu et donc pas de NA.


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


Comparons à nouveau les performances de toutes nos méthodes -

 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


C'est toute une différence entre la valeur théorique et empirique ! Qu’en est-il du LOCF et de l’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


Maintenant, nous voyons enfin ce que vaut LOCF ! Elle surpasse clairement toutes les autres méthodes !


Pièges potentiels + interpolation

Bien entendu, ce n’est pas robuste à 100 %. D’une part, en faisant LOCF, nous introduisons une baisse importante des prix lorsque les données manquantes se terminent. Si cela coïncide avec certaines valeurs aberrantes sur le deuxième vecteur de prix, les résultats pourraient changer considérablement. (*Un exercice pour le lecteur : retournez le signe sur le mouvement de prix de price_vec_na_2[95] et vérifiez comment cela affecte les résultats). Il n'est pas tout à fait clair s'il est « propre » d'introduire simplement cette baisse de prix plutôt que, par exemple, d'interpoler entre le pic de prix price_vec_na[91] et la valeur « normale » par la suite price_vec_na[95] . Cependant, surtout pour une utilisation « live », l’interpolation n’est pas vraiment possible ! Après tout, si nous sommes aujourd’hui le jour n°93, comment pouvons-nous interpoler en utilisant une valeur future enregistrée à la fin du jour n°95 ? Pour une étude historique – bien sûr, cela reste une option, mais on ne sait toujours pas comment l’interpréter et l’utiliser pour de véritables prévisions ! En conclusion, l’interpolation à travers la dimension temporelle est possible, mais quelque peu plus discutable.


Conclusions

J'ai essayé de donner une petite étude de cas pour présenter et expliquer pourquoi LOCF est souvent l'option la plus attrayante et la plus simple à utiliser pour gérer les données manquantes dans les séries chronologiques financières.


Pour récapituler, les avantages sont :

  • Attrayant du point de vue de la martingale/« flux d’informations »
  • Super facile à mettre en œuvre
  • Pas besoin d'optimisation (contrairement à l'imputation)
  • Gère les valeurs aberrantes au moins décemment


Quelques inconvénients :

  • Peut potentiellement provoquer des sauts importants à la fin de la période manquante
  • Peut manquer certaines dynamiques conjointes nuancées lorsqu'il est utilisé pour plusieurs variables


En tant que quant dans un magasin de prop trading, je l'utilise pour presque toutes mes études comme base de référence efficace. Certaines situations nécessitent bien sûr des mesures plus nuancées, mais celles-ci sont rares et ne sont généralement pas non plus vraiment « résolues » à 100 % par l’une des 3 autres méthodes mentionnées.