Détection périodique d'une série temporelle générique

53

Cet article est la suite d'un autre article lié à une méthode générique de détection des valeurs aberrantes dans les séries chronologiques . Fondamentalement, à ce stade, je suis intéressé par un moyen robuste de découvrir la périodicité / saisonnalité d’une série temporelle générique affectée par beaucoup de bruit. Du point de vue du développeur, j'aimerais une interface simple telle que:

unsigned int discover_period(vector<double> v);

vest le tableau contenant les échantillons et la valeur de retour est la période du signal. Le point principal est que, encore une fois, je ne peux faire aucune hypothèse concernant le signal analysé. J'ai déjà essayé une approche basée sur l'autocorrélation du signal (détection des pics d'un corrélogramme), mais ce n'est pas robuste comme je le voudrais.

Gianluca
la source
1
Avez-vous essayé xts :: periodicity?
Fabrício

Réponses:

49

Si vous ne savez vraiment pas quelle est la périodicité, la meilleure approche consiste probablement à trouver la fréquence correspondant au maximum de la densité spectrale. Cependant, le spectre aux basses fréquences sera affecté par la tendance, vous devez donc commencer par décourager la série. La fonction R suivante devrait faire le travail pour la plupart des séries. Il est loin d'être parfait, mais je l'ai testé sur quelques dizaines d'exemples et cela semble fonctionner correctement. Il renverra 1 pour les données qui n'ont pas de périodicité forte, et la durée de la période sinon.

Mise à jour: version 2 de la fonction. C'est beaucoup plus rapide et semble être plus robuste.

find.freq <- function(x)
{
    n <- length(x)
    spec <- spec.ar(c(x),plot=FALSE)
    if(max(spec$spec)>10) # Arbitrary threshold chosen by trial and error.
    {
        period <- round(1/spec$freq[which.max(spec$spec)])
        if(period==Inf) # Find next local maximum
        {
            j <- which(diff(spec$spec)>0)
            if(length(j)>0)
            {
                nextmax <- j[1] + which.max(spec$spec[j[1]:500])
                period <- round(1/spec$freq[nextmax])
            }
            else
                period <- 1
        }
    }
    else
        period <- 1
    return(period)
}
Rob Hyndman
la source
Je vous remercie. Encore une fois, je vais essayer cette approche le plus tôt possible et écrire ici les résultats finaux.
Gianluca
2
Votre idée est assez bonne, mais dans mon cas, elle ne parvient pas à détecter la périodicité d'une série chronologique très simple (et pas si bruyante) comme dl.dropbox.com/u/540394/chart.png . Avec mon approche "empirique" (basée sur l'autocorrélation), l'algorithme simple que j'ai écrit renvoie une période exacte de 1008 (avoir un échantillon toutes les 10 minutes, cela signifie 1008/24/6 = 7, donc une périodicité hebdomadaire). Mes principaux problèmes sont les suivants: 1) La convergence est trop lente (il faut beaucoup de données historiques) et j'ai besoin d'une approche en ligne réactive. 2) il est inefficace du point de vue de l'utilisation de la mémoire; 3) ce n'est pas robuste du tout;
Gianluca
Je vous remercie. Malheureusement, cela ne fonctionne toujours pas comme je le pensais. Pour la même série chronologique que le commentaire précédent, elle renvoie 166, ce qui n’est que partiellement exact (de mon point de vue, la période hebdomadaire évidente est plus intéressante). Et en utilisant une série chronologique très bruyante, comme celle-ci, dl.dropbox.com/u/540394/chart2.png (une analyse de la fenêtre du récepteur TCP), la fonction renvoie 10, alors que je m'attendais à 1 (je ne vois aucune évidence périodicité). En passant, je sais que ce sera très difficile de trouver ce que je cherche, car je suis confronté à des signaux trop différents.
Gianluca
166 n'est pas une mauvaise estimation de 168. Si vous savez que les données sont observées heure par heure avec un schéma hebdomadaire, alors pourquoi en estimer la fréquence?
Rob Hyndman
5
Une version améliorée est dans le paquet de prévisions commefindfrequency
Rob Hyndman
10

Si vous vous attendez à ce que le processus soit stationnaire (la périodicité / la saisonnalité ne changera pas avec le temps), un périodogramme ressemblant au chi carré (voir par exemple Sokolove et Bushell, 1978) peut constituer un bon choix. Il est couramment utilisé dans l'analyse de données circadiennes qui peuvent contenir des quantités de bruit extrêmement importantes, mais dont les périodicités sont très stables.

Cette approche ne fait aucune hypothèse sur la forme de la forme d'onde (sauf si elle est cohérente d'un cycle à l'autre), mais exige que tout bruit soit de moyenne constante et non corrélé au signal.

chisq.pd <- function(x, min.period, max.period, alpha) {
N <- length(x)
variances = NULL
periods = seq(min.period, max.period)
rowlist = NULL
for(lc in periods){
    ncol = lc
    nrow = floor(N/ncol)
    rowlist = c(rowlist, nrow)
    x.trunc = x[1:(ncol*nrow)]
    x.reshape = t(array(x.trunc, c(ncol, nrow)))
    variances = c(variances, var(colMeans(x.reshape)))
}
Qp = (rowlist * periods * variances) / var(x)
df = periods - 1
pvals = 1-pchisq(Qp, df)
pass.periods = periods[pvals<alpha]
pass.pvals = pvals[pvals<alpha]
#return(cbind(pass.periods, pass.pvals))
return(cbind(periods[pvals==min(pvals)], pvals[pvals==min(pvals)]))
}

x = cos( (2*pi/37) * (1:1000))+rnorm(1000)
chisq.pd(x, 2, 72, .05)

Les deux dernières lignes ne sont qu'un exemple, montrant qu'il peut identifier la période d'une fonction trigonométrique pure, même avec beaucoup de bruit additif.

Comme écrit, le dernier argument ( alpha) de l'appel est superflu, la fonction renvoie simplement la "meilleure" période qu'il peut trouver; décommentez la première returndéclaration et commentez la seconde pour qu'elle renvoie une liste de toutes les périodes significatives au niveau alpha.

Cette fonction ne fait aucune sorte de vérification de cohérence pour vous assurer que vous avez mis des périodes identifiables, elle ne fonctionne pas (avec des périodes fractionnaires), et il n’existe aucune sorte de contrôle de comparaison multiple intégré si vous décidez de le faire. regarde plusieurs périodes. Mais à part cela, il devrait être raisonnablement robuste.

Riches
la source
Ça a l'air intéressant mais je ne comprends pas la sortie, ça ne me dit pas où commence la période et la plupart des pvalues ​​de 1.
Herman Toothrot
3

Vous voudrez peut-être définir plus clairement ce que vous voulez (pour vous-même, sinon ici). Si ce que vous recherchez est la période stationnaire la plus statistiquement significative contenue dans vos données bruitées, il existe essentiellement deux itinéraires à suivre:

1) calculer une estimation robuste d'autocorrélation et prendre le coefficient maximal
2) calculer une estimation robuste de densité spectrale de puissance et utiliser le maximum du spectre

Le problème avec # 2 est que pour toute série chronologique bruyante, vous obtiendrez une grande quantité de puissance dans les basses fréquences, ce qui rend difficile la distinction. Il existe certaines techniques pour résoudre ce problème (c'est-à-dire pré-blanchir, puis estimer le PSD), mais si la période réelle de vos données est suffisamment longue, la détection automatique sera difficile.

Votre meilleur choix est probablement de mettre en œuvre une routine d'autocorrélation robuste, comme celle décrite au chapitre 8.6, 8.7 dans Statistiques robustes - Théorie et méthodes de Maronna, Martin et Yohai. La recherche sur Google pour "robust durbin-levinson" donnera également des résultats.

Si vous cherchez simplement une réponse simple, je ne suis pas sûre qu'il en existe une. La détection de période dans une série chronologique peut être compliquée, et demander une routine automatisée capable d'effectuer de la magie peut s'avérer excessif.

Wesley Burr
la source
Merci pour vos précieuses informations, je regarderai ce livre à coup sûr.
Gianluca
3

Vous pouvez utiliser la théorie de la transformation de Hilbert à partir de DSP pour mesurer la fréquence instantanée de vos données. Le site http://ta-lib.org/ contient un code source ouvert permettant de mesurer la période de cycle dominante des données financières; la fonction correspondante est appelée HT_DCPERIOD; vous pourrez peut-être l'utiliser ou adapter le code à vos besoins.

lecteur babelproof
la source
3

Une approche différente pourrait être la décomposition en mode empirique. Le package R est appelé EMD développé par l'inventeur de la méthode:

require(EMD)
ndata <- 3000  
tt2 <- seq(0, 9, length = ndata)  
xt2 <- sin(pi * tt2) + sin(2* pi * tt2) + sin(6 * pi * tt2) + 0.5 * tt2  
try <- emd(xt2, tt2, boundary = "wave")  
### Ploting the IMF's  
par(mfrow = c(try$nimf + 1, 1), mar=c(2,1,2,1))  
rangeimf <- range(try$imf)  
for(i in 1:try$nimf) {  
plot(tt2, try$imf[,i], type="l", xlab="", ylab="", ylim=rangeimf, main=paste(i, "-th IMF", sep="")); abline(h=0)  
}  
plot(tt2, try$residue, xlab="", ylab="", main="residue", type="l", axes=FALSE); box()

La méthode a été baptisée «Empirical» pour une bonne raison et il existe un risque de confusion entre les fonctions de mode intrinsèque (les composants additifs individuels). D'autre part, la méthode est très intuitive et peut être utile pour une inspection visuelle rapide de la cyclicité.

Fabrizio Maccallini
la source
0

En référence au message de Rob Hyndman ci-dessus https://stats.stackexchange.com/a/1214/70282

La fonction find.freq fonctionne à merveille. Sur le jeu de données quotidien que j’utilise, il a correctement calculé la fréquence à 7.

Lorsque je l’essayais uniquement les jours de la semaine, la fréquence était de 23, ce qui est remarquablement proche de 21,42857 = 29,6 * 5/7, ce qui correspond au nombre moyen de jours de travail par mois. (Ou à l'inverse 23 * 7/5 est 32.)

En regardant mes données quotidiennes, j'ai expérimenté l'idée de prendre la première période, de calculer la moyenne par la suite, puis de trouver la prochaine période, etc. Voir ci-dessous:

find.freq.all = function (x) {  
  f = find.freq (x);
  fréq = c (f);  
  tandis que (f> 1) {
    début = 1; #aussi essayer de commencer = f;
    x = période.apply (x, seq (début, longueur (x), f), moyenne); 
    f = find.freq (x);
    fréq = c (fréq, f);
  }
  if (longueur (freqs) == 1) {return (freqs); }
  pour (i in 2: length (freqs)) {
    freqs [i] = freqs [i] * freqs [i-1];
  }
  freqs [1: (longueur (freqs -1));
}
find.freq.all (dailyts) #en utilisant des données quotidiennes

Ce qui précède donne (7,28) ou (7,35) selon que le seq commence par 1 ou f. (Voir le commentaire ci-dessus.)

Ce qui impliquerait que les périodes saisonnières pour les msts (...) soient de (7,28) ou (7,35).

La logique semble sensible aux conditions initiales étant donné la sensibilité des paramètres de l'algorithme. La moyenne de 28 et 35 est de 31,5, ce qui est proche de la durée moyenne d'un mois.

Je soupçonne avoir réinventé la roue, quel est le nom de cet algorithme? Y at-il une meilleure mise en œuvre dans R quelque part?

Plus tard, j'ai utilisé le code ci-dessus en essayant tous les départs de 1 à 7 et j'ai eu 35,35,28,28,28,28,28 pour la deuxième période. La moyenne s’élève à 30, ce qui correspond au nombre moyen de jours dans un mois. Intéressant...

Des pensées ou des commentaires?

Chris
la source
0

On peut aussi utiliser le test de Ljung-Box pour déterminer quelle différence saisonnière atteint la meilleure stationnarité. Je travaillais sur un sujet différent et je l’utilisais en fait aux mêmes fins. Essayez différentes périodes telles que 3 à 24 pour des données mensuelles. Et testez chacune d’elles par Ljung-Box et stockez les résultats du Chi-Square. Et choisissez la période avec la plus basse valeur de Khi-deux.

Voici un code simple pour le faire.

minval0 <- 5000 #assign a big number to be sure Chi values are smaller
minindex0 <- 0
periyot <- 0

for (i in 3:24) { #find optimum period by Qtests over original data

        d0D1 <- diff(a, lag=i)

        #store results
        Qtest_d0D1[[i]] <- Box.test(d0D1, lag=20, type = "Ljung-Box")

        #store Chi-Square statistics
        sira0[i] <- Qtest_d0D1[[i]][1]
}
#turn list to a data frame, then matrix
datam0 <- data.frame(matrix(unlist(sira0), nrow=length(Qtest_d0D1)-2, byrow = T))
datamtrx0 <- as.matrix(datam0[])
#get min value's index
minindex0 <- which(datamtrx0 == min(datamtrx0), arr.ind = F)
periyot <- minindex0 + 2
ali
la source