14  clickSelects et showSelected nommés

Dans ce chapitre nous expliquons comment utiliser les variables clickSelects et showSelected nommées pour créer des noms de sélecteurs basés sur des données. Cette fonctionnalité facilite l’écriture du code animint2 et accélère la compilation.

Plan du chapitre :

14.1 Téléchargement du jeu de données

Les données de l’exemple proviennent du package PeakSegJoint qui détecte les pics dans des séquences de données génomiques. Le code ci-dessous télécharge le jeu de données.

if(!requireNamespace("animint2data"))
  remotes::install_github("animint/animint2data")
Loading required namespace: animint2data
data(PSJ, package="animint2data")
sapply(PSJ, class)
           problem.labels                  problems            filled.regions 
             "data.frame"              "data.frame"              "data.frame" 
                 coverage                     first modelSelection.by.problem 
             "data.frame"                    "list"                    "list" 
       regions.by.problem          peaks.by.problem         error.total.chunk 
                   "list"                    "list"              "data.frame" 
          error.total.all 
             "data.frame" 

On peut voir, ci-dessus, que PSJ est une liste de plusieurs éléments (listes et tableaux de données).

14.2 Exploration des données PSJ à l’aide de ggplots statiques

Nous commençons par un graphique de quelques données ChIP-seq génomiques, des données séquentielles dont les valeurs augmentent dans les zones actives (généralement lorsque des gènes sont transcrits). Dans le code ci-dessous, nous affichons chaque échantillon dans un panneau séparé.

library(animint2)
ann.colors <- c(
  noPeaks="#f6f4bf",
  peakStart="#ffafaf",
  peakEnd="#ff4c4c",
  peaks="#a445ee")
(gg.cov <- ggplot()+
  scale_y_continuous(
    "couverture des lectures alignées",
    breaks=function(limits){
      floor(limits[2])
    })+
  scale_x_continuous(
    "position sur chr11 (kilobases = kb)")+
  coord_cartesian(xlim=c(118167.406, 118238.833))+
  geom_tallrect(aes(
    xmin=chromStart/1e3, xmax=chromEnd/1e3,
    fill=annotation),
    alpha=0.5,
    color="grey",
    data=PSJ$filled.regions)+
  scale_fill_manual(values=ann.colors)+
  theme_bw()+
  theme(panel.margin=grid::unit(0, "cm"))+
  facet_grid(sample.id ~ ., labeller=function(df){
    df$sample.id <- sub("McGill0", "", sub(" ", "\n", df$sample.id))
    df
  }, scales="free")+
  geom_line(aes(
    base/1e3, count),
    data=PSJ$coverage,
    color="grey50"))

La figure ci-dessus montre les données brutes bruitées sous forme d’un geom_line() gris. Les rectangles de couleur indiquent si le début ou la fin d’un pic doit être prédit dans une région et un échantillon donnés. Ensuite, nous ajoutons un panneau pour les problèmes de segmentation. Dans chaque problème, un algorithme a cherché un pic commun à tous les échantillons. Pour chaque problème, l’algorithme prédit les positions de début et de fin du pic de valeur élevée. Le code ci-dessous calcule un tableau de contingence avec une ligne pour chaque problème de ce type.

library(data.table)
(show.problems <- data.table(PSJ$problems)[
, y := problem.i/max(problem.i), by=bases.per.problem][])
     sample.id bases.per.problem problem.i              problem.name
  1:  problems              1629         1 chr11:118172963-118174591
  2:  problems              1629         2 chr11:118175144-118176570
 ---                                                                
203:  problems            104267         1 chr11:118125367-118194817
204:  problems            104267         2 chr11:118194818-118266077
     problemStart problemEnd          y
  1:    118172963  118174591 0.01818182
  2:    118175144  118176570 0.03636364
 ---                                   
203:    118125367  118194817 0.50000000
204:    118194818  118266077 1.00000000

Dans le code ci-dessus une colonne y est ajoutée pour l’affichage des problèmes.

(gg.cov.prob <- gg.cov+
  ggtitle("Sélection du problème")+
  geom_text(aes(
    chromStart/1e3, 0.9,
    label=sprintf(
      "%d taille moyenne des problèmes %.1f kb",
      problems, mean.bases/1e3)),
    showSelected="bases.per.problem",
    data=PSJ$problem.labels,
    hjust=0)+
  geom_segment(aes(
    problemStart/1e3, y,
    xend=problemEnd/1e3, yend=y),
    showSelected="bases.per.problem",
    clickSelects="problem.name",
    size=5,
    data=show.problems))

Ci-dessus, on voit qu’un panneau inférieur intitulé problems a été ajouté au graphique précédent. Le graphique statique ci-dessus est surchargé ; la version interactive sera lisible parce qu’elle n’affichera qu’une seule valeur de bases.per.problem à la fois. Pour sélectionner les différentes valeurs de bases.per.problem (taille du problème), nous utiliserons un autre graphique, qui présentera le meilleur taux d’erreur pour chaque taille de problème, comme le montrent les données ci-dessous.

(res.error <- data.table(PSJ$error.total.chunk))
    bases.per.problem fp fn errors regions min.bases.per.problem
 1:              1629  1  4      5      36              1349.501
 2:              2304  0  2      2      36              1908.686
---                                                             
12:             73728  0 12     12      36             61077.960
13:            104267  0 12     12      36             86377.166
    max.bases.per.problem
 1:              1966.387
 2:              2781.188
---                      
12:             88998.027
13:            125862.051

Le tableau de contingence ci-dessus comporte une ligne par valeur de bases.per.problem, un paramètre de taille de fenêtre coulissante, que nous allons explorer à l’aide de l’interactivité. Nous utilisons ces données pour tracer le graphique ci-dessous.

(gg.res.error <- ggplot()+
  ggtitle("choisir la taille du problème")+
  ylab("nombre minimal d’erreurs de prédiction d’étiquettes")+
  geom_line(aes(
    bases.per.problem, errors),
    data=res.error)+
  geom_tallrect(aes(
    xmin=min.bases.per.problem,
    xmax=max.bases.per.problem),
    clickSelects="bases.per.problem",
    alpha=0.5,
    data=res.error)+
  scale_x_log10())

La figure ci-dessus montre le nombre minimum d’erreurs d’étiquettes en fonction de la taille du problème. Des rectangles gris seront utilisés pour sélectionner la taille du problème.

Un paramètre de pénalité contrôle le nombre d’échantillons avec un pic commun, il est défini dans le tableau de données de sélection de modèle dans le code ci-dessous.

pdot <- function(L){
  out_list <- list()
  for(i in seq_along(L)){
    out_list[[i]] <- data.table(
      problem.dot=names(L)[[i]], L[[i]])
  }
  rbindlist(out_list)
}
(all.modelSelection <- pdot(PSJ$modelSelection.by.problem))
                        problem.dot bases.per.problem problem.i
  1: chr11.118172963.118174591peaks              1629         1
  2: chr11.118172963.118174591peaks              1629         1
 ---                                                           
979: chr11.118194818.118266077peaks            104267         2
980: chr11.118194818.118266077peaks            104267         2
                  problem.name problemStart problemEnd min.log.lambda
  1: chr11:118172963-118174591    118172963  118174591           -Inf
  2: chr11:118172963-118174591    118172963  118174591        4.56662
 ---                                                                 
979: chr11:118194818-118266077    118194818  118266077       12.13670
980: chr11:118194818-118266077    118194818  118266077       12.71684
     max.log.lambda model.complexity peaks   min.lambda   max.lambda errors
  1:        4.56662                4     4      0.00000     96.21835     NA
  2:        4.68868                3     3     96.21835    108.70956     NA
 ---                                                                       
979:       12.71684                1     1 186596.24134 333312.50992     14
980:            Inf                0     0 333312.50992          Inf     16

Le code ci-dessus utilise la fonction pdot() qui fait appel à la méthode liste de tableaux de données pour ajouter une colonne nommée problem.dot, laquelle servira ensuite à définir les sélecteurs dans la visualisation interactive. Nous traçons ci-dessous le nombre de pics et d’erreurs d’étiquettes, en fonction du paramètre de pénalité de l’algorithme.

long.modelSelection <- melt(
  data.table(all.modelSelection)[, errors := as.numeric(errors)],
  measure.vars=c("peaks","errors"))
log.lambda.range <- all.modelSelection[, c(
  min(max.log.lambda), max(min.log.lambda))]
modelSelection.labels <- unique(all.modelSelection[, data.table(
  problem.name,
  bases.per.problem,
  problemStart,
  problemEnd,
  log.lambda=mean(log.lambda.range),
  peaks=max(peaks)+0.5)])
(gg.model.selection <- ggplot()+
   scale_x_continuous("log(pénalité)")+
   geom_segment(aes(
     min.log.lambda, value,
     xend=max.log.lambda, yend=value),
     showSelected=c("bases.per.problem", "problem.name"),
     data=long.modelSelection,
     size=5)+
   geom_text(aes(
     log.lambda, peaks,
     label=sprintf(
       "%.1f kb dans le problème %s",
       (problemEnd-problemStart)/1e3, problem.name)),
     showSelected=c("bases.per.problem", "problem.name"),
     data=data.frame(modelSelection.labels, variable="peaks"))+
   ylab("")+
   facet_grid(variable ~ ., scales="free"))
Warning: Removed 717 rows containing missing values (geom_segment).

La figure ci-dessus montre errors (panneau supérieur) et peaks (panneau inférieur) en fonction de log(penalty). Là encore, cette version statique est surchargée ; l’interactivité sera utilisée pour rendre la figure lisible (en n’affichant que le sous-ensemble de données correspondant aux valeurs sélectionnées de bases.per.problem et problem.name).

14.3 Visualisation interactive des données (incomplète)

Dans cette section, nous combinons les ggplots de la section précédente pour créer une visualisation avec interactivité qui rélie les graphiques. Le code ci-dessous utilise theme_animint() pour associer certaines options d’affichage au graphique de couverture précédent, et l’option first est ajoutée pour spécifier quels sous-ensembles de données doivent être affichés en premier.

timing.incomplete.construct <- system.time({
  coverage.counts <- table(PSJ$coverage$sample.id)
  facet.rows <- length(coverage.counts)+1
  vis.incomplete <- animint(
    out.dir="ch14incomplete",
    first=list(
      bases.per.problem=6516,
      problem.name="chr11:118174946-118177139"),
    coverage=gg.cov.prob+theme_animint(
      last_in_row=TRUE, colspan=2,
      width=800, height=facet.rows*100),
    resError=gg.res.error,
    modelSelection=gg.model.selection)
})

La sortie temporelle ci-dessus montre que la définition initiale est rapide. Générer cette visualisation des données préliminaires (incomplètes) dans le code ci-dessous est également rapide.

before.incomplete <- Sys.time()
vis.incomplete
cat(elapsed.incomplete <- Sys.time()-before.incomplete, "secondes\n")
7.640906 secondes

Nous voyons ci-dessus une visualisation des données avec trois graphiques.

  • Dans la partie supérieure, quatre profils de données ChIP-seq sont affichés, ainsi qu’un panneau de problèmes qui divise l’axe X en sections dans lesquels l’algorithme de segmentation s’exécute.
  • En cliquant sur le graphique en bas à gauche, on sélectionne la taille du problème, ce qui met à jour les problèmes affichés en haut.
  • Le graphique en bas à droite montre le nombre de pics et d’erreurs en fonction de la pénalité (plus grande pour moins de pics).

14.4 Ajouter de l’interactivité à l’aide de boucles for

Dans cette section, nous ajoutons des couches aux ggplots précédents en utilisant une boucle for, une approche sous-optimale, que nous présentons pour la comparer à une meilleure approche (clickSelects et showSelected nommés), présentée dans la prochaine section. La visualisation de la section précédente est incomplète. Nous aimerions ajouter

  • des rectangles dans le graphique en bas à gauche pour nous permettre de sélectionner le nombre de pics prédits dans un problème donné ;
  • des segments et des rectangles dans le graphique du haut pour montrer les pics prédits et les erreurs d’étiquettes.

Une façon (inefficace) d’ajouter ces éléments serait d’utiliser la boucle for codée ci-dessous. Pour chaque problème, il existe un sélecteur (appelé problem.dot) pour le nombre de pics dans ce problème. Dans cette boucle for, nous ajoutons donc quelques couches aux graphiques coverage et modelSelection à l’aide de clickSelects="problem.dot" ou showSelected="problem.dot" .

vis.first <- vis.incomplete
vis.first$first <- c(vis.incomplete$first, PSJ$first)
vis.first$modelSelection <- vis.first$modelSelection+
  ggtitle("choisir le nombre d’échantillons ayant un pic commun")
print(timing.for.construct <- system.time({
  vis.for <- vis.first
  vis.for$title <- "PSJ avec boucles for"
  vis.for$out.dir <- "ch14for"
  for(problem.dot in names(PSJ$modelSelection.by.problem)){
    if(problem.dot %in% names(PSJ$peaks.by.problem)){
      peaks <- PSJ$peaks.by.problem[[problem.dot]]
      peaks[[problem.dot]] <- peaks$peaks
      prob.peaks.names <- c(
        "bases.per.problem", "problem.i", "problem.name",
        "chromStart", "chromEnd", problem.dot)
      prob.peaks <- unique(data.frame(peaks)[, prob.peaks.names])
      prob.peaks$sample.id <- "problems"
      vis.for$coverage <- vis.for$coverage +
        geom_segment(aes(
          chromStart/1e3, 0,
          xend=chromEnd/1e3, yend=0),
          clickSelects="problem.name",
          showSelected=c(problem.dot, "bases.per.problem"),
          data=peaks, size=7, color="deepskyblue")
    }
    modelSelection.dt <- PSJ$modelSelection.by.problem[[problem.dot]]
    modelSelection.dt[[problem.dot]] <- modelSelection.dt$peaks
    vis.for$modelSelection <- vis.for$modelSelection+
      geom_tallrect(aes(
        xmin=min.log.lambda, 
        xmax=max.log.lambda), 
        clickSelects=problem.dot,
        showSelected=c("problem.name", "bases.per.problem"),
        data=modelSelection.dt, alpha=0.5)
  }
}))
   user  system elapsed 
  0.942   0.000   0.942 

Notez le temps d’exécution du code ci-dessus, qui comprend uniquement l’évaluation du code R pour les ggplots (et non l’affichage). Ensuite, nous compilons la visualisation des données.

before.for <- Sys.time()
vis.for
Warning in checkSingleShowSelectedValue(meta$selectors): showSelected variables
with only 1 level: chr11.118184422.118184700peaks,
chr11.118192951.118193582peaks, chr11.118203893.118204314peaks
cat(elapsed.for <- Sys.time()-before.for, "seconds\n")
1.144312 seconds

Notez que la compilation nécessite plusieurs secondes, car il y a de nombreux geoms (cliquez sur « Show download status table » pour les voir tous). Par rapport à la visualisation des données de la section précédente, celle-ci présente

  • des segments bleus qui apparaissent dans le graphique des données de couverture supérieure, pour indiquer les pics prédits ;
  • des rectangles de sélection qui peuvent être cliqués dans le graphique en bas à droite, pour modifier le nombre d’échantillons présentant un pic dans le problème sélectionné.

Dans la prochaine section, nous allons créer la même visualisation des données, mais de manière plus efficace.

14.5 Définition de l’interactivité à l’aide de clickSelects et showSelected nommés

Dans cette section, nous créons une version plus efficace de la précédente visualisation des données en utilisant des clickSelects et des showSelected nommés. En général, toute visualisation des données définie à l’aide de boucles for dans le code R peut être optimisée par cette méthode. Tout d’abord, nous définissons quelques données communes.

(sample.peaks <- pdot(PSJ$peaks.by.problem))
                         problem.dot bases.per.problem problem.i
   1: chr11.118172963.118174591peaks              1629         1
   2: chr11.118172963.118174591peaks              1629         1
  ---                                                           
1998: chr11.118194818.118266077peaks            104267         2
1999: chr11.118194818.118266077peaks            104267         2
                   problem.name problemStart problemEnd peaks  sample.id
   1: chr11:118172963-118174591    118172963  118174591     1 McGill0091
   2: chr11:118172963-118174591    118172963  118174591     2 McGill0091
  ---                                                                   
1998: chr11:118194818-118266077    118194818  118266077     4 McGill0091
1999: chr11:118194818-118266077    118194818  118266077     4 McGill0322
      chromStart  chromEnd     mean
   1:  118173535 118173819 3.802817
   2:  118173535 118173819 3.802817
  ---                              
1998:  118209753 118218118 2.190317
1999:  118209753 118218118 2.810879

La sortie ci-dessus montre un tableau avec une ligne par pic qui peut être affiché, pour différents échantillons, problèmes, et choix interactifs des variables bases.per.problem et peaks. Notez la colonne problem.dot : elle détermine le nom du sélecteur qui stockera le nombre de pics sélectionné pour le problème.

L’idée principale du code ci-dessous est celle-ci : pour chaque problème, un sélecteur est défini par la colonne problem.dot pour le nombre de pics dans ce problème. Nous utilisons showSelected=c(problem.dot="peaks") et clickSelects=c(problem.dot="peaks") pour indiquer que le nom du sélecteur se trouve dans la colonne problem.dot, et que la valeur de la sélection se trouve dans la colonne peaks. Le compilateur animint2dir() crée une variable de sélection pour chaque valeur unique de problem.dot (et il utilise les valeurs correspondantes dans peaks pour définir/mettre à jour la/les valeur/geoms sélectionnée(s)).

print(timing.named.construct <- system.time({
  vis.named <- vis.first
  vis.named$title <- "PSJ avec clickSelects et showSelected nommés"
  vis.named$out.dir <- "ch14named"
  vis.named$coverage <- vis.named$coverage+
    geom_segment(aes(
      chromStart/1e3, 0,
      xend=chromEnd/1e3, yend=0),
      clickSelects="problem.name",
      showSelected=c(problem.dot="peaks", "bases.per.problem"),
      data=sample.peaks, size=7, color="deepskyblue")
  vis.named$modelSelection <- vis.named$modelSelection+
    geom_tallrect(aes(
      xmin=min.log.lambda,
      xmax=max.log.lambda),
      clickSelects=c(problem.dot="peaks"),
      showSelected=c("problem.name", "bases.per.problem"),
      data=all.modelSelection, alpha=0.5)
}))
   user  system elapsed 
  0.003   0.000   0.003 

De toute évidence, le code R ci-dessus, qui utilise les clickSelects et showSelected nommés, est beaucoup plus rapide. Nous le compilons ci-dessous.

before.named <- Sys.time()
vis.named
cat(elapsed.named <- Sys.time()-before.named, "seconds\n")
7.32982 seconds

L’animint produit ci-dessus devrait être identique à l’autre visualisation des données de la section précédente. Les temps d’exécution ci-dessus montrent que les clickSelects et showSelected nommés sont beaucoup plus rapides que les boucles for, tant au niveau de la définition que de la compilation.

14.6 Comparaison de l’utilisation du disque

Dans cette section, nous calculons l’utilisation du disque pour les deux méthodes.

vis.dirs.vec <- c("ch14incomplete", "ch14for", "ch14named")
vis.dirs.text <- paste(vis.dirs.vec, collapse=" ")
(cmd <- paste("du -ks", vis.dirs.text))
[1] "du -ks ch14incomplete ch14for ch14named"
(kb.dt <- fread(cmd=cmd, col.names=c("kilobytes", "path")))
   kilobytes           path
1:       988 ch14incomplete
2:      3120        ch14for
3:      1408      ch14named

Le tableau de contingence ci-dessus montre que la visualisation des données définie à l’aide de boucles for prend environ deux fois plus d’espace disque que la visualisation des données utilisant des clickSelects et showSelected nommés.

14.7 Résumé du chapitre et exercices

Le tableau ci-dessous résume l’utilisation du disque et les temps d’exécution présentés dans ce chapitre.

data.table(
  kb.dt,
  construct.seconds=c(
    timing.incomplete.construct[["elapsed"]],
    timing.for.construct[["elapsed"]],
    timing.named.construct[["elapsed"]]),
  compile.seconds=as.numeric(c(
    elapsed.incomplete,
    elapsed.for,
    elapsed.named)))
   kilobytes           path construct.seconds compile.seconds
1:       988 ch14incomplete             0.001        7.640906
2:      3120        ch14for             0.942       68.658735
3:      1408      ch14named             0.003        7.329820

Il ressort clairement du tableau de contingence ci-dessus que les clickSelects et showSelected nommés sont plus efficaces à ces deux égards et devraient être privilégiés aux boucles for.

Exercices :

  • Utilisez des clickSelects et des showSelected nommés pour créer une visualisation illustrant le surapprentissage et le sous-apprentissage, comme nous l’avons fait dans cette visualisation du modèle linéaire et des plus proches voisins.
  • Utilisez des clickSelects et des showSelected nommés pour créer une visualisation de données dans votre domaine d’expertise.

Dans le chapitre 15, nous vous expliquerons comment visualiser les algorithmes de recherche de racines (« zero-finding »).