Pourquoi y a-t-il une différence entre la prédiction sur l'ensemble de validation et l'ensemble de tests?

8

J'ai un modèle XGBoost essayant de prédire si une devise montera ou descendra la prochaine période (5 min). J'ai un ensemble de données de 2004 à 2018. J'ai divisé les données randomisées en 95% de train et 5% de validation et la précision sur l'ensemble de validation est jusqu'à 55%. Lorsque j'utilise ensuite le modèle sur un nouvel ensemble de test (données de 2019), la précision descend en dessous de 51%.

Quelqu'un peut-il expliquer pourquoi cela pourrait être?

Je veux dire, je suppose que le modèle n'a pas "vu" (formé) les données de validation plus qu'il n'a les données de test, donc peut-il vraiment être trop adapté?

J'ai joint un modèle simple ci-dessous pour illustrer. Celui-ci donne 54% sur l'ensemble de validation mais seulement 50,9% sur l'ensemble de test .

Merci pour toute aide!

NB Une théorie que j'avais était que, comme certaines fonctionnalités reposaient sur des données historiques (par exemple, la moyenne mobile), il pouvait s'agir d'une fuite de données. J'ai ensuite essayé de corriger cela en n'échantillonnant que des données qui ne faisaient pas partie de la création de la moyenne mobile. Par exemple, s'il y a une moyenne mobile de 3 périodes, je n'échantillonne / n'utilise pas les lignes de données de 2 périodes en arrière. Cela n'a rien changé donc ce n'est pas dans le modèle ci-dessous.

NB2 Le modèle ci-dessous est une version simple de ce que j'utilise. La raison d'un ensemble de validation pour moi est que j'utilise un algorithme génétique pour le réglage de l'hyperparamètre mais tout ce qui est supprimé ici pour plus de clarté.

import pandas as pd
import talib as ta
from sklearn.utils import shuffle
pd.options.mode.chained_assignment = None
from sklearn.metrics import accuracy_score

# ## TRAINING AND VALIDATING
# ### Read in data
input_data_file = 'EURUSDM5_2004-2018_cleaned.csv'   # For train and validation
df = pd.read_csv(input_data_file)

# ### Generate features
#######################
# SET TARGET
#######################
df['target'] = df['Close'].shift(-1)>df['Close']       # target is binary, i.e. either up or down next period

#######################
# DEFINE FEATURES
#######################
df['rsi'] = ta.RSI(df['Close'], 14) 

# ### Treat the data
#######################
# FIND AND MAKE CATEGORICAL VARAIBLES AND DO ONE-HOT ENCODING
#######################
for col in df.drop('target',axis=1).columns:     # Crude way of defining variables with few unique variants as categorical
    if df[col].nunique() < 25:
        df[col] = pd.Categorical(df[col])

cats = df.select_dtypes(include='category')     # Do one-hot encoding for the categorical variables
for cat_col in cats:
    df = pd.concat([df,pd.get_dummies(df[cat_col], prefix=cat_col,dummy_na=False)],axis=1).drop([cat_col],axis=1)

uints = df.select_dtypes(include='uint8')
for col in uints.columns:                   # Variables from the one-hot encoding is not created as categoricals so do it here
    df[col] = df[col].astype('category')

#######################
# REMOVE ROWS WITH NO TRADES
#######################
df = df[df['Volume']>0]

#######################
# BALANCE NUMBER OF UP/DOWN IN TARGET SO THE MODEL CANNOT SIMPLY CHOOSE ONE AND BE SUCCESSFUL THAT WAY
#######################
df_true = df[df['target']==True]
df_false = df[df['target']==False]

len_true = len(df_true)
len_false = len(df_false)
rows = min(len_true,len_false)

df_true = df_true.head(rows)
df_false = df_false.head(rows)
df = pd.concat([df_true,df_false],ignore_index=True)
df = shuffle(df)
df.dropna(axis=0, how='any', inplace=True)

# ### Split data
df = shuffle(df)
split = int(0.95*len(df))

train_set = df.iloc[0:split]
val_set = df.iloc[split:-1]

# ### Generate X,y
X_train = train_set[train_set.columns.difference(['target', 'Datetime'])]
y_train = train_set['target']

X_val = val_set[val_set.columns.difference(['target', 'Datetime'])]
y_val = val_set['target']

# ### Scale
from sklearn.preprocessing import StandardScaler
sc = StandardScaler()

cont = X_train.select_dtypes(exclude='category')                   # Find columns with continous (not categorical) variables
X_train[cont.columns] = sc.fit_transform(X_train[cont.columns])    # Fit and transform

cont = X_val.select_dtypes(exclude='category')                     # Find columns with continous (not categorical) variables
X_val[cont.columns] = sc.transform(X_val[cont.columns])            # Transform

cats = X_train.select_dtypes(include='category')
for col in cats.columns:
    X_train[col] = X_train[col].astype('uint8')

cats = X_val.select_dtypes(include='category')
for col in cats.columns:
    X_val[col] = X_val[col].astype('uint8')


# ## MODEL
from xgboost import XGBClassifier
model = XGBClassifier()
model.fit(X_train, y_train)

predictions = model.predict(X_val)
acc = 100*accuracy_score(y_val, predictions)
print('{0:0.1f}%'.format(acc))

# # TESTING
input_data_file = 'EURUSDM5_2019_cleaned.csv'   # For testing
df = pd.read_csv(input_data_file)

#######################
# SET TARGET
#######################
df['target'] = df['Close'].shift(-1)>df['Close']       # target is binary, i.e. either up or down next period
#######################
# DEFINE FEATURES
#######################
df['rsi'] = ta.RSI(df['Close'], 14)

#######################
# FIND AND MAKE CATEGORICAL VARAIBLES AND DO ONE-HOT ENCODING
#######################
for col in df.drop('target',axis=1).columns:     # Crude way of defining variables with few unique variants as categorical
    if df[col].nunique() < 25:
        df[col] = pd.Categorical(df[col])

cats = df.select_dtypes(include='category')     # Do one-hot encoding for the categorical variables
for cat_col in cats:
    df = pd.concat([df,pd.get_dummies(df[cat_col], prefix=cat_col,dummy_na=False)],axis=1).drop([cat_col],axis=1)

uints = df.select_dtypes(include='uint8')
for col in uints.columns:                   # Variables from the one-hot encoding is not created as categoricals so do it here
    df[col] = df[col].astype('category')

#######################
# REMOVE ROWS WITH NO TRADES
#######################
df = df[df['Volume']>0]
df.dropna(axis=0, how='any', inplace=True)

X_test = df[df.columns.difference(['target', 'Datetime'])]
y_test = df['target']

cont = X_test.select_dtypes(exclude='category')                     # Find columns with continous (not categorical) variables
X_test[cont.columns] = sc.transform(X_test[cont.columns])            # Transform

cats = X_test.select_dtypes(include='category')
for col in cats.columns:
    X_test[col] = X_test[col].astype('uint8')

predictions = model.predict(X_test)
acc = 100*accuracy_score(y_test, predictions)
print('{0:0.1f}%'.format(acc))
DBSE
la source

Réponses:

6

La seule différence semble être les données. Peut-être que l'ensemble de test (qui était les données les plus récentes) différait légèrement de celui des ensembles de formation / validation et a conduit à une sous-performance de votre modèle.

minou
la source
6

La chose la plus probable est qu'il y a eu une certaine dérive du concept. Depuis que votre modèle est formé sur les données jusqu'en 2018 et testé sur 2019, les choses ont changé et certains de ces changements que votre modèle ne peut pas prévoir.

Quelques autres possibilités cependant:

Vous dites que vous avez effectué un réglage hyperparamétrique, mais que vous l'avez omis du code pour plus de simplicité. Mais si vous utilisez l'ensemble de validation pour choisir les hyperparamètres, alors le score que vous obtenez sera biaisé de manière optimiste. (Mais vous dites que le modèle n'a pas vu l'ensemble de validation, alors ce n'est peut-être pas ainsi que vous le faites.)

Enfin, il est possible que vous ayez tout fait correctement et qu'il n'y ait pas vraiment de dérive de concept, mais que les effets aléatoires ne représentent que quelques points de précision.

Ben Reiniger
la source
2

Il y a deux raisons principales:

  1. Le modèle formé présente des performances proches de celles aléatoires. Par exemple, 50% est une performance aléatoire dans une tâche de classification binaire en supposant une appartenance à une classe égale. En d'autres termes, le modèle n'apprend pas de modèles prédictifs significatifs des données de 2004 à 2018.

  2. Il pourrait y avoir de nouveaux modèles dans les données de 2019. Les tendances (à peine apprises) des données de 2004 à 2018 ne sont pas transférées aux données de 2019.

Brian Spiering
la source
Oh, oui, j'ai en quelque sorte manqué qu'il s'agissait d'une classification binaire, que les scores rapportés étaient des précisions, et seulement 54% et 51%. +1
Ben Reiniger
0

Comme le veut l'ancien mantra de l'investissement, "les performances passées ne sont pas indicatives des performances futures".

Mon premier candidat est sur-adapté. Bien que la probabilité qu'un modèle particulier soit symptomatique d'une certaine direction, même s'il n'est pas du tout causal (ou prédictif au-delà de l'échantillon en question), est astronomiquement faible, il existe également une quantité astronomique de modèles à détecter pouvant présenter un tel comportement. .

Supposons que ce sont de vrais schémas que vous avez appris:
pendant que vous entraîniez un algo à apprendre ses trois fonds et ses têtes et épaules, des centaines de banques l'étaient aussi, et à le faire plus rapidement que vous et à utiliser ces informations.
Cette information s'est reflétée dans différents mouvements de prix, car ils en savaient plus qu'en 2018 et ont agi différemment, votre modèle ne sait pas encore prendre en compte ces actions car elles sont nouvelles.

Robin Gertenbach
la source