Reading time ~ 10 minutes ->

L’objectif de cet article est d’identifier sur la semaine ou sur le mois, les journées où les sessions sont anormalement élevées. Plusieurs méthodes nous permettent d’identifier les outliers soit les valeurs aberrantes. Je pense notatement au calcul de centiles ou via le z-score (Cote Z). Existe’-il une méthode robuste? Nous testerons différentes solutions. Première étape, le chargement des datas avec readRDS.

cim <- readRDS("data/cim_db_shiny_31012019.rds")

Approche 1 : Identification par les centiles.

Nous allons observer uniquement les sessions pour l’instant. Réalisons le premier graphique. le package ggridges comprend une fonction pour faciliter la visualisation des courbes de distribution (Density plot). Le paramètre quantiles_lines permet le représentation de quantiles. Fixons les quantiles à 0.05 et 0.95.

cim %>% 
  filter(metrics == "sessions") %>%
  ggplot() +
  aes(hits, site, fill = ..y..) + 
  geom_density_ridges2(aes(), scale = 2, quantile_lines = TRUE, quantiles = c(0.05, 0.95), alpha = 0.9)

Les valeurs élevées de L'essentiel et de RTL rendent la comparaison assez difficile. En effet l’échelle des x couvre une grande étendue.

Pour rappel, nous essayons d’identifier les jours où le nombre de sessions est anormal. Pouvons-nous le faire en identifiant les sessions dont le nombre de sessions est au-delà du 95e centile?

cim %>% 
  filter(metrics == "sessions") %>%
  filter(site %in% c("RTL Luxembourg", "L'essentiel", "Wort.lu")) %>% 
  ggplot() +
  aes(hits, site, fill = ..y..) + 
  geom_density_ridges2(aes(), scale = 2, quantile_lines = TRUE, quantiles = c(0.05, 0.95), alpha = 0.9)

On voit sur ce graphique de manière assez évidente 2 modes pour RTLet L'essentiel. Il s’agit probablement des sessions mesures les jours de la semaine et puis le week-end.

Bon, concentrons-nous sur un site uniquement. Utilisons les semaines de 2018 pour les ordonnées. Appelons ..quantile.. pour représenter les échelles.

cim %>% 
  filter(metrics == "sessions") %>%
  filter(site %in% c("RTL Luxembourg")) %>%
  filter(date >= "2018-01-01") %>%
  mutate(week = lubridate::week(date)) %>% 
  ggplot() +
  aes(hits, forcats::fct_rev(factor(week)), fill = factor(..quantile..)) +
  geom_density_ridges_gradient(scale = 3, quantile_lines = TRUE, quantiles = c(0.05, 0.95)) +
  scale_fill_viridis_d()

On peut voir effectivement que toutes les semaines, nous pouvons avoir des jours dans le 95e centile. Plusieurs approches sont donc possibles, sur l’ensemble de l’année, par mois ou par semaine. Essayons le même graphique, mais pour chaque mois.

cim %>% 
  filter(metrics == "sessions") %>%
  filter(site %in% c("RTL Luxembourg")) %>%
  filter(date >= "2018-01-01") %>%
  mutate(month = lubridate::month(date)) %>% 
  ggplot() +
  aes(hits, forcats::fct_rev(factor(month)), fill = factor(..quantile..)) +
  geom_density_ridges_gradient(scale = 2, quantile_lines = TRUE, quantiles = c(0.05, 0.95)) +
  scale_fill_viridis_d()

Il est évident que suivant le nombre de dates que nous souhaitons identifier, il faudra choisir l’intervalle. C’est-à-dire que le nombre de dates sera > que l’intervalle choisi (1 an, 12 mois, 52 semaines). Étant donné que cette time série à une évolution (trends), la valeur des sessions appartenant au quantile souhaité varie avec le temps. Testons la fonction calcPercentile du package openair.

cim %>% 
  filter(metrics == "sessions") %>%
  filter(site %in% c("RTL Luxembourg")) %>%
  filter(date >= "2018-01-01") %>% 
    openair::calcPercentile(pollutant = "hits", avg.time = "month",
  percentile = 99, data.thresh = 5, start = NA)
##          date percentile.99
## 1  2018-01-01      294377.9
## 2  2018-02-01      308913.5
## 3  2018-03-01      319241.9
## 4  2018-04-01      334887.2
## 5  2018-05-01      309357.6
## 6  2018-06-01      420902.9
## 7  2018-07-01      317699.4
## 8  2018-08-01      334534.9
## 9  2018-09-01      303468.4
## 10 2018-10-01      561667.5
## 11 2018-11-01      307346.6
## 12 2018-12-01      325115.6
## 13 2019-01-01      338126.6

Mouais… En fait, cela revient à prendre pour chaque mois la valeur max… Voyons cela :

cim %>% 
  filter(metrics == "sessions") %>%
  filter(site %in% c("RTL Luxembourg")) %>%
  filter(date >= "2018-01-01") %>% 
  mutate(month = lubridate::month(date)) %>% 
  group_by(month) %>% 
  filter(hits == max(hits)) %>% 
  arrange(desc(date))
## # A tibble: 12 x 5
## # Groups:   month [12]
##    date       site           metrics    hits month
##    <date>     <chr>          <chr>     <dbl> <dbl>
##  1 2019-01-30 RTL Luxembourg sessions 341126     1
##  2 2018-12-03 RTL Luxembourg sessions 327866    12
##  3 2018-11-29 RTL Luxembourg sessions 312577    11
##  4 2018-10-14 RTL Luxembourg sessions 578463    10
##  5 2018-09-10 RTL Luxembourg sessions 303975     9
##  6 2018-08-03 RTL Luxembourg sessions 335632     8
##  7 2018-07-31 RTL Luxembourg sessions 321351     7
##  8 2018-06-01 RTL Luxembourg sessions 431512     6
##  9 2018-05-13 RTL Luxembourg sessions 309510     5
## 10 2018-04-10 RTL Luxembourg sessions 336131     4
## 11 2018-03-02 RTL Luxembourg sessions 319454     3
## 12 2018-02-27 RTL Luxembourg sessions 317729     2

Soit 12 dates à examiner. Est-ce que ces 12 dates représentent des dates anormalement hautes? Voyons cela sur un graphique. Sauvegardons les dates dans un vecteur.

cim %>% 
  arrange(desc(date)) %>% 
  filter(metrics == "sessions") %>%
  filter(site %in% c("RTL Luxembourg")) %>%
  filter(date >= "2018-01-01", date < "2019-01-01") %>% 
  mutate(month = lubridate::month(date)) %>% 
  group_by(month) %>% 
  filter(hits == max(hits)) %>% 
  pull(date) -> high_date
cim %>% 
  filter(metrics == "sessions") %>%
  filter(site %in% c("RTL Luxembourg")) %>%
  filter(date >= "2018-01-01", date < "2019-01-01") %>%
  mutate(month = lubridate::month(date)) %>% 
  select(date, month, hits) %>% 
  ggplot() +
  aes(date, hits, col = (date %in% high_date)) +
  geom_point(size = 2) +
  geom_line(aes(group = month)) +
  theme(legend.position = "none") +
  labs(title = "RTL Luxembourg - Sessions 2018") +
  facet_wrap(~month, scales = "free")

Tout cela ne nous aide pas de trop. Nous voyons bien les jours en bleu où le sessions sont les plus importantes pour chaque mois.

Approche 2 : Règle des 3 sigmas.

Voici ce que nous dit wikipedia :

La règle des trois sigmas exprime une heuristique fréquemment utilisée : la plupart des valeurs se situent a moins de trois fois l’écart-type de la moyenne. Pour de nombreuses applications pratiques, ce pourcentage de 99.3% peut être considéré comme une quasi-certitude. L’usage de cette heuristique dépend cependant du domaine : ainsi en sciences sociales, un résultat est considéré comme significatif si son intervalle de confiance est au moins de 95%, soit de l’ordre de deux sigmas, alors qu’en physique des particules, le seuil de significativité se situe autour de cinq sigmas (soit un intervalle de confiance à 99.99994%).

La règle des trois sigmas est également applicable à d’autres distributions que la loi normale. En effet, l’Inégalité de Bienaymé-Tchebychev permet d’affirmer que pour toute variable aléatoire, au moins 88.8% des réalisations se situent dans un intervalle de trois sigmas.

Ces valeurs numériques (68%, 95% et 99.7%) proviennent de la fonction de répartition de la loi normale.

Ajoutons la moyenne pour chaque mois afin de sélectionner notre référence à laquelle nous ajouterons le calcul de l’écart-type (sd = Standard Deviation en anglais). Nous voulons afficher les dates dont la valeur est supérieure ou inférieure à 3 sigma.

cim %>% 
  filter(metrics == "sessions") %>%
  filter(site %in% c("RTL Luxembourg")) %>%
  filter(date >= "2018-01-01", date < "2019-01-01") %>%
  mutate(month = lubridate::month(date)) %>%
  group_by(month) %>% 
  mutate(mean.month = mean(hits), med.month = median(hits)) %>% 
  ungroup() %>% 
  select(date, month, mean.month, med.month, hits) %>% 
  ggplot() +
  aes(date, hits) +
  geom_point(size = 3, alpha = .4) +
  geom_hline(aes(yintercept = mean.month), col = "blue") +
  facet_wrap(~month, scales = "free")

Que se passe-t-il si nous ajoutons un repère à 3 écarts-type de notre moyenne mensuelle.

cim %>% 
  filter(metrics == "sessions") %>%
  filter(site %in% c("RTL Luxembourg")) %>%
  filter(date >= "2018-01-01", date < "2019-01-01") %>%
  mutate(month = lubridate::month(date)) %>%
  group_by(month) %>% 
  mutate(mean.month = mean(hits), med.month = median(hits)) %>%
  mutate(sd = sd(hits), Usd = (mean.month + 3 * sd), Lsd = (mean.month - 3 * sd)) %>% 
  ungroup() %>% 
  ggplot() +
  aes(date, hits, col = (hits > Usd | hits < Lsd)) +
  geom_point(size = 2, alpha = .75) +
  geom_hline(aes(yintercept = mean.month), col = "blue") +
  geom_hline(aes(yintercept = Lsd), col = "red") +
  geom_hline(aes(yintercept = Usd), col = "red") +
  theme(legend.position = "none") +
  facet_wrap(~month, scales = "free")

On voit que les valeurs max sont exclues de l’intervalle -3*sd, +3*sd, ne concerne plus que 4 valeurs :

  • février
  • juin
  • octobre

Voyons ce que la méthode donne pour 2017. Fixons l’échelle des ordonnées.

cim %>% 
  filter(metrics == "sessions") %>%
  filter(site %in% c("RTL Luxembourg")) %>%
  filter(date >= "2017-01-01", date < "2018-01-01") %>%
  mutate(month = lubridate::month(date)) %>%
  group_by(month) %>% 
  mutate(mean.month = mean(hits), med.month = median(hits)) %>%
  mutate(sd = sd(hits), upper.sd = (mean.month + 3 * sd), lower.sd = (mean.month - 3 * sd)) %>% 
  ungroup() %>% 
  ggplot() +
  aes(date, hits, col = (hits > upper.sd)) +
  geom_line(aes(group = month), col = "black") +
  geom_point(size = 2, alpha = .75) +
  geom_hline(aes(yintercept = mean.month), col = "blue") +
  geom_hline(aes(yintercept = lower.sd), col = "red") +
  geom_hline(aes(yintercept = upper.sd), col = "red") +
  theme(legend.position = "none") +
  labs(title = "2017") +
  facet_wrap(~month, scale = "free_x")

5 jours spécifiques pour 2017. C’est déjà mieux que 12

approche 3 : BoxPlot

Et si finalement, l’identification pouvait se faire grâce aux boites à moustache ? En effet les valeurs aberrantes sont facilement visualisables. Pour rappel, nous étions sur une identification en février, juin et octobre.

cim %>% 
  filter(metrics == "sessions") %>%
  filter(site %in% c("RTL Luxembourg")) %>%
  filter(date >= "2018-01-01", date < "2019-01-01") %>%
  mutate(month = lubridate::month(date)) %>%
  ggplot() +
  aes(factor(month), hits) +
  geom_boxplot(outlier.colour = "red", outlier.size = 5) +
  geom_jitter(width = 0.05, size = 5, col = "blue", alpha = 0.3) +
  theme(legend.position = "none") +
  labs(title = "2018")

Nous voyons effectivement qu’il est possible d’identifier les valeurs aberrantes grâce à ce type de graphique. Comment ces valeurs sont calculées? Simplement avec l’IQR, soit l’Inter-Quartile-Range. IQR = Q3 - Q1

  • Q1 - 1.5*IQR
  • Q3 + 1.5*IQR

La fonction boxplot.stat permet d’identifier les valeurs anormales.

cim %>% 
  filter(metrics == "sessions") %>%
  filter(site %in% c("RTL Luxembourg")) %>%
  filter(date >= "2018-01-01", date < "2019-01-01") %>%
  mutate(month = lubridate::month(date)) %>% 
  group_by(month) %>%
  select(month, hits) %>% 
  nest() %>% 
  mutate(outliers = map(data, ~ boxplot.stats(pull(.x)))) %>% 
  pull(outliers) %>% # output is a list
  map("out") %>% # keep only the "out" element   
  set_names(paste0("month_", 12:01)) %>% #set month name
  compact() %>% # discard month with NULL
  enframe() %>% # transform list a nested list
  unnest(cols = c(value))
## # A tibble: 11 x 2
##    name      value
##    <chr>     <dbl>
##  1 month_11 312577
##  2 month_11 203102
##  3 month_11 212915
##  4 month_11 294541
##  5 month_11 205289
##  6 month_10 522478
##  7 month_10 578463
##  8 month_6  394929
##  9 month_6  431512
## 10 month_5  187793
## 11 month_2  317729

Approche 4 : tsoutliers du package forecast

Le package forecast propose la fonction tsoutliers pour identifier des outliers et retourner la position (index). La fonction propose aussi une valeur de remplacement. Petit détail, il faut en input une TimeSerie. Testons cela sur 2017 et 2018.

cim %>% 
  filter(metrics == "sessions") %>%
  filter(site %in% c("RTL Luxembourg")) %>%
  filter(date >= "2017-01-01", date < "2019-01-01") %>%
  select(date, hits) %>%
  arrange(date) %>%
  select(hits) %>% 
  ts() %>%
  forecast::tsoutliers()
## $index
## [1]  45 191 281 282 517 527 652 653
## 
## $replacements
## [1] 223845.0 230671.5 256749.3 293607.7 281399.0 315575.0 278185.7 311630.3

Les index retournés sont utilisés pour sélectionner les dates.

cim %>% 
  filter(metrics == "sessions") %>%
  filter(site %in% c("RTL Luxembourg")) %>%
  filter(date >= "2017-01-01", date < "2019-01-01") %>%
  select(date, hits) %>%
  arrange(date) %>% 
  slice(c(45, 191, 281, 282, 517, 527, 652, 653))
## # A tibble: 8 x 2
##   date         hits
##   <date>      <dbl>
## 1 2017-02-14 383061
## 2 2017-07-10 361443
## 3 2017-10-08 510494
## 4 2017-10-09 387819
## 5 2018-06-01 431512
## 6 2018-06-11 394929
## 7 2018-10-14 578463
## 8 2018-10-15 522478

Ce qui est intéressant ici c’est que les valeurs sont identifiées sur les 2 années sans groupement par mois. Les dates retournées sont les mêmes que celles identifiées dans l’approche 2.

Approche 5 : Control Chart

Moving Range

Cette méthode est inspirée de ce post post. Elle se repose sur le Moving Range, il s’agit de calculer la différence absolue entre chaque suite de valeurs (en quelque sorte les valeurs lagguées) divisée par une constante. Nous reviendrons sur cette constante un peu plus loin. Commençons par définir une nouvelle table.

En réalité il s’agit d’un control chart ou carte de qualité. Une carte de contrôle, ou plus exactement un graphique de contrôle sont des outils utilisés dans le domaine du contrôle de la qualité afin de maîtriser un processus. Elle permet de déterminer le moment où apparaît une cause particulière de variation d’une caractéristique, entraînant une altération du processus. Par exemple un processus de fabrication pourra être mis à l’arrêt avant de produire des pièces qui seront non conformes. (source : wikipedia)

La raison d’utiliser une plage c’est que cela permet de ne pas prendre l’écart-type (standard déviation - qui calcule la variation sur l’ensemble des valeurs) et donc de permettre l’identification de variations aléatoires non systématiques.

cim2 <- cim %>%
  arrange(date) %>% 
  filter(metrics == "sessions") %>%
  filter(site %in% c("RTL Luxembourg")) %>%
  filter(date >= "2018-01-01", date < "2019-01-01") %>%
  mutate(month = lubridate::month(date)) %>%
  group_by(month) %>% 
  mutate(mean.month = mean(hits), med.month = median(hits)) %>%
  mutate(sd = sd(hits), upper.sd = (mean.month + 3 * sd), lower.sd = (mean.month - 3 * sd)) %>% 
  ungroup()

Calculons la différence entre 2 dates journalières.

cim2 %>% 
  mutate(diff = hits - lag(hits)) %>% 
  select(date, month, hits, diff) %>% 
  ggplot() +
  aes(date, diff) +
  geom_col() +
  labs(title = "2018") +
  facet_wrap(~month, scale = "free_x")

Transformons les valeurs en valeurs absolues :

cim2 %>% 
  mutate(diff = abs(hits - lag(hits))) %>% 
  select(date, month, diff) %>% 
  ggplot() +
  aes(date, diff) +
  geom_col() +
  labs(title = "2018") +
  facet_wrap(~month, scale = "free_x")

Calculons la moyenne de ces différences : mean(mR)

cim2 %>% 
  mutate(diff = abs(hits - lag(hits))) %>% 
  mutate(mean_mr = mean(diff, na.rm = TRUE)) %>% 
  select(date, month, diff, mean_mr) %>% 
  pull(mean_mr) %>% 
  unique()
## [1] 28633.46

Et appliquons la division par la constante 1.128 pour N = 2. Soit seq_dv = mean_mr / (constante = 1.128)

cim2 %>% 
  mutate(diff = abs(hits - lag(hits))) %>% 
  mutate(mean_mr = mean(diff, na.rm = TRUE)) %>%
  mutate(seq_dev = mean_mr/1.128) %>%  #seq_dev calculation
  select(date, month, diff, seq_dev) %>% 
  pull(seq_dev) %>% 
  unique()
## [1] 25384.27

Constante d2(), d3()…

Kenith Grey, le développeur du package ggQC explique bien comment calculer les constantes utilisées avec les control charts. Vous pouvez consulter son article ici.

Lower et Upper Control Limit

Comme lors du calcul de l’intervalle de confiance d’une moyenne, nous appliquons cette valeur à la moyenne des sessions. Soit mean + 3 * seq_dev, mean - 3 * seq_dev.

cim2.control <- cim2 %>% 
  mutate(diff = abs(hits - lag(hits))) %>% 
  mutate(mean_mr = mean(diff, na.rm = TRUE)) %>%
  mutate(seq_dev = mean_mr/1.128) %>%
  mutate(lower.cl = mean.month - 3 * seq_dev, upper.cl = mean.month + 3 * seq_dev) %>% 
  select(date, month, hits, mean.month, lower.sd, upper.sd, lower.cl, upper.cl)

cim2.control %>% head()
## # A tibble: 6 x 8
##   date       month   hits mean.month lower.sd upper.sd lower.cl upper.cl
##   <date>     <dbl>  <dbl>      <dbl>    <dbl>    <dbl>    <dbl>    <dbl>
## 1 2018-01-01     1 258510    240528.  154745.  326311.  164375.  316681.
## 2 2018-01-02     1 235248    240528.  154745.  326311.  164375.  316681.
## 3 2018-01-03     1 282221    240528.  154745.  326311.  164375.  316681.
## 4 2018-01-04     1 250210    240528.  154745.  326311.  164375.  316681.
## 5 2018-01-05     1 213997    240528.  154745.  326311.  164375.  316681.
## 6 2018-01-06     1 191218    240528.  154745.  326311.  164375.  316681.
cim2.control %>% 
  ggplot() +
  aes(date, hits, col = (hits > upper.cl | hits < lower.cl)) +
  geom_line(aes(group = month), col = "black") +
  geom_point(size = 2, alpha = .75) +
  geom_hline(aes(yintercept = mean.month), col = "blue") +
  geom_hline(aes(yintercept = lower.cl), col = "red") +
  geom_hline(aes(yintercept = upper.cl), col = "red") +
  labs(title = "2018") +
  facet_wrap(~month, scale = "free_x") +
  theme(legend.position = "none")

Comparons avec la méthode de la déviation standard.

cim2.control %>% 
  ggplot() +
  aes(date, hits, col = (hits > upper.sd | hits < lower.sd)) +
  geom_line(aes(group = month), col = "black") +
  geom_point(size = 2, alpha = .75) +
  geom_hline(aes(yintercept = mean.month), col = "blue") +
  geom_hline(aes(yintercept = lower.cl), col = "red") +
  geom_hline(aes(yintercept = upper.cl), col = "red") +
  geom_hline(aes(yintercept = upper.sd), col = "purple") +
  geom_hline(aes(yintercept = lower.sd), col = "purple") +
  labs(title = "2018") +
  theme(legend.position = "none") +
  facet_wrap(~month, scale = "free_x")

On voit effectivement que la zone déterminée par la deviation standard est moins robuste aux outliers. Et sur 2017? Adaptons le graphique afin d’y afficher les dates.

cim %>%
  arrange(date) %>% 
  filter(metrics == "sessions") %>%
  filter(site %in% c("RTL Luxembourg")) %>%
  filter(date >= "2018-01-01", date < "2019-01-01") %>%
  mutate(month = lubridate::month(date)) %>%
  mutate(year = lubridate::year(date)) %>% 
  mutate(diff = abs(hits - lag(hits))) %>% 
  group_by(year, month) %>% 
  mutate(mean.month = mean(hits)) %>%
  mutate(mean_mr = mean(diff, na.rm = TRUE)) %>%
  mutate(seq_dev = mean_mr/1.128) %>%
  mutate(lower.cl = mean.month - 3 * seq_dev, upper.cl = mean.month + 3 * seq_dev) %>% 
  select(date, year, month, hits, mean.month, lower.cl, upper.cl) %>% 
  ggplot() +
  aes(date, hits, col = (hits > upper.cl | hits < lower.cl)) +
  geom_line(aes(group = month), col = "black") +
  geom_point(size = 4, alpha = 1) +
  geom_hline(aes(yintercept = mean.month), col = "blue") +
  scale_x_date(breaks = "day", date_labels = "%a", minor_breaks = NULL) +
  geom_hline(aes(yintercept = lower.cl), col = "red") +
  geom_hline(aes(yintercept = upper.cl), col = "red") +
  geom_text(aes(x = date, y = hits, label = format(date, "%d")), col = "black", size = 2) +
  theme(legend.position = "none", axis.text.x = element_text(size = 6, angle = 90)) +
  facet_wrap(year ~month, scales = "free_x")  

Sortons les dates anormales :

cim %>%
  arrange(date) %>% 
  filter(metrics == "sessions") %>%
  filter(site %in% c("RTL Luxembourg")) %>%
  filter(date >= "2018-01-01", date < "2019-01-01") %>%
  mutate(month = lubridate::month(date)) %>%
  mutate(year = lubridate::year(date)) %>% 
  mutate(diff = abs(hits - lag(hits))) %>% 
  group_by(year, month) %>% 
  mutate(mean.month = mean(hits)) %>%
  mutate(mean_mr = mean(diff, na.rm = TRUE)) %>%
  mutate(seq_dev = mean_mr/1.128) %>%
  mutate(lower.cl = mean.month - 3 * seq_dev, upper.cl = mean.month + 3 * seq_dev) %>% 
  select(site, date, year, month, hits, mean.month, lower.cl, upper.cl)%>% 
  filter(hits >= upper.cl | hits <= lower.cl) %>% 
  pull(date)
## [1] "2018-03-02" "2018-04-07" "2018-05-05" "2018-06-01" "2018-06-11"
## [6] "2018-10-14" "2018-10-15"

via le package qicharts2

Le package qicharts permet de réaliser différents types de control chart. Run chart, I chart…

qicharts2::qic(x = date, y = hits, data = cim2.control)

qicharts2::qic(x = date, y = hits, chart = "i", data = cim2.control)

Aller plus loin : Une fonction spécifique pour le CIM

Nous voici à la fin de cette exploration. Pour allez un peu plus loin, nous allons écrire une fonction simplifiant l’identification des jours dont les mesures de sessions dans ce cas sont aberrantes. Nous avons notre série temporelle en entrée et en sortie les dates supérieures à upper.cl ou inférieures à lower.cl. Nous y incluons aussi la génération d’un graphique pour visualiser les dates de la série.

detect_date <- function(df, metric, website, min.date, max.date, graph = TRUE, lcl = TRUE, list = TRUE) {

options(scipen = 999)  
  
# Calculte upper and lower control limit  
    
df %>%
  arrange(date) %>% 
  filter(metrics == metric) %>%
  filter(site %in% website) %>%
  filter(date >= min.date, date < max.date) %>%
  mutate(month = lubridate::month(date)) %>%
  mutate(year = lubridate::year(date)) %>% 
  group_by(site, year, month) %>% 
  mutate(diff = abs(hits - lag(hits))) %>% 
  mutate(mean.month = mean(hits)) %>%
  mutate(mean_mr = mean(diff, na.rm = TRUE)) %>%
  mutate(seq_dev = mean_mr/1.128) %>%
  mutate(lower.cl = mean.month - 3 * seq_dev, upper.cl = mean.month + 3 * seq_dev) %>% 
  select(site, date, year, month, hits, mean.month, lower.cl, upper.cl) %>% 
  group_by(site) %>% 
  nest() -> df.control

# Function to generate plot

draw_chart <- as_mapper(~ ggplot(.x) +
  aes(date, hits, col = !(hits > upper.cl | hits < lower.cl)) +
  geom_line(aes(group = month), col = "black") +
  geom_hline(aes(yintercept = mean.month), col = "blue") +
  scale_x_date(breaks = "day", date_labels = "%a", minor_breaks = NULL) +
  geom_hline(aes(yintercept = lower.cl), col = "red") +
  geom_hline(aes(yintercept = upper.cl), col = "red") +
  geom_point(size = 4, alpha = 1) +
  geom_text(aes(x = date, y = hits, label = format(date, "%d")), , col = "black", size = 2) +
  ggthemes::theme_solarized_2() + 
  theme(legend.position = "none", 
        axis.text.x = element_text(size = 6, angle = 90),
        axis.title = element_blank(),
        plot.margin = unit(c(1.5, 1.5, 1.5, 1.5), "cm")) +
  labs(title = .y) +
  facet_wrap(year ~month, scales = "free_x")  
)

df.control %>% 
  mutate(plot = map2(data, site, draw_chart)) -> df.control_graph


# Recode Site name in CDN

df.control %>%
  unnest(cols = c(data)) %>% 
  ungroup() %>%
  mutate(site = case_when(
    site == "RTL Luxembourg" ~ "rtl.lu",
    site == "L'essentiel" ~ "lessentiel.lu",
    site == "Wort.lu" ~ "wort.lu",
    site == "Tageblatt" ~ "tageblatt.lu",
    site == "Revue" ~ "revue.lu",
    site == "PaperJam" ~ "paperjam.lu",
    site == "Luxweb.com" ~ "luxweb.com",           
    site == "Le Jeudi" ~ "jeudi.lu",
    site == "IMMOTOP.LU" ~ "immotop.lu",
    site == "Editus" ~ "editus.lu",
    site == "atOffice.lu" ~ "atoffice.lu",
    site == "Luxtimes.lu" ~ "luxtimes.lu",          
    site == "Le Quotidien" ~ "quotidien.lu",
    site == "Eldoradio" ~ "eldoradio.lu",
    site == "Delano" ~ "delano.lu",
    site == "Luxauto" ~ "luxauto.lu",
    site == "Wortimmo.lu" ~ "wortimmo.lu" ,         
    site == "Explorator" ~ "explorator.lu",
    site == "atHome.lu" ~ "athome.lu")) -> df.control.recoded


# Filter output from argument set in the function

 if(lcl) {
df.control.recoded %>%
  filter(hits >= upper.cl | hits <= lower.cl) %>%
  select(site, date) -> df.control_date

 } else {   
df.control.recoded %>%
  filter(hits >= upper.cl) %>%
  select(site, date) -> df.control_date
}

# Transfom in list if set as argument

if(list) {
  df.control_date %>% 
    plyr::dlply(.variables = "site") %>% 
    map(2) -> df.control_date
}

# Show graph if set as argument

if(graph) {

  list(df.control_graph$plot, df.control_date) %>% 
    flatten()
  

} else {
  
 df.control_date 
  
  }

}

Testons la fonction.

detect_date(cim, 
            metric = "sessions", 
            website = c("RTL Luxembourg", "L'essentiel", "Wort.lu"), 
            min.date = "2018-01-01", 
            max.date = "2019-01-01", 
            graph = FALSE,
            lcl = FALSE,
            list = FALSE)
## # A tibble: 16 x 2
##    site          date      
##    <chr>         <date>    
##  1 rtl.lu        2018-06-01
##  2 rtl.lu        2018-06-11
##  3 rtl.lu        2018-10-14
##  4 rtl.lu        2018-10-15
##  5 wort.lu       2018-01-31
##  6 wort.lu       2018-03-20
##  7 wort.lu       2018-05-16
##  8 wort.lu       2018-06-01
##  9 wort.lu       2018-10-14
## 10 wort.lu       2018-10-15
## 11 wort.lu       2018-10-16
## 12 lessentiel.lu 2018-06-11
## 13 lessentiel.lu 2018-06-19
## 14 lessentiel.lu 2018-07-31
## 15 lessentiel.lu 2018-10-01
## 16 lessentiel.lu 2018-12-12

En sortie nous avons bien une liste avec des dates pour chaque site et le graphique approprié.

Encore plus loin : 2 fonctions pour composer une url de recherche.

Nous allons générer dans une table les URL afin de consulter l’actualité pour chaque date en sortie sur Google. Pour cela, nous allons créer 2 fonctions d’encodage.

# Creating encoder with as_mapper

encode_date <- as_mapper(~ .x %>% 
  as.Date() %>% 
  format("%m/%d/%Y") %>% 
  str_replace_all("/", "%2F")
  )


encode_url <- function(x, y) {
  
  paste0("https://www.google.lu/search?q=", y, "&tbs=cdr%3A1%2C", "cd_min%3A", 
         encode_date(x), "%2Ccd_max%3A", encode_date(x), "&tbm=nws")
}  

Astuce avec enframe() du package tibble

J’ai rencontré ce problème : transformer une liste en table dont chaque élément est de longueur différente. as_tibble fonctionne si la longueur de chaque élément est identique.

Par ex. cela fonctionne ici :

list(site1 = c("url1", "url2"), site2 = c("url2", "url3")) %>% 
  as_tibble()
## # A tibble: 2 x 2
##   site1 site2
##   <chr> <chr>
## 1 url1  url2 
## 2 url2  url3

Mais pas ici :

list(site1 = c("url1", "url2"), site2 = c("url2", "url3", "url3")) %>% 
  as_tibble()

La solution est de passé par enframe() du package tibble, puis unnest() pour avoir une table complète.

list(site1 = c("url1", "url2"), site2 = c("url2", "url3", "url3")) %>% 
  enframe() %>% 
  unnest(cols = c(value))
## # A tibble: 5 x 2
##   name  value
##   <chr> <chr>
## 1 site1 url1 
## 2 site1 url2 
## 3 site2 url2 
## 4 site2 url3 
## 5 site2 url3

Composons une fonction spécifique list_to_df:

list_to_df <- compose(unnest, enframe)


list(site1 = c("url1", "url2"), site2 = c("url2", "url3", "url3")) %>% list_to_df()
## # A tibble: 5 x 2
##   name  value
##   <chr> <chr>
## 1 site1 url1 
## 2 site1 url2 
## 3 site2 url2 
## 4 site2 url3 
## 5 site2 url3

Table des dates avec URL et occurrences.

Nous arrivons à la fin de notre article Nous allons générer une table HTML avec kabble et kabbleExtra comprenant les dates, répétition et URL vers Google en lien.

detect_date(cim, 
            metric = "sessions", 
            website = c("RTL Luxembourg", "L'essentiel", "Wort.lu"), 
            min.date = "2017-01-01", 
            max.date = "2019-01-01", 
            graph = FALSE,
            lcl = FALSE,
            list = FALSE) %>% 
  mutate(url = encode_url(date, site)) %>% 
  group_by(date) %>% 
  mutate(occurency = n()) %>%
  arrange(date, occurency, site) %>%
  select(date, occurency, site, url) %>% 
  mutate(url = kableExtra::text_spec("link", link = url)) %>% 
  knitr::kable("html", escape = FALSE) %>%
  kableExtra::kable_styling(bootstrap_options = c("striped", "hover", "condensed", "responsive")) 
date occurency site url
2017-01-13 1 rtl.lu link
2017-01-19 1 lessentiel.lu link
2017-02-14 3 lessentiel.lu link
2017-02-14 3 rtl.lu link
2017-02-14 3 wort.lu link
2017-03-23 3 lessentiel.lu link
2017-03-23 3 rtl.lu link
2017-03-23 3 wort.lu link
2017-07-10 1 rtl.lu link
2017-09-04 1 wort.lu link
2017-09-24 1 rtl.lu link
2017-10-06 1 lessentiel.lu link
2017-10-08 2 rtl.lu link
2017-10-08 2 wort.lu link
2017-10-09 2 rtl.lu link
2017-10-09 2 wort.lu link
2018-01-31 1 wort.lu link
2018-03-20 1 wort.lu link
2018-05-16 1 wort.lu link
2018-06-01 2 rtl.lu link
2018-06-01 2 wort.lu link
2018-06-11 2 lessentiel.lu link
2018-06-11 2 rtl.lu link
2018-06-19 1 lessentiel.lu link
2018-07-31 1 lessentiel.lu link
2018-10-01 1 lessentiel.lu link
2018-10-14 2 rtl.lu link
2018-10-14 2 wort.lu link
2018-10-15 2 rtl.lu link
2018-10-15 2 wort.lu link
2018-10-16 1 wort.lu link
2018-12-12 1 lessentiel.lu link