Le graphique linéaire comporte trop de lignes, existe-t-il une meilleure solution?

31

J'essaie de représenter graphiquement le nombre d'actions par les utilisateurs (dans ce cas, "j'aime") au fil du temps.

J'ai donc "Nombre d'actions" comme mon axe y, mon axe x est le temps (semaines), et chaque ligne représente un utilisateur.

Mon problème est que je veux regarder ces données pour un ensemble d'environ 100 utilisateurs. Un graphique linéaire devient rapidement un gâchis avec 100 lignes. Existe-t-il un meilleur type de graphique que je peux utiliser pour afficher ces informations? Ou devrais-je envisager de pouvoir activer / désactiver des lignes individuelles?

J'aimerais voir toutes les données à la fois, mais être capable de discerner le nombre d'actions avec une grande précision n'est pas très important.

Pourquoi je fais ça

Pour un sous-ensemble de mes utilisateurs (les meilleurs utilisateurs), je veux savoir lesquels n'ont peut-être pas aimé une nouvelle version de l'application qui a été déployée à une certaine date. Je recherche une baisse significative du nombre d'actions des utilisateurs individuels.

réglementer
la source
5
Avez-vous envisagé de rendre les lignes semi-transparentes en modifiant l'alpha utilisé pour les tracer?
Fomite
1
@EpiGrad Suggestion raisonnable, mais cela ne rendrait pas vraiment plus facile de voir ce que je recherche.
réglementer ce
1
@regulatethis Je suggérerais une approche "petit multiple" en utilisant le facet_wrap fonction de ggplot2 pour créer un bloc de 4 x 5 graphiques (4 lignes, 5 colonnes - ajuster en fonction du rapport hauteur / largeur souhaité) avec ~ 5 utilisateurs par graphique. Cela devrait être assez clair et vous pouvez le faire évoluer jusqu'à environ 10 utilisateurs par graphique, ce qui donne de la place pour 200 avec un tracé 4x5 ou 360 avec un tracé 6x6.
SlowLearner

Réponses:

31

Je voudrais suggérer une analyse préliminaire (standard) pour éliminer les principaux effets de (a) la variation parmi les utilisateurs, (b) la réponse typique parmi tous les utilisateurs au changement, et (c) la variation typique d'une période à l'autre .

Une façon simple (mais nullement la meilleure) de procéder consiste à effectuer quelques itérations de "polissage médian" sur les données pour balayer les médianes des utilisateurs et les médianes des périodes, puis lisser les résidus au fil du temps. Identifiez les lissés qui changent beaucoup: ce sont les utilisateurs que vous souhaitez mettre en valeur dans le graphique.

Parce que ce sont des données de comptage, c'est une bonne idée de les ré-exprimer en utilisant une racine carrée.

À titre d'exemple de ce qui peut en résulter, voici un ensemble de données simulé sur 60 semaines de 240 utilisateurs qui entreprennent généralement 10 à 20 actions par semaine. Un changement dans tous les utilisateurs s'est produit après la semaine 40. Trois d'entre eux ont été «invités» à répondre négativement au changement. Le graphique de gauche montre les données brutes: nombre d'actions par utilisateur (avec des utilisateurs distingués par leur couleur) dans le temps. Comme l'affirme la question, c'est un gâchis. Le graphique de droite montre les résultats de cet EDA - dans les mêmes couleurs qu'avant - avec les utilisateurs inhabituellement réactifs automatiquement identifiés et mis en évidence. L'identification - bien qu'elle soit quelque peu ponctuelle - est complète et correcte (dans cet exemple).

Figure 1

Voici le Rcode qui a produit ces données et effectué l'analyse. Il pourrait être amélioré de plusieurs manières, notamment

  • Utiliser un polish médian complet pour trouver les résidus, plutôt qu'une seule itération.

  • Lissage des résidus séparément avant et après le point de changement.

  • Peut-être en utilisant un algorithme de détection des valeurs aberrantes plus sophistiqué. L'actuel marque simplement tous les utilisateurs dont la plage de résidus est plus de deux fois la plage médiane. Bien que simple, il est robuste et semble bien fonctionner. (Une valeur réglable par l'utilisateur,threshold peut être ajustée pour rendre cette identification plus ou moins stricte.)

Les tests suggèrent néanmoins que cette solution fonctionne bien pour un large éventail de comptes d'utilisateurs, de 12 à 240 ou plus.

n.users <- 240        # Number of users (here limited to 657, the number of colors)
n.periods <- 60       # Number of time periods
i.break <- 40         # Period after which change occurs
n.outliers <- 3       # Number of greatly changed users
window <- 1/5         # Temporal smoothing window, fraction of total period
response.all <- 1.1   # Overall response to the change
threshold <- 2        # Outlier detection threshold

# Create a simulated dataset
set.seed(17)
base <- exp(rnorm(n.users, log(10), 1/2))
response <- c(rbeta(n.users - n.outliers, 9, 1),
              rbeta(n.outliers, 5, 45)) * response.all
actual <- cbind(base %o% rep(1, i.break), 
                base * response %o% rep(response.all, n.periods-i.break))
observed <- matrix(rpois(n.users * n.periods, actual), nrow=n.users)

# ---------------------------- The analysis begins here ----------------------------#
# Plot the raw data as lines
set.seed(17)
colors = sample(colors(), n.users) # (Use a different method when n.users > 657)
par(mfrow=c(1,2))
plot(c(1,n.periods), c(min(observed), max(observed)), type="n",
     xlab="Time period", ylab="Number of actions", main="Raw data")
i <- 0
apply(observed, 1, function(a) {i <<- i+1; lines(a, col=colors[i])})
abline(v = i.break, col="Gray")  # Mark the last period before a change

# Analyze the data by time period and user by sweeping out medians and smoothing
x <- sqrt(observed + 1/6)                        # Re-express the counts
mean.per.period <- apply(x, 2, median)
residuals <- sweep(x, 2, mean.per.period)
mean.per.user <- apply(residuals, 1, median)
residuals <- sweep(residuals, 1, mean.per.user)

smooth <- apply(residuals, 1, lowess, f=window)  # Smooth the residuals
smooth.y <- sapply(smooth, function(s) s$y)      # Extract the smoothed values
ends <- ceiling(window * n.periods / 4)          # Prepare to drop near-end values
range <- apply(smooth.y[-(1:ends), ], 2, function(x) max(x) - min(x))

# Mark the apparent outlying users
thick <- rep(1, n.users)
thick[outliers <- which(range >= threshold * median(range))] <- 3
type <- ifelse(thick==1, 3, 1)

cat(outliers) # Print the outlier identifiers (ideally, the last `n.outliers`)

# Plot the residuals
plot(c(1,n.periods), c(min(smooth.y), max(smooth.y)), type="n",
     xlab="Time period", ylab="Smoothed residual root", main="Residuals")
i <- 0
tmp <- lapply(smooth, 
       function(a) {i <<- i+1; lines(a, lwd=thick[i], lty=type[i], col=colors[i])})
abline(v = i.break, col="Gray")
Whuber
la source
3
threshold2.5n.users <- 500n.outliers <- 100threshold <- 2.5
16

En général, je trouve que plus de deux ou trois lignes sur une seule facette d'une intrigue commencent à être difficiles à lire (même si je le fais toujours tout le temps). C'est donc un exemple intéressant de ce qu'il faut faire lorsque vous avez quelque chose qui pourrait être conceptuellement un tracé à 100 facettes. Une façon possible consiste à dessiner les 100 facettes, mais au lieu d'essayer de les mettre toutes sur la page en même temps, en les regardant une par une dans une animation.

Nous avons en fait utilisé cette technique dans mon travail - nous avons initialement créé l'animation montrant 60 tracés de ligne différents en arrière-plan pour un événement (le lancement d'une nouvelle série de données), puis nous avons constaté que, ce faisant, nous avons en fait repris certaines caractéristiques des données qui n'avait pas été visible dans les tracés à facettes avec 15 ou 30 facettes par page.

Voici donc une autre façon de présenter les données brutes, avant de commencer à supprimer l'utilisateur et les effets de temps typiques, comme recommandé par @whuber. Ceci est présenté comme une alternative supplémentaire à sa présentation des données brutes - je vous recommande vivement de poursuivre ensuite l'analyse selon des lignes telles que celles qu'il suggère.

Une façon de contourner ce problème consiste à produire séparément les graphiques de séries chronologiques de 100 (ou 240 dans l'exemple de @ whuber) et de les assembler en une animation. Le code ci-dessous produira 240 images distinctes de ce type et vous pourrez ensuite utiliser un logiciel de création de films gratuit pour en faire un film. Malheureusement, la seule façon de le faire et de conserver une qualité acceptable était un fichier de 9 Mo, mais si vous n'avez pas besoin de l'envoyer sur Internet, cela ne peut pas être un problème et de toute façon je suis sûr qu'il existe des moyens de contourner cela avec un peu plus avertis en animation. Le package d'animation dans R pourrait être utile ici (vous permet de tout faire dans un appel de R) mais je l'ai gardé simple pour cette illustration.

J'ai fait l'animation de telle sorte qu'elle dessine chaque ligne en noir intense puis laisse une ombre verte semi-transparente pâle derrière pour que l'œil obtienne une image progressive des données accumulées. Il y a à la fois des risques et des opportunités - l'ordre dans lequel les lignes sont ajoutées laissera une impression différente, vous devriez donc envisager de la rendre significative d'une manière ou d'une autre.

Voici quelques photos du film, qui utilise les mêmes données que @whuber a généré: entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici

# ---------------------------- Data generation - by @whuber ----------------------------#

n.users <- 240        # Number of users (here limited to 657, the number of colors)
n.periods <- 60       # Number of time periods
i.break <- 40         # Period after which change occurs
n.outliers <- 3       # Number of greatly changed users
window <- 1/5         # Temporal smoothing window, fraction of total period
response.all <- 1.1   # Overall response to the change
threshold <- 2        # Outlier detection threshold

# Create a simulated dataset
set.seed(17)
base <- exp(rnorm(n.users, log(10), 1/2))
response <- c(rbeta(n.users - n.outliers, 9, 1),
              rbeta(n.outliers, 5, 45)) * response.all
actual <- cbind(base %o% rep(1, i.break), 
                base * response %o% rep(response.all, n.periods-i.break))
observed <- matrix(rpois(n.users * n.periods, actual), nrow=n.users)

# ---------------------------- The analysis begins here ----------------------------#

# Alternative presentation of original data 
# 
setwd("eg animation")

for (i in 1:n.users){
    png(paste("line plot", i, ".png"),600,600,res=60)
    plot(c(1,n.periods), c(min(observed), max(observed)), 
        xlab="Time period", ylab="Number of actions", 
        main="Raw data", bty="l", type="n")
    if(i>1){apply(observed[1:i,], 1, function(a) {lines(a, col=rgb(0,100,0,50,maxColorValue=255))})}
    lines(observed[i,], col="black", lwd=2)
    abline(v = i.break, col="Gray")  # Mark the last period before a change
    text(1,60,i)
    dev.off()
}

##
# Then proceed to further analysis eg as set out by @whuber
Peter Ellis
la source
+1, c'est une bonne idée. Vous pouvez également lancer une nouvelle fenêtre de périphérique à l'aide de windows()ou quartz(), puis imbriquer votre for()boucle à l'intérieur. NB, vous devrez mettre un Sys.sleep(1)au bas de votre boucle afin que vous puissiez réellement voir les itérations. Bien sûr, cette stratégie n'enregistre pas réellement un fichier vidéo - il vous suffit de le réexécuter à chaque fois que vous souhaitez le revoir.
gung - Réintègre Monica
+1 Très bonne idée - je vais essayer cela la prochaine fois que j'aurai. (GTW, Mathematica , par exemple, simplifie la création et la sauvegarde de telles animations.)
whuber
Idée étonnante - une animation dans ce sens (ou le code et les données à générer) ferait une annexe en ligne très sexy à une publication.
N Brouwer
7

L'une des choses les plus faciles à faire est un boxplot. Vous pouvez immédiatement voir comment évoluent les médianes de votre échantillon et quels jours ont le plus de valeurs aberrantes.

day <- rep(1:10, 100)
likes <- rpois(1000, 10)
d <- data.frame(day, likes)
library(ggplot2)
qplot(x=day, y=likes, data=d, geom="boxplot", group=day)

entrez la description de l'image ici

Pour une analyse individuelle, je suggère de prélever un petit échantillon aléatoire de vos données et d'analyser des séries chronologiques distinctes.

jem77bfp
la source
1
Solution intéressante, mais ce que je veux vraiment pouvoir voir comment est le "changement" par utilisateur. Je veux voir les fluctuations d'activité pour les utilisateurs individuels. C'est pourquoi j'ai choisi une ligne au départ, mais la visualisation est tout simplement trop encombrée maintenant.
réglementer ce
Eh bien, cela dépend vraiment des modèles que vous souhaitez voir dans vos données, peut-être que si vous pouviez nous dire ce que vous essayez de découvrir, nous pourrions trouver une solution.
jem77bfp
Pour un sous-ensemble de mes utilisateurs (les meilleurs utilisateurs), je veux savoir lesquels n'ont peut-être pas aimé une nouvelle version de l'application qui a été déployée à une certaine date. Je recherche une baisse significative du nombre d'actions des utilisateurs individuels.
régule ce
Bienvenue sur le site @ jem77bfp. il a dit qu'il voulait voir toutes les données. Mais ce serait bien d'avoir plus de détails, je suis d'accord.
Peter Flom - Réintègre Monica
+1 - au lieu de visualiser les diagrammes rectangulaires, mais il peut être utile de connecter les statistiques récapitulatives dans des graphiques linéaires. Voir ma réponse pour un exemple et une discussion ci-dessous.
Andy W
7

Sûr. Tout d'abord, triez par nombre moyen d'actions. Faites ensuite (disons) 4 graphiques de 25 lignes chacun, un pour chaque quartile. Cela signifie que vous pouvez réduire les axes y (mais clarifier l'étiquette de l'axe y). Et avec 25 lignes, vous pouvez les faire varier selon le type de ligne et la couleur et peut-être tracer le symbole et obtenir une certaine clarté

Ensuite, empilez les graphiques verticalement avec un seul axe temporel.

Ce serait assez facile dans R ou SAS (du moins si vous avez la version 9 de SAS).

Peter Flom - Réintégrer Monica
la source
2
+1 - Je suggérerais cependant encore moins de lignes par petit multiple! Voir mon article de blog sur le sujet et un exemple. Le tri est également une excellente idée, et d'autres potentiels pourraient inclure la valeur à la ligne de base ou au suivi, ou des mesures de changement (telles que la pente positive ou négative, le pourcentage de changement, etc.).
Andy W
Agréable! Qu'est-ce que le blog communautaire? Comment accéder ou écrire pour cela?
Peter Flom - Réintègre Monica
3
n'hésitez pas à vous arrêter dans la salle de discussion de Skewed Distribution pour savoir comment rejoindre le blog. Nous sommes toujours ouverts à plus de contributions des membres de la communauté.
Andy W
0

Je trouve que lorsque vous manquez d'options de type de graphique et de paramètres de graphique, l'introduction du temps par le biais de l'animation est la meilleure façon d'afficher, car elle vous donne une dimension supplémentaire pour travailler et vous permet d'afficher plus d'informations de manière facile à suivre. . Votre objectif principal doit être l'expérience utilisateur final.

DataDancer
la source
Aviez-vous quelque chose en tête qui diffère de la solution que Peter Ellis a publiée ici ? Dans l'affirmative, pourriez-vous nous en dire plus?
whuber
0

Si vous êtes le plus intéressé par le changement pour les utilisateurs individuels, c'est peut-être une bonne situation pour une collection de Sparklines (comme cet exemple de The Pudding ):

Exemple de sparklines de pudding.cool

Ceux-ci sont assez détaillés, mais vous pouvez afficher beaucoup plus de graphiques à la fois en supprimant les étiquettes et les unités des axes.

De nombreux outils de données les ont intégrés ( Microsoft Excel a des graphiques sparkline ), mais je suppose que vous voudriez tirer un package pour les construire dans R.

bryanbraun
la source