The French version of this document is no longer maintained: be sure to check the more up-to-date English version.

Graphiques avec R

Exemples de données
Données univariées quantitatives
Données univariées ordonnées
Données univariées qualitatives
Données bivariées quantitatives
Données bivariées qualitative/quantitative
Données bivariées qualitatives
Effets graphiques
Données multivariées quantitatives
Données multivariées en partie qualitatives
Treillis

Dans cette partie, nous montrons comment transformer des données en graphiques avec R, qu'il s'agisse de graphiques simples pour des données univariées ou bivariées, ou des graphiques dont la compréhension requiert un peu d'algèbre linéaire ou des algorithmes non triviaux.

Exemples de données

Il y a plein d'exemples de données qui viennent avec R et avec lesquelles on peut jouer.

Un exemple

En voici un.

> ?faithful
> data(faithful)
> faithful
    eruptions waiting
1       3.600      79
2       1.800      54
3       3.333      74
...
270     4.417      90
271     1.817      46
272     4.467      74

> str(faithful)
`data.frame':   272 obs. of  2 variables:
 $ eruptions: num  3.60 1.80 3.33 2.28 4.53 ...
 $ waiting  : num  79 54 74 62 85 55 88 85 51 85 ...

D'autres exemples

Pour regarder quelles sont les données présentes dans un paquetage :

data(package='ts')

Data sets in package `ts':

AirPassengers           Monthly Airline Passenger Numbers 1949-1960
austres                 Quarterly Time Series of the Number of
                        Australian Residents
beavers                 Body Temperature Series of Two Beavers
BJsales                 Sales Data with Leading Indicator.
EuStockMarkets          Daily Closing Prices of Major European Stock
                        Indices, 1991-1998.
JohnsonJohnson          Quarterly Earnings per Johnson & Johnson Share
LakeHuron               Level of Lake Huron 1875--1972
lh                      Luteinizing Hormone in Blood Samples
lynx                    Annual Canadian Lynx trappings 1821--1934
Nile                    Flow of the River Nile
nottem                  Average Monthly Temperatures at Nottingham,
                        1920--1939
sunspot                 Yearly Sunspot Data, 1700--1988. Monthly
                        Sunspot Data, 1749--1997.
treering                Yearly Treering Data, -6000--1979.
UKDriverDeaths          Road Casualties in Great Britain 1969--84
UKLungDeaths            Monthly Deaths from Lung Diseases in the UK
UKgas                   UK Quarterly Gas Consumption
USAccDeaths             Accidental Deaths in the US 1973--1978
WWWusage                Internet Usage per Minute

Les données présentes dans le paquetage "base" :

Data sets in package `base':

Formaldehyde            Determination of Formaldehyde concentration
HairEyeColor            Hair and eye color of statistics students
InsectSprays            Effectiveness of insect sprays
LifeCycleSavings        Intercountry life-cycle savings data
OrchardSprays           Potency of orchard sprays
PlantGrowth             Results from an experiment on plant growth
Titanic                 Survival of passengers on the Titanic
ToothGrowth             The effect of vitamin C on tooth growth in
                        guinea pigs
UCBAdmissions           Student admissions at UC Berkeley
USArrests               Violent crime statistics for the USA
USJudgeRatings          Lawyers' ratings of state judges in the US
                        Superior Court
USPersonalExpenditure   Personal expenditure data
VADeaths                Death rates in Virginia (1940)
airmiles                Passenger miles on US airlines 1937-1960
airquality              New York Air Quality Measurements
anscombe                Anscombe's quartet of regression data
attenu                  Joiner-Boore Attenuation Data
attitude                Chatterjee-Price Attitude Data
cars                    Speed and Stopping Distances for Cars
chickwts                The Effect of Dietary Supplements on Chick
                        Weights
co2                     Moana Loa Atmospheric CO2 Concentrations
discoveries             Yearly Numbers of `Important' Discoveries
esoph                   (O)esophageal Cancer Case-control study
euro                    Conversion rates of Euro currencies
eurodist                Distances between European Cities
faithful                Old Faithful Geyser Data
freeny                  Freeny's Revenue Data
infert                  Secondary infertility matched case-control
                        study
iris                    Edgar Anderson's Iris Data as data.frame
iris3                   Edgar Anderson's Iris Data as 3-d array
islands                 World Landmass Areas
longley                 Longley's Economic Regression Data
morley                  Michaelson-Morley Speed of Light Data
mtcars                  Motor Trend Car Data
nhtemp                  Yearly Average Temperatures in New Haven CT
phones                  The Numbers of Telephones
precip                  Average Precipitation amounts for US Cities
presidents              Quarterly Approval Ratings for US Presidents
pressure                Vapour Pressure of Mercury as a Function of
                        Temperature
quakes                  Earthquake Locations and Magnitudes in the
                        Tonga Trench
randu                   Random Numbers produced by RANDU
rivers                  Lengths of Major Rivers in North America
sleep                   Student's Sleep Data
stackloss               Brownlee's Stack Loss Plant Data
state                   US State Facts and Figures
sunspots                Monthly Mean Relative Sunspot Numbers
swiss                   Swiss Demographic Data
trees                   Girth, Height and Volume for Black Cherry
                        Trees
uspop                   Populations Recorded by the US Census
volcano                 Topographic Information on Auckland's Maunga
                        Whau Volcano
warpbreaks              Breaks in Yarn during Weaving
women                   Heights and Weights of Women

Il y en a aussi beaucoup dans le paquetage MASS (qui correspondent au livre "Modern Applied Statistics with S").

data(package='MASS')

R précise même :

Use `data(package = .packages(all.available = TRUE))'
to list the data sets in all *available* packages.

Tous les exemples

C'est un peu violent, mais on peut prendre tous les exemples et leur faire subir un certain trairement. J'ai parfois utilisé de telles manipulations lors de la rédaction de ces notes pour trouver un exemple vérifiant certaines propriétés.

#!/bin/sh
cd /usr/lib/R/library/
for lib in *(/)
do
  if [ -d $lib/data ]
  then
    (
      cd $lib/data
      echo 'library('$lib')'
      ls 
    ) | grep -vi 00Index | perl -p -e 's#(.*)\..*?$#data($1); doit($1)#'
  fi
done > /tmp/ALL.R

Je peux avoir une liste de toutes les données comme suit :

doit <- function (...) {}
source("/tmp/ALL.R")
sink("str_data")
ls.str()
q()

Je peux chercher des données avec des valeurs extrèmes (outliers) ainsi :

doit <- function (d) {
  name <- as.character(substitute(d))
  cat(paste("Processing", name, "\n"))
  if (!exists(name)) {
    cat(paste("  Skipping (does not exist!)", name, "\n"))
  } else if (is.vector(d)) {
    doit2(d, name)
  } else if (is.data.frame(d)) {
    for (x in names(d)) {
      cat(paste("Processing", name, "/", x, "\n"))
      doit2(d[[x]], paste(name,"/",x))
    }
  } else
    cat(paste("  Skipping (unknown reason)", substitute(d), "\n"))
}
doit2 <- function (x,n) {
  if( is.numeric(x) ) 
    really.do.it(x,n)
  else
    cat("    Skipping (non numeric)\n")
}
really.do.it <- function (x,name) {
  x <- x[!is.na(x)]
  m <- median(x)
  i <- IQR(x)
  n1 <- sum(x>m+1.5*i)
  n2 <- sum(x<m-1.5*i)
  n <- length(x)
  p1 <- round(100*n1/n, digits=0)
  p2 <- round(100*n2/n, digits=0)
  if( n1+n2>0 ) {
    boxplot(x, main=name)
    cat(paste("      OK ", n1+n2, "/", n, " (",p1,"%, ",p2,"%)\n", sep=''))
  }
}

source("ALL.R", echo=F)

On peut aussi procéder ainsi pour tester du code.

Vocabulaire

On parle de données univariées quand il n'y a qu'une seule variable, bivariées quand il y en a deux, multivariées quand il y en a plus.

On parle de données quantitatives ou numériques, quand cette variable contient des nombres, avec lesquels faire de l'arithmétique a un sens : par exemple des températures, mais pas des numéros de département.

On parle de données qualitatives en cas contraire : par exemple une variable dont les valeurs sont "oui" ou "non", dont les valeurs sont des couleurs, ou dont les valeurs sont des numéros de département.

On rencontre parfois des données qualitatives ordonnées. Par exemple, une variable dont les valeurs seraient "jamais", "rarement", "de temps à autres", "souvent", "tout le temps". Ces données peuvent provenir de la répartition en classes de données quantitatives.

Données univariées quantitatives

Quelques nombres

On peut résumer ces données à l'aide de quelques nombres : la moyenne, le minimum, le maximum, la médianne (on met les nombres dans l'ordre et on prend celui du milieu) les quartiles (idem, mais on prend les nombres à un quard du début ou de la fin).

> summary(faithful$eruptions)
 Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
1.600   2.163   4.000   3.488   4.454   5.100
> mean(faithful$eruptions)
[1] 3.487783

Il faut toujours regarder les données avec un oeil critique : il faut en particulier se demander si les valeurs extrémales sont normales ou si elles ne proviendraient pas d'une quelconque erreur.

On peut aussi regarder diverses mesures de la dispersion, comme l'écart moyen à la médiane (MAD), l'écart-type ("standard deviation", en anglais) ou l'intervalle inter-quartiles (IQR).

> mad(faithful$eruptions)
[1] 0.9510879
> sd(faithful$eruptions)
[1] 1.141371
> IQR(faithful$eruptions)
[1] 2.2915

Je profite de l'occasion pour signaler les liens entre moyenne, variance, médiane et écart moyen à la moyenne. Quand on a un ensemble de nombres x1, x2, ... xn, on peut essayer de trouver le réel m qui minimise la somme

abs(m-x1) + abs(m-x2) + ... + abs(m-xn).

C'est la médiane. On peut aussi essayer de trouver le réel m qui minimise la somme

(m-x1)^2 + (m-x2)^2 + ... + (m-xn)^2.

C'est la moyenne. (Cette propriété de la moyenne s'appelle la propriété des moindres carrés.)

D'où la définition suivante : la variance d'une série statistique x1, x2, ..., xn est la moveyye des carrés des écarts à la moyenne :

          (m-x1)^2 + (m-x2)^2 + ... + (m-xn)^2
Var(x) = --------------------------------------.
                          n

Au début, j'avais du mal à comprendre la pertinence de la variance comme mesure de la dispersion ; la moyenne des valeurs absolues des différences avec la moyenne me semblait plus naturelle -- en effet, pourquoi introduire ces carrés ? La définition précédente de la moyenne peut donc servir de motivation de la notion de variance. Il y a d'autres motivations. L'une est que c'est facile à calculer, à la main, de manière incrémentale. Une autre est que ça intervient dans plein de résultats théoriques (inégalité de Bienaimé-Tchebichev, etc.).

Les notions de moyenne et de variance perdent de leur pertinence pour des distributions non symétriques ou pour des distributions avec beaucoup de valeurs extrèmes.

La médiane, l'intervalle interquartile ou l'écart moyen à la médiane sont des mesures robustes du centre ou de la dispersion : ils donnent de bons résultats, même sur un échantillon non normal, contrairement à la moyenne ou à l'écart-type. Par contre, si l'échantillon est normal, ils sont moins efficaces : leur variance est plus grande. Nous reviendrons sur ces différences quand nous parlerons d'estimateurs.

Moments

A FAIRE : est-ce le bon endroit (on est juste après la variance,
certes, mais on est aussi au milieu de notions simples...)

Nous venons de voir que (pour une série statistique centrée, i.e., de moyenne zéro) la variance est la moyenne des carrés. On peut regarder ce qui se passe avec d'autres exposants : le moment d'ordre k d'une série statistique est la moyenne de ses puissances k. Si on le note M_k, on a

moyenne = M_1
variance = M_2 - M_1^2

Le moment d'ordre 3 s'appelle ********A FAIRE******* (en anglais : skewness). Pour une distribution symétrique (centrée sur zéro), il est nul. Pour vérifier si une distribution est symétrique, et pour quantifier l'écart à la symétrie, il suffit de calculer le troisième moment de la série normalisée.

A FAIRE : exemple.

Le moment d'ordre 4 (kurtosis) permet de voir si la distribution est éloignée d'une distribution normale, et de quantifier cet écart.

On le rencontre par exemple dans l'étude des données financières : on suppose souvent que les données qu'on manipule suivent une loi normale, mais dans ce domaine, ce n'est pas le cas. C'est d'autant plus grave que la différence avec la distribution normale se révèle par des valeurs atypiques anormalement nombreuses. Pour le voir, on calcule la densité de la distribution des retours sur investissement et on la superpose à la distribution d'une loi normale. Pour mieux voir, on a pris une échelle logarithmique. On remarque deux choses : d'une part, la distribution est "plus pointue" que la distribution normale, d'autre part, il y a plus de valeurs atypiques.

op <- par(mfrow=c(2,2))
data(EuStockMarkets)
x <- EuStockMarkets
# We aren't interested in the spot prices, but in the returns
# return[i] = ( price[i] - price[i-1] ) / price[i-1]
y <- apply(x, 2, function (x) { diff(x)/x[-length(x)] })
# We normalize the data
z <- apply(y, 2, function (x) { (x-mean(x))/sd(x) })
for (i in 1:4) {
  d <- density(z[,i])
  plot(d$x,log(d$y),ylim=c(-5,1),xlim=c(-5,5))
  curve(log(dnorm(x)),col='red',add=T)
}
par(op)

*

On vérifie cela par le calcul :

> apply(z^3,2,mean)
       DAX        SMI        CAC       FTSE
-0.4344056 -0.5325112 -0.1059855  0.1651614

> apply(z^4,2,mean)
     DAX      SMI      CAC     FTSE
8.579151 8.194598 5.265506 5.751968

Alors que pour une distribution normale, on obtiendrait 0 et 3.

> mean(rnorm(100000)^3)
[1] -0.003451044
> mean(rnorm(100000)^4)
[1] 3.016637

On peut aussi faire plusieurs simulations (comme on vient de le faire) et regarder la distribution des valeurs obtenues : celles qui viennent de nos données sont-elles si grandes que ça ?

Pour le troisième moment, deux des valeurs sont extrèmes, mais les deux autres sont assez normales.

n <- dim(z)[1]
N <- 2000
m <- matrix(rnorm(n*N), nc=N, nr=n)
a <- apply(m^3,2,mean)
b <- apply(z^3,2,mean)
hist(a, col='light blue', xlim=range(c(a,b)),
     main="Troisième moment")
points(b, rep(.2*par("usr")[3] + .8*par("usr")[4], length(b)), type='h', col='red',lwd=3)
points(b, rep(.2*par("usr")[3] + .8*par("usr")[4], length(b)), col='red', lwd=3)

*

Par contre, pour la kurtosis, nos valeurs sont vraiment anormalement élevées.

n <- dim(z)[1]
N <- 2000
m <- matrix(rnorm(n*N), nc=N, nr=n)
a <- apply(m^4,2,mean)
b <- apply(z^4,2,mean)
hist(a, col='light blue', xlim=range(c(a,b)),
     main="Distribution attendue de la kurtoris et valeurs observées")
points(b, rep(.2*par("usr")[3] + .8*par("usr")[4], length(b)), type='h', col='red',lwd=3)
points(b, rep(.2*par("usr")[3] + .8*par("usr")[4], length(b)), col='red', lwd=3)

*

Nous reverrons un peu plus loin comment quantifier cet écart à la normalité -- ce que nous venons de faire s'appelle un "calcul de p-valeur par bootstrap paramétrique".

Un tas de nombresx

On peut voir les données qu'on étudie comme une masse informe de nombres, dans laquelle on ne voit rien (c'est pourquoi j'ai précédemment utilisé la commande str, pour n'avoir que les début des données : les inclure en intégralité n'apporterait rien).

Une première manière d'y voir quelque chose consiste à les ordonner : mais on a toujours plus d'une centaine de nombres, on n'y voit rien.

> str( sort(faithful$eruptions) )
 num [1:272] 1.60 1.67 1.70 1.73 1.75 ...

Mais on remarque que, dans ces nombres, les deux premiers chiffres sont presque toujours les mêmes. De plus, après ces deux premiers chiffres, il n'en reste qu'un. On peut donc les ranger dans différentes classes, selon les deux premiers chiffres, et indiquer à côté le chiffre restant. C'est ce que les anglo-saxons appellent un "stem-and-leaf plot" (graphique avec une tige et des feuilles). C'est juste une autre manière de représenter le tas de nombres initial.

> stem(faithful$eruptions)

The decimal point is 1 digit(s) to the left of the |

16 | 070355555588
18 | 000022233333335577777777888822335777888
20 | 00002223378800035778
22 | 0002335578023578
24 | 00228
26 | 23
28 | 080
30 | 7
32 | 2337
34 | 250077
36 | 0000823577
38 | 2333335582225577
40 | 0000003357788888002233555577778
42 | 03335555778800233333555577778
44 | 02222335557780000000023333357778888
46 | 0000233357700000023578
48 | 00000022335800333
50 | 0370

Et on pourrait même faire ça à la main. Voici un peu plus d'explications sur ce type de représentation :

http://www.shodor.org/interactivate/discussions/steml.html
http://davidmlane.com/hyperstat/A28117.html
http://www.google.fr/search?q=stem-and-leaf&ie=UTF-8&oe=UTF-8&hl=fr&btnG=Recherche+Google&meta=

Diagramme de dispersion (stripchart)

Un premier graphique consiste à mettre les points sur un axes, tout simplement.

data(faithful)
stripchart(faithful$eruptions)

*

Néanmoins, s'il y a beaucoup de données, ou s'il y a plusieures observations qui ont la même valeur, le résultat est peu lisible. On peut alors ajouter un peu de bruit, de manière que les points ne se superposent pas.

# Bruit uniquement horizontal
stripchart(faithful$eruptions, jitter=TRUE)

*

stripchart(faithful$eruptions, method='jitter')

*

C'est un bon exercice de familiarisation des commandes rnorm et autres que de tenter de faire ça soi-même.

my.jittered.stripchart <- function (x) {
  x.name <- deparse(substitute(x))
  n <- length(x)
  plot( runif(n) ~ x, xlab=x.name, ylab='random' )
}
my.jittered.stripchart(faithful$eruptions)

*

my.jittered.stripchart <- function (x) {
  x.name <- deparse(substitute(x))
  n <- length(x)
  x <- x + diff(range(x))*.05* (-.5+runif(n))
  plot( runif(n) ~ x, xlab=paste("jittered", x.name), ylab='random' )
}
my.jittered.stripchart(faithful$eruptions)

*

On peut aussi représenter les données elles-mêmes, après les avoir triées,

plot(sort(faithful$eruptions))

*

(les deux paliers horizontaux correspondent aux deux pics de l'histogramme).

En fait, c'est simplement un diagramme de dispersion auquel on a ajouté une dimension.

plot(sort(faithful$eruptions))
rug(faithful$eruptions, side=2)

*

Cela permet aussi de voir si les données sont discrètes (si on fait un diagramme de dispersion sans ajout de bruit (jitter), on peut ne pas s'en appercevoir).

x <- round( rnorm(100), digits=1 )
plot(sort(x))
rug(jitter(x), side=2)

*

Effectifs cumulés (courbe de répartition)

On peut aussi considérer le graphe des effectifs cumulés (c'est juste le symétrique du précédent).

effectifs.cumules <- function (x) {
  x.name <- deparse(substitute(x))
  n <- length(x)
  plot( 1:n ~ sort(x), xlab=x.name, ylab='effectifs cumulés')
}    
effectifs.cumules(faithful$eruptions)

*

Dans certains cas, les individus portent des noms : on peut les faire figurer sur le graphique (c'est le même type de graphique que précédemment, mais tourné de 90 degrés).

data(islands)
dotchart(islands)

*

dotchart(sort(log(islands)))

*

D'un point de vue théorique, la courbe des effectifs (ou plutôt des fréquences cumulées) est très importante, mais son interprétation ne saute pas vraiment aux yeux. Dans les exemples suivants, on montre la densité de différentes distributions et les courbes de répartition correspondantes.

op <- par(mfcol=c(2,4))
do.it <- function (x) {
  hist(x, probability=T, col='light blue')
  lines(density(x), col='red', lwd=3)
  x <- sort(x)
  q <- ppoints(length(x))
  plot(q~x, type='l')
  abline(h=c(.25,.5,.75), lty=3, lwd=3, col='blue')
}
n <- 200
do.it(rnorm(n))
do.it(rlnorm(n))
do.it(-rlnorm(n))
do.it(rnorm(n, c(-5,5)))
par(op)

*

Les boites à moustache sont une version simplifiée de la courbe de répartition : on se contente de mettre les quartiles, i.e., les intersections avec les droites pointillées bleues horizontales ci-dessus.

Boite à moustache

On peut représenter graphiquement les 5 quartiles (minimum, premier quartile, médiane, troisième quratile et maximum) : on obtient une boite à moustaches.

boxplot(faithful$eruptions, range=0)

*

On comprend mieux cette appellation si on la représente horizontalement.

boxplot(faithful$eruptions, range=0, horizontal=TRUE)

*

Sur cet exemple, on voit clairement que les données ne sont pas symétriques : on sait donc que ce serait une mauvaise idée de leur appliquer des procédures statistiques qui supposent que les données sont symétriques, ou même carrément normales.

C'est l'une des utilités de ce type de graphique : regarder si les données sont symétriques.

Cette représentation des quartiles est plus simple et plus directement compréhensible que la suivante, en termes d'aires :

op <- par(mfrow=c(1,2))
do.it <- function (x) {
  d <- density(x)
  plot(d, type='l')
  q <- quantile(x)
  do.it <- function (i, col) {
    x <- d$x[i]
    y <- d$y[i]
    polygon( c(x,rev(x)), c(rep(0,length(x)),rev(y)), border=NA, col=col )
  }
  do.it(d$x <= q[2], 'red')
  do.it(q[2] <= d$x & d$x <= q[3], 'green')
  do.it(q[3] <= d$x & d$x <= q[4], 'blue')
  do.it(d$x >= q[4], 'yellow')
  lines(d, lwd=3)
}
do.it( rnorm(2000) )
do.it( rlnorm(200) )
par(op)

*

(Dans cet exemple, les quatre aires sont les mêmes : on affirme souvent que l'oeil humain n'est pas capable de comparer des aires, en voici une preuve éclatante.)

Si on ne précise pas l'option << range=0 >>, le graphe va mettre en évidence les points atypiques, i.e., très éloignés ("outliers", en anglais), qui sont au delà de 1.5 fois l'intervalle interquartile. Sur cet exemple, il n'y en a pas.

boxplot(faithful$eruptions, horizontal=TRUE)

*

Dans certains cas, c'est tout à fait normal.

# Il y en a, c'est génant, mais pas pathologique
boxplot(rnorm(500), horizontal=TRUE)

*

S'il n'y en a que quelques uns, très isolés, il peut s'agit d'erreurs.

x <- c(rnorm(30),20)
x <- sample(x, length(x))
boxplot( x, horizontal=T )

*

library(boot)
data(aml)
boxplot( aml$time, horizontal=T )

*

Ils peuvent aussi signaler que la distribution n'est pas du tout normale.

data(attenu)
boxplot(attenu$dist, horizontal=T)

*

On tente alors de se ramener à des données plus normales en appliquant une fonction bien choisie (nous reviendrons sur cela plus loin).

data(attenu)
boxplot(log(attenu$dist), horizontal=T)

*

La présence de données extrèmes est assez génante, car beaucoup de traitements statistiques (le calcul de la moyenne, de l'écart-type, la régression) y sont très sensibles.

> x <- aml$time
> summary(x)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
   5.00   12.50   23.00   29.48   33.50  161.00
> y <- x[x<160]
> summary(y)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
   5.00   12.25   23.00   23.50   32.50   48.00

C'est l'autre utilité des boites à moustaches : repérer les valeurs extrèmes, qui peuvent être normales (mais dont il faut être conscient, car elles risquent de fausser les calculs ultérieurs) ; qui peuvent résulter d'erreurs, qu'il convient de corriger ; ou qui signale que la distribution n'est pas normale et contient beaucoup de valeurs extrèmes.

En fait, plus l'échantillon est gros, plus on a de valeurs extrèmes.

y <- c(rnorm(10+100+1000+10000+100000))
x <- c(rep(1,10), rep(2,100), rep(3,1000), rep(4,10000), rep(5,100000))
x <- factor(x)
plot(y~x, horizontal=T, las=1)

*

On pourrait dessiner des boites dont les moustaches s'étendent plus loin pour des échantillons plus gros, mais attention : si la présence de valeurs extrèmes dans les gros échantillons est normale, il peuvent toujours avoir un effet de levier non négligeable.

Exercice : dessiner des boites à moustaches dont la longueur des moustaches s'étend plus loin que 1.5 fois l'intervalle interquartiles pour des échantillons plus gros.

On peut décorer cette boite à moustaches d'un intervalle de confiance pour la médiane.

boxplot(faithful$eruptions, notch=TRUE)

*

data(breslow)
boxplot(breslow$n, notch=TRUE)

*

On peut aussi ajouter un diagramme de dispersion.

boxplot(faithful$eruptions, horizontal=T)
rug(faithful$eruption, ticksize=.2)

*

Histogramme et densité

On peut aussi représenter ces données par un histogramme : on répartit les données dans différentes classes (l'ordinateur peut s'en charger) et, pour chaque classe, on trace une barre verticale d'autant plus haute que la classe comporte d'éléments.

hist(faithful$eruptions)

*

Ce qui est assez génant, c'est que des choix de classes différents peuvent donner des histogrammes complètement différents.

Il y a d'une part l'effet de la largeur des classes.

hist(faithful$eruptions, breaks=20, col="light blue")

*

Mais la position des classes peut complètement changer l'histogramme et le faire apparaitre tantôt symétrique, tantôt non, quand bien-même la largeur des classes ne changerait pas. Ainsi, dans les deux histogrammes suivants, le deuxième pic n'a pas l'air symétrique, mais dissymétrie n'est pas dans le même sens...

op <- par(mfrow=c(2,1))
hist(faithful$eruptions, breaks=seq(1,6,.5), col='light blue')
hist(faithful$eruptions, breaks=.25+seq(1,6,.5), col='light blue')
par(op)

*

On peut remplacer l'histogramme par le graphe d'une fonction. Si on voit les données comme une somme de masses de Dirac, il suffit de faire la convolution avec un "noyau" bien choisi, par exemple, la densité de la loi gaussienne.

hist(faithful$eruptions, 
     probability=TRUE, breaks=20, col="light blue")
points(density(faithful$eruptions, bw=.1), type='l', col='red', lwd=3)

*

Les courbes de densité présentent aussi le premier problème des histogrammes : un choix de noyau différent donne une courbe différente -- mais le second problème disparait.

hist(faithful$eruptions, 
     probability=TRUE, breaks=20, col="light blue")
points(density(faithful$eruptions, bw=1),  type='l', lwd=3, col='black')
points(density(faithful$eruptions, bw=.5), type='l', lwd=3, col='blue')
points(density(faithful$eruptions, bw=.3), type='l', lwd=3, col='green')
points(density(faithful$eruptions, bw=.1), type='l', lwd=3, col='red')

*

On peut décorer un histogramme de bien des manières. On peut par exemple ajouter un diagramme de dispersion en dessous ou superposer à sa densité une densité normale.

hist(faithful$eruptions, 
     probability=TRUE, breaks=20, col="light blue")
rug(faithful$eruptions)
points(density(faithful$eruptions, bw=.1), type='l', lwd=3, col='red')
f <- function(x) {
  dnorm(x, 
        mean=mean(faithful$eruptions), 
        sd=sd(faithful$eruptions),
  ) 
}
curve(f, add=T, col="red", lwd=3, lty=2)

*

Graphe de symétrie (peu usité)

Quand on cherche à voir si des données sont symétriques, on peut mettre les données dans l'ordre et tenter d'apparier la première avec la dernière, plus la seconde avec l'avant-dernière, etc. C'est ce que fait le graphique suivant, qui représente la distance à la médiane du (n-i)-ième point en fonction de celle du i-ième point.

symetry.plot <- function (x,...) {
  x <- x[ !is.na(x) ]
  x <- sort(x)
  x <- abs(x - median(x))
  n <- length(x)
  nn <- ceiling(n/2)
  plot( x[n:(n-nn+1)] ~ x[1:nn] ,
        xlab='Distance below median',
        ylab='Distance above median', 
        ...)    
}
symetry.plot(faithful$eruptions)
abline(0,1)

*

Le problème, c'est qu'il est assez délicat de voir quand on est << loin >> de la droite : plus l'échantillon contient de points, plus le graphe est proche de la droite. On voit ici qu'avec une centaine de points, on peut quand même en être assez loin.

n <- 100
N <- 1e3
symetry.plot( rnorm(n), pch='.', col='red', 
              main=paste("Echantillon de", n, "individus") )
for (i in 1:N) {
  x <- sort(rnorm(n))
  x <- abs(x - median(x))
  l <- length(x)
  ll <- ceiling(n/2)
  points( x[l:(l-ll+1)] ~ x[1:ll], pch='.', col='red' )
}
abline(0,1)

*

Avec dix points, c'est encore pire...

(C'est probablement pour cette raison que ce genre de graphique est peu utilisé...)

n <- 10
N <- 1e3
symetry.plot( rnorm(n), pch=15, col='red', cex=.5,
              main=paste("Echantillon de", n, "individus") )
for (i in 1:N) {
  x <- sort(rnorm(n))
  x <- abs(x - median(x))
  l <- length(x)
  ll <- ceiling(n/2)
  points( x[l:(l-ll+1)] ~ x[1:ll], pch=15, col='red', cex=.5 )
}
abline(0,1)

*

Une manière simple de voir si on est << loin >> ou pas consiste donc à superposer le diagramme de symétrie au graphique précédent (en se souvenant qu'il a été construit avec des échantillons normaux, et qu'avec des échantillons symétriques non normaux, les choses peuvent être différentes).

symetry.plot <- function (x, col2='red',...) {
  x <- x[ !is.na(x) ]
  x <- sort(x)
  x <- abs(x - median(x))
  n <- length(x)
  nn <- ceiling(n/2)
  plot( x[n:(n-nn+1)] ~ x[1:nn] ,
        xlab='Distance below median',
        ylab='Distance above median', 
        ...)
  m <- mean(x)
  s <- sd(x)
  N <- 1000
  for (i in 1:N) {
    y <- sort(rnorm(n, mean=m, sd=s))
    y <- abs(y - median(y))
    n <- length(y)
    nn <- ceiling(n/2)
    points( y[n:(n-nn+1)] ~ y[1:nn], pch='.', col=col2 )
  }
  points( x[n:(n-nn+1)] ~ x[1:nn], ...)
  abline(0,1, ...)
}
symetry.plot(faithful$eruptions)

*

symetry.plot(rnorm(100))

*

symetry.plot(runif(100))

*

Graphique quantile-quantile

Nous venons de présenter une manière (graphique) de voir si une distribution est symétrique. Une autre chose que l'on aime bien, dans les données qu'on est amené à étudier, ce sont les variables normales.

Dans certains cas, il est flagrant que la variable n'est pas normale : c'est le cas de la durée des eruptions du Old Faithful Geyser (il y a deux pics). Dans d'autres cas, c'est moins flagrant. Une première manière de s'en appercevoir consiste à comparer la densité de nos données avec une densité normale.

data(airquality)
x <- airquality[,4]
hist(x, probability=TRUE, breaks=20, col="light blue")
rug(jitter(x, 5))
points(density(x), type='l', lwd=3, col='red')
f <- function(t) {
  dnorm(t, mean=mean(x), sd=sd(x) ) 
}
curve(f, add=T, col="red", lwd=3, lty=2)

*

On peut aussi voir graphiquement si une variable est normale : il suffit de représenter les quantiles d'une distribution normale en fonction des quantiles de l'échantillon.

Il y a déjà une fonction qui fait cela. (La fonction qqline trace une droite passant par les premier et troisième quartiles.) Dans cet exemple, les données sont à peu près normales, mais on voit qu'elles sont discrètes.

x <- airquality[,4]
qqnorm(x)
qqline(x)

*

Voici ce que l'on peut obtenir avec un échantillon normal.

y <- rnorm(100)
qqnorm(y)
qqline(y)

*

Maintenant, avec un échantillon non normal.

y <- rnorm(100)^2
qqnorm(y)
qqline(y)

*

Comme précédemment, on peut juxtaposer plusieurs qqplots normaux à notre graphique, pour voir à quel point les données sont éloignées de la normale.

my.qqnorm <- function (x, N=1000, ...) {
  op <- par()
  x <- x[!is.na(x)]
  n <- length(x)
  m <- mean(x)
  s <- sd(x)
  print("a")
  qqnorm(x, axes=F, ...)
  for (i in 1:N) {
    par(new=T)
    qqnorm(rnorm(n, mean=m, sd=s), col='red', pch='.', 
           axes=F, xlab='', ylab='', main='')
  }
  par(new=T)
  qqnorm(x, ...)
  qqline(x, col='blue', lwd=3)
  par(op)
}
my.qqnorm(rnorm(100))

*

my.qqnorm(runif(100), main='loi uniforme')

*

my.qqnorm(exp(rnorm(100)), main='loi log-normale')

*

my.qqnorm(c(rnorm(50), 5+rnorm(50)), main='deux pics')

*

my.qqnorm(c(rnorm(50), 20+rnorm(50)), main='deux pics éloignés')

*

x <- rnorm(100)
x <- x + x^3
my.qqnorm(x, main='distribution très étalée')

*

Voici d'autres exemples de QQplot.

Deux distributions décalées à gauche.

y <- exp(rnorm(100))
qqnorm(y, main='(1) Loi log-normale'); qqline(y, col='red')

*

y <- rnorm(100)^2
qqnorm(y, ylim=c(-2,2), main="(2) Carré d'une va suivant une loi normale")
qqline(y, col='red')

*

Une distribution décalée à droite.

y <- -exp(rnorm(100))
qqnorm(y, ylim=c(-2,2), main="(3) Opposé d'une v.a. suivant une loi log-normale")
qqline(y, col='red')

*

Une distribution moins dispersée que la distribution normale (ça s'appelle une distribution leptokurtique).

y <- runif(100, min=-1, max=1)
qqnorm(y, ylim=c(-2,2), main='(4) Loi uniforme'); qqline(y, col='red')

*

Une distribution plus dispersée que la distribution normale (ça s'appelle une distribution platykurtique).

y <- rnorm(10000)^3
qqnorm(y, ylim=c(-2,2), main="(5) Cube d'une v.a. suivant une loi normale")
qqline(y, col='red')

*

Une distribution avec plusieurs pics.

y <- c(rnorm(50), 5+rnorm(50))
qqnorm(y, main='(6) Deux pics')
qqline(y, col='red')

*

y <- c(rnorm(50), 20+rnorm(50))
qqnorm(y, main='(7) Deux pics plus éloignés')
qqline(y, col='red')

*

y <- sample(seq(0,1,.1), 100, replace=T)
qqnorm(y, main='(7) Loi discrète')
qqline(y, col='red')

*

On peut lire ces graphiques de la manière suivante.

a. Si la loi est plus concentrée qu'une loi normale à gauche de la moyenne, la partie gauche du graphe se trouve au dessus de la droite (exemples 1, 2 et 4 ci-dessus).

b. Si la loi est moins concentrée qu'une loi normale à gauche de la moyenne, la partie gauche du graphe se trouve en dessous de la droite (exemple 3 ci-dessus).

c. Si la loi est plus concentrée qu'une loi normale à droite de la moyenne, la partie droite du graphe se trouve en dessous de la droite (exemples 3 et 4 ci-dessus).

d. Si la loi est moins concentrée qu'une loi normale à droite de la moyenne, la partie droite du graphe se trouve au dessus de la droite (exemples 1 et 2 ci-dessus).

L'exemple 5 ci-dessus s'interprète donc ainsi : la distribution est symétrique ; à gauche de 0, près de 0, elle est plus concentrée qu'une loi normale ; à gauche de 0, loin de 0, elle est moins concentrée qu'une loi normale ; à droite de 0, c'est pareil.

x <- seq(from=0, to=2, length=100)
y <- exp(x)-1
plot( y~x, type='l', xlim=c(-2,2), ylim=c(-2,2), col='red')
lines( x~y, type='l', col='green')
x <- -x
y <- -y
lines( y~x, type='l', col='blue', )
lines( x~y, type='l', col='cyan')
abline(0,1)
legend( -2, 2,
        c( "loi moins concentrée que la normale à droite",  
           "loi plus concentrée que la normale à droite",
           "loi moins concentrée que la normale à gauche",
           "loi plus concentrée que la normale à gauche"
         ),
        lwd=3,
        col=c("red", "green", "blue", "cyan")
      )
title(main="Lecture d'un qqplot")

*

e. Si la distribution est << plus décentrée vers la gauche qu'avec une loi normale >> (plus précisément, si la médiane est inférieure à la moyenne des premier et troisième quartiles) , alors la courbe passe en dessous de la droite au centre du graphique (exemples 1 et 2 ci-dessus).

f. Si << plus décentrée vers la droite qu'avec une loi normale >> (plus précisément, si la médiane est supérieure à la moyenne des premier et troisième quartiles) , alors la courbe passe au dessus de la droite au centre du graphique (exemple 3 ci-dessus).

g. Si la distribution est symétrique (plus précisément : si la médiane est au milieu des premier et troisème quartiles), alors la courbe coupe la droite au centre du dessin (exemples 4 et 5 ci-dessus).

op <- par()
layout( matrix( c(2,2,1,1), 2, 2, byrow=T ),
        c(1,1), c(1,6),
      )
# Le graphique
n <- 100
y <- rnorm(n)
x <- qnorm(ppoints(n))[order(order(y))]
par(mar=c(5.1,4.1,0,2.1))
plot( y~x, col="blue" )
y1 <- scale( rnorm(n)^2 )
x <- qnorm(ppoints(n))[order(order(y1))]
lines(y1~x, type="p", col="red")
y2 <- scale( -rnorm(n)^2 )
x <- qnorm(ppoints(n))[order(order(y2))]
lines(y2~x, type="p", col="green")
abline(0,1)

# La légende
# Quelqu'un sait-il comment superposer des graphiques ?
# Je parviens uniquement à les juxtaposer...
# (normalement, c'est le paramètre graphique "fig", mais ça ne marche pas...)
# Si : voir un peu plus loin (il faut préciser par(new=T)).
par(bty='n', ann=F)
g <- seq(0,1, length=10)
e <- g^2
f <- sqrt(g)
h <- c( rep(1,length(e)), rep(2,length(f)), rep(3,length(g)) )
par(mar=c(0,4.1,0,0))
boxplot( c(e,f,g) ~ h, horizontal=T, 
         border=c("red", "green", "blue"),
         xaxt='n',
         yaxt='n', 
         )
title(main="Lecture d'un qqplot")
par(op)

*

On peut réaliser un QQplot à la main, en reprenant sa définition donnée plus haut.

y <- rnorm(100)^2
y <- scale(x)
y <- sort(x)
x <- qnorm( seq(0,1,length=length(y)) )
plot(y~x)
abline(0,1)

*

Regardons comment la fonction qqnorm est programmée.

> qqnorm
function (y, ...)
UseMethod("qqnorm")

> help.search("qqnorm")
Help files with alias or title matching `qqnorm',
type `help(FOO, package = PKG)' to inspect entry `FOO(PKG) TITLE':
qqnorm(base)            Quantile-Quantile Plots
qqnorm.gls(nlme)        Normal Plot of Residuals from a gls Object
qqnorm.lme(nlme)        Normal Plot of Residuals or Random Effects
                        from an lme Object

> apropos("qqnorm")
[1] "my.qqnorm"      "qqnorm"         "qqnorm.default"

> qqnorm.default
function (y, ylim, main = "Normal Q-Q Plot", xlab = "Theoretical Quantiles",
    ylab = "Sample Quantiles", plot.it = TRUE, ...)
{
  y <- y[!is.na(y)]
  if (0 == (n <- length(y)))
    stop("y is empty")
  if (missing(ylim))
    ylim <- range(y)
  x <- qnorm(ppoints(n))[order(order(y))]
  if (plot.it)
    plot(x, y, main = main, xlab = xlab, ylab = ylab, ylim = ylim,
          ...)
  invisible(list(x = x, y = y))
}

On peut faire la même chose pour comparer avec d'autres distributions.

qq <- function (y, ylim, quantiles=qnorm,
    main = "Q-Q Plot", xlab = "Theoretical Quantiles",
    ylab = "Sample Quantiles", plot.it = TRUE, ...)
{
  y <- y[!is.na(y)]
  if (0 == (n <- length(y)))
    stop("y is empty")
  if (missing(ylim))
    ylim <- range(y)
  x <- quantiles(ppoints(n))[order(order(y))]
  if (plot.it)
    plot(x, y, main = main, xlab = xlab, ylab = ylab, ylim = ylim,
          ...)
  # From qqline
  y <- quantile(y, c(0.25, 0.75))
  x <- quantiles(c(0.25, 0.75))
  slope <- diff(y)/diff(x)
  int <- y[1] - slope * x[1]
  abline(int, slope, ...)
  invisible(list(x = x, y = y))
}

y <- runif(100)
qq(y, quantiles=qunif)

*

(Les différentes interprétations du qqplot restent valables, mais les points e, f et g ne renseignent plus sur la symétrie de la distribution, mais comparent cette symétrie avec celle de la loi de référence, qui n'est pas nécessairement symétrique.)

Même si ça n'est pas du tout justifié, on utilise parfois un graphique quantile-quantile pour comparer une variable positive avec une demi-gaussienne (nous le verrons un peu plus loin pour examiner les distances de Cook).

Detrended probability plot

A FAIRE : trouver le nom français...

Pour mieux voir les écarts à la normalité dans un graphique quantiles-quantiles, on peut le tourner, de sorte que la droite soit horizontale.

Attention : ce n'est pas simplement une rotation de 45 degrés car on compare généralement avec une distribution normale standard. Cette droite passe par les premier et troisième quartiles.

two.point.line <- function (x1,y1,x2,y2, ...) {
  a1 <- (y2-y1)/(x2-x1)
  a0 <- y1 - a1 * x1
  abline(a0,a1,...)
}
trended.probability.plot <- function (x, q=qnorm) {
  n <- length(x)
  plot( sort(x) ~ q(ppoints(n)), , xlab='theoretical quantiles', ylab='sample quantiles')
  two.point.line(q(.25), quantile(x,.25), q(.75), quantile(x,.75), col='red')
}
detrended.probability.plot <- function (x, q=qnorm) {
  n <- length(x)
  x <- sort(x)
  x1 <- q(.25)
  y1 <- quantile(x,.25)
  x2 <- q(.75)
  y2 <- quantile(x,.75)
  a1 <- (y2-y1)/(x2-x1)
  a0 <- y1 - a1 * x1
  u <- q(ppoints(n))
  x <- x - (a0 + a1 * u)
  plot(x ~ u, xlab='theoretical quantiles', ylab='sample quantiles')    
  abline(h=0, col='red')
}

op <- par(mfrow=c(3,2))
x <- runif(20)
trended.probability.plot(x)
detrended.probability.plot(x)
x <- runif(500)
trended.probability.plot(x)
detrended.probability.plot(x)
trended.probability.plot(x, qunif)
detrended.probability.plot(x,qunif)
par(op)

*

Courbe de concentration de Gini (pas important)

Cette année, j'ai dû suivre un cours d'analyse de données (tests statistiques, régression, analyse en composantes principales, analyse des correspondances, analyse discriminante, analyse hiérarchique -- mais comme j'avais déjà commencé à rédiger ce document, je n'y ai pas appris grand-chose) au cours duquel on nous a présenté (de manière assez confuse) la courbe de concentration de Gini (ou de Lorenz).

C'est cette courbe qui résume des informations de la forme << 20% des malades bénéficient de 90% des dépenses de la sécurité sociale >>. Plaçons-nous par exemple dans la situation suivante

 Pourcentage des malades | Pourcentage des dépenses
----------------------------------------------------
       20%               |          90%
       30%               |          95%
       50%               |          99%

il s'agit de pourcentages cummulés :

les 20% les plus dépensiers sont responsables de 90% des dépenses
les 30% les plus dépensiers sont responsables de 95% des dépenses
les 50% les plus dépensiers sont responsables de 99% des dépenses

On peut représenter graphiquement ces informations.

xy <- matrix(c( 0, 0,
               .2, .9, 
               .3, .95, 
               .5, .99,
                1, 1), byrow=T, nc=2)
plot(xy, type='b', pch=15)
polygon(xy, border=F, col='pink')
lines(xy, type='b', pch=15)
abline(0,1,lty=2)

*

L'aire entre la courbe et la diagonale est d'autant plus grande que la situation est inégalitaire : c'est l'indice de Gini (en fait, pour que l'indice de Gini varie entre 0 (situation égalitaire) et 1 (situation vraiment très inégalitaire), on multiple cette aire par deux).

Voici un autre exemple :

Dans une cellule,
20   gènes sont exprimés 10^5 fois ("exprimé", ça veut dire transcrit en ARNm)
2700 gènes sont exprimés 10^2 fois
280  gènes sont exprimés 10 fois.

Convertissons cela en effectifs cumulés :

20   gènes       100000 ARNm
2720 gènes       100100 ARNm
3000 gènes       100110 ARNm

puis en fréquences :

 Gènes | ARNm
-------------------
  0.7% | 0.9989012
 90.7% | 0.9999001
  100% | 1

Voici la courbe (la situation est beaucoup plus inégalitaire que la précédente !) :

x <- c(0,20,2720,3000)/3000
y <- c(0,100000,100100,100110)/100110
plot(x,y, type='b', pch=15)
polygon(x,y, border=F, col='pink')
lines(x,y, type='b', pch=15)
abline(0,1,lty=2)

*

L'exemple classique pour la courbe de Gini est l'étude de l'inégalité des revenus.

Des fonction pour le dessin de la courbe et le calcul de l'indice de Gini se trouvente dans la bibliothèque ineq. (Les courbes obtenues sont en fait les symétriques des précédentes par rapport à la diagonale : ça ne change rien).

La fonction Gini, dans la bibliothèque ineq, calcule l'indice de concentration de Gini. Il n'est bien défini que si la variable statistique étudiée est positive (dans les exemples précédents, cette variable était << les dépenses de sécurité sociale d'un assuré >>, << le nombre de transcrits d'un gène >>, << le salaire d'un individu >> -- nous ne donnions pas directement cette variable, mais simplement les effectifs (ou effectifs cumulés) de sa répartition en classes).

> n <- 500
> library(ineq)
> Gini(runif(n))
[1] 0.3241409
> Gini(runif(n,0,10))
[1] 0.3459194
> Gini(runif(n,10,11))
[1] 0.01629126
> Gini(rlnorm(n))
[1] 0.5035944
> Gini(rlnorm(n,0,2))
[1] 0.8577991
> Gini(exp(rcauchy(n,1)))
[1] 0.998
> Gini(rpois(n,1))
[1] 0.5130061
> Gini(rpois(n,10))
[1] 0.1702435

library(ineq)
op <- par(mfrow=c(2,3))
plot(Lc(runif(n,0,1)), main="uniforme [0,1]", col='red')
plot(Lc(runif(n,0,10)), main="uniforme [0,10]", col='red')
plot(Lc(runif(n,10,11)), main="uniforme [10,11]", col='red')
plot(Lc(rlnorm(n)), main="log-normale", col='red')
plot(Lc(rlnorm(n,0,4)), main="log-normale, plus large", col='red')
plot(Lc(rcauchy(n,1)), main="Cauchy", col='red')
plot(Lc(exp(rcauchy(n,1))), main="log-Cauchy", col='red')
plot(Lc(rpois(n,1)), main="Poisson de moyenne 1", col='red')
plot(Lc(rpois(n,10)), main="Poisson de moyenne 10", col='red')
par(op)

*

On peut faire le calcul de l'indice de Gini nous-même.

n <- 500
x <- qlnorm((1:(n-1))/n, 1, 2.25)
x <- sort(x)
i <- (1:n)/n
plot(cumsum(x)/sum(x) ~ i, lwd=3, col='red')
abline(0,1)
2*sum(i-cumsum(x)/sum(x))/n

Exercice : dans les exemples précédents, les données étaient présentées de deux manières différentes : des effectifs cumulés et des sommes (premiers exemples) ou des réalisations d'une variables aléatoire. Ecrire le code nécessaire pour passer d'une représentation à l'autre.

Données univariées ordonnées

On ne s'intéresse pas à la valeur numérique, mais au classement. On peut traiter ces données tantôt comme des données quantitatives (d'ailleurs, quand on a une variable quantitative, on peut ranger ces valeurs dans des classes et les remplacer par leur numéro d'ordre), après une éventuelle transformation (de sorte que les données soit distribuées normalement et pas uniformément), tantôt comme des données qualitatives (car, contrairement aux variables quantitatives, elles ne peuvent prendre qu'un nombre fini de valeurs).

Les facteurs ordonnés se construisent exactement comme les facteurs, la commande factor étant simplement remplacée par la commande ordered.

> l <- c('petit', 'moyen', 'grand')
> ordered( sample(l,10,replace=T), levels=l)
 [1] grand moyen moyen grand moyen petit moyen petit moyen moyen
Levels:  petit < moyen < grand

Les graphiques sont les mêmes qu'avec des facteurs, à la seule différence que l'ordre des facteurs n'est pas arbitraire.

# (Graphique trompeur : l'origine n'est pas sur le dessin)
data(esoph)
dotchart(table(esoph$agegp))

*

barplot(table(esoph$agegp))

*

hist(as.numeric(esoph$agegp), 
     breaks=seq(.5,.5+length(levels(esoph$agegp)),step=1),
     col='light blue')

*

boxplot(as.numeric(esoph$agegp), horizontal=T)

*

stripchart(jitter(as.numeric(esoph$agegp),2), method='jitter')

*

plot(table(esoph$agegp), type='b', pch=7)

*

Données univariées qualitatives

On a une liste de données non numériques (en fait, ces données peuvent être codées par des nombres, mais une différence de ces nombres n'a pas de sens : par exemple, des numéros de département ou des numéros de réponse à des questions).

Différentes manières de présenter ces données

Il y a deux manières de présenter ces données : soit un vecteur de chaines de caractères (ou plus précisément de facteurs : quand on l'imprime, on dirait que ce sont des chaines de caractères, mais comme on s'attend à n'avoir qu'un petit nombre de valeurs, souvent répétées, c'est codé de manière un peu plus efficace), soit une table d'effectifs.

> p <- factor(c("oui", "non"))
> x <- sample(p, 100, replace=T)
> str(x)
 Factor w/ 2 levels "non","oui": 1 2 2 2 2 1 2 2 2 2 ...
> table(x)
x
non oui
 48  52

La deuxième représentation est beaucoup plus compacte. Si on n'a que des variables quantitatives, c'est probablement celle qu'on choisira.

> data(HairEyeColor)
> HairEyeColor
, , Sex = Male

       Eye
Hair    Brown Blue Hazel Green
  Black    32   11    10     3
  Brown    38   50    25    15
  Red      10   10     7     7
  Blond     3   30     5     8
 
, , Sex = Female
 
       Eye
Hair    Brown Blue Hazel Green
  Black    36    9     5     2
  Brown    81   34    29    14
  Red      16    7     7     7
  Blond     4   64     5     8

Par contre, si on a un mélange de variables qualitatives et quantitatives, on préférera la première représentation.

> data(chickwts)
> str(chickwts)
`data.frame':   71 obs. of  2 variables:
 $ weight: num  179 160 136 227 217 168 108 124 143 140 ...
 $ feed  : Factor w/ 6 levels "casein","horseb..",..: 2 2 2 2 2 2 2 2 2 2 ...

Mais n'anticipons pas trop : nous allons commencer par nous limiter au cas de données univariées.

Nous avons déjà vu que la commande table permettait d'obtenir la représentation compacte.

> table(chickwts$feed)
   casein horsebean   linseed  meatmeal   soybean sunflower
       12        10        12        11        14        12

Dans l'autre sens, on peut par exemple utiliser la commande rep.

> x <- table(chickwts$feed)
> rep(names(x),x)
 [1] "casein"    "casein"    "casein"    "casein"    "casein"    "casein"
 [7] "casein"    "casein"    "casein"    "casein"    "casein"    "casein"
[13] "horsebean" "horsebean" "horsebean" "horsebean" "horsebean" "horsebean"
[19] "horsebean" "horsebean" "horsebean" "horsebean" "linseed"   "linseed"
[25] "linseed"   "linseed"   "linseed"   "linseed"   "linseed"   "linseed"
[31] "linseed"   "linseed"   "linseed"   "linseed"   "meatmeal"  "meatmeal"
[37] "meatmeal"  "meatmeal"  "meatmeal"  "meatmeal"  "meatmeal"  "meatmeal"
[43] "meatmeal"  "meatmeal"  "meatmeal"  "soybean"   "soybean"   "soybean"
[49] "soybean"   "soybean"   "soybean"   "soybean"   "soybean"   "soybean"
[55] "soybean"   "soybean"   "soybean"   "soybean"   "soybean"   "sunflower"
[61] "sunflower" "sunflower" "sunflower" "sunflower" "sunflower" "sunflower"
[67] "sunflower" "sunflower" "sunflower" "sunflower" "sunflower"

Graphique en colonnes

data(HairEyeColor)
x <- apply(HairEyeColor, 2, sum)
barplot(x)
title(main="Graphique en colonnes")

*

barplot(x, col=1, density=c(3,7,11,20), angle=c(45,-45,45,-45))
title(main="Graphique en colonnes")

*

Graphique en barre

x <- apply(HairEyeColor, 2, sum)
barplot(as.matrix(x), legend.text=TRUE)
title("Graphique en barre")

*

barplot(as.matrix(x), horiz=TRUE, col=rainbow(length(x)), legend.text=TRUE)
title(main="Graphique en barre")

*

Diagramme de Pareto

C'est simplement un graphique en colonnes, dans lequel les données sont mises dans l'ordre (l'ordre des effectifs décroissants). S'il n'y a que très peu de valeurs possibles, ça n'apporte pas grand-chose (d'autant plus que dans les exemples précédents, les données étaient déjà dans l'ordre : nous faisions des disgrammes de Paréto sans le savoir...).

data(attenu)
op <- par(las=2) # Écrire les labels perpendiculairement aux axes
barplot(table(attenu$event))
title(main="Graphique en colonnes")
par(op)

*

op <- par(las=2)
barplot(rev(sort(table(attenu$event))))
title(main="Graphique de Paréto")
par(op)

*

Souvent, on rajoute les effectifs cumulés.

# Avec la commande barplot, je n'y arrive pas.
pareto <- function (x) {
  op <- par(mar = c(5, 4, 4, 5) + 0.1)
  par(las=2)
  if( ! inherits(x, "table") ) {
    x <- table(x)
  }
  x <- rev(sort(x))
  plot( x, type='h', axes=F, lwd=16 )
  axis(2)
  points( x, type='h', lwd=12, col=heat.colors(length(x)) )
  y <- cumsum(x)/sum(x)
  par(new=T)
  plot(y, type="b", lwd=3, pch=7, axes=F, xlab='', ylab='', main='')
  points(y, type='h')
  axis(4)
  print(names(x))
  axis(1, at=1:length(x), labels=names(x))
  par(op)
}
pareto(attenu$event)    
title(main="Graphique de Paréto avec effectifs cumulés")

*

Camembert

x <- apply(HairEyeColor, 2, sum)
pie(x)
title(main="Camembert")

*

On constate que les êtres humains perçoivent mal les différences entre des angles ou des surfaces. Or c'est justement ce sur quoi reposent les camemberts : ce n'est donc pas toujours un bon moyen de présenter l'information (c'est beau, mais peu informatif).

dotchart

Quand on a beaucoup de valeurs, les "dotplots" peuvent remplacer les graphiques en colonnes.

x <- apply(HairEyeColor, 2, sum)
dotchart(x)

*

Ils restent très lisibles.

data(Cars93)
dotchart(table(Cars93$Manufacturer))

*

library(nlme)
data(Milk)
dotchart(table(Milk$Cow))

*

Données bivariées quantitatives

On a cette fois-ci deux séries de nombres, en parallèle : il s'agit le plus souvent d'un tableau à deux colonnes, chaque ligne du tableau (chaque couple de valeurs) correspondant à un individu.

Nuage de points

data(cars)
plot(cars$dist ~ cars$speed, 
     xlab = "Speed (mph)", 
     ylab = "Stopping distance (ft)", 
     las = 1)
title(main = "Nuage de points")

*

On peut ajouter un diagramme de dispersion unidimensionnel dans les marges.

plot(cars$dist ~ cars$speed, 
     xlab = "Speed (mph)", 
     ylab = "Stopping distance (ft)", 
     las = 1)
title(main = "cars data")
rug(side=1, jitter(cars$speed, 5))
rug(side=2, jitter(cars$dist, 20))

*

On peut aussi ajouter une boite à moustache.

op <- par()
layout( matrix( c(2,1,0,3), 2, 2, byrow=T ),
        c(1,6), c(4,1),
      )

par(mar=c(1,1,5,2))
plot(cars$dist ~ cars$speed, 
     xlab='', ylab='',
     las = 1)
rug(side=1, jitter(cars$speed, 5) )
rug(side=2, jitter(cars$dist, 20) )
title(main = "cars data")

par(mar=c(1,2,5,1))
boxplot(cars$dist, axes=F)
title(ylab='Stopping distance (ft)', line=0)

par(mar=c(5,1,1,2))
boxplot(cars$speed, horizontal=T, axes=F)
title(xlab='Speed (mph)', line=1)

par(op)

*

On peut tenter d'approcher ces données par une droite.

plot(dist ~ speed, data=cars)
abline(lm(dist ~ speed, data=cars), col='red')

*

La fonction lowess permet d'avoir une courbe non nécessairement rectiligne (nous verrons d'autres exemples du même type quand nous parlerons de régression (linéaire, curvilinéaire, robuste)).

plot(cars, xlab = "Speed (mph)", ylab = "Stopping distance (ft)",
     las = 1)
lines(lowess(cars$speed, cars$dist, f = 2/3, iter = 3), col = "red")
title(main = "cars data")

*

Avec une variable périodique

Si l'une des variable est périodique, on peut la représenter sur un cercle.

On peut par exemple représenter l'affluence sur un site Web en fonction de l'heure du jour ou le volume des précipitation en fonction de la direction du vent. (La fonction stars permet aussi de faire ce genre de chose.)

x <- c(15, 9, 75, 90, 1, 1, 11, 5, 9, 8, 33, 11, 11, 
       20, 14, 13, 10, 28, 33, 21, 24, 25, 11, 33)
# j'ai essayé pendant près d'une demi-heure d'utiliser la commande stars
# pour faire ça, mais sans succès.
clock.plot <- function (x, col=rainbow(n), ...) {
  if( min(x)<0 ) x <- x - min(x)
  if( max(x)>1 ) x <- x/max(x)
  n <- length(x)
  if(is.null(names(x))) names(x) <- 0:(n-1)
  m <- 1.05
  plot(0, type='n', xlim=c(-m,m), ylim=c(-m,m), 
       axes=F, xlab='', ylab='', ...)
  a <- pi/2 - 2*pi/200*0:200
  polygon( cos(a), sin(a) )
  v <- .02
  a <- pi/2 - 2*pi/n*0:n
  segments( (1+v)*cos(a), (1+v)*sin(a), (1-v)*cos(a), (1-v)*sin(a) )
  segments( cos(a), sin(a), 0, 0, col='light grey', lty=3) 
  ca <- -2*pi/n*(0:50)/50
  for (i in 1:n) {
    a <- pi/2 - 2*pi/n*(i-1)
    b <- pi/2 - 2*pi/n*i
    polygon( c(0, x[i]*cos(a+ca), 0),
             c(0, x[i]*sin(a+ca), 0),
             col=col[i] )
    v <- .1
    text((1+v)*cos(a), (1+v)*sin(a), names(x)[i])
  }
}
clock.plot(x, main="Affluence d'un site Web en fonction de l'heure")

*

Treillis

On peut aussi découper l'ensemble des valeurs de Y en tranches et, pour chacune de ces tranches, faire une boite à moustaches, un histogramme ou une courbe de densité des valeurs de X correspondantes. Ces fonctions sont dans la bibliothèque lattice.

library(lattice)
y <- cars$dist
x <- cars$speed
# vitesse <- shingle(x, co.intervals(x, number=6))
vitesse <- equal.count(x)
histogram(~ y | vitesse)

*

bwplot(~ y | vitesse, layout=c(1,6))

*

densityplot(~ y | vitesse, aspect='xy')

*

densityplot(~ y | vitesse, layout=c(1,6))

*

Avant de connaître les graphiques en treillis, je découpais l'une des variables en quartiles, et je représentais la médiane (ou les quartiles) de l'autre pour chacun de ces quartiles.

y <- cars$dist
x <- cars$speed
q <- quantile(x)
o1 <- x<q[2]
o2 <- q[2]<x & x<q[3]
o3 <- q[3]<x & x<q[4]
o4 <- q[4]<x 
dotchart(c(median(y[o1]), median(y[o2]), 
           median(y[o3]), median(y[o4])),
         labels=as.character(1:4),
         xlab="speed", ylab="distance")

*

my.dotchart <- function (y,x,...) {
  x <- as.matrix(x)
  z <- NULL
  for (i in 1:dim(x)[2]) {
    q <- quantile(x[,i])
    for (j in 1:4) {
      z <- append(z, median(y[ q[j] <= x[,i] & x[,i] <= q[j+1] ]))
      names(z)[length(z)] <- paste(colnames(x)[i], as.character(j))
    }
  }
  dotchart(z,...)
}
my.dotchart(y,x, xlab="speed", ylab="distance")

*

my.dotchart <- function (y,x,...) {
  x <- as.matrix(x)
  z <- NULL
  for (i in 1:dim(x)[2]) {
    q <- quantile(x[,i])
    for (j in 1:4) {
      ya <- y[ q[j] <= x[,i] & x[,i] <= q[j+1] ]
      z <- rbind(z, quantile(ya))
      rownames(z)[dim(z)[1]] <- paste(colnames(x)[i], as.character(j))
    }
  }
  dotchart(t(z))
}
my.dotchart(y,x, xlab="speed", ylab="distance")

*

Je n'arrive pas à superposer ce genre de diagramme : faisons les choses à la main.

my.dotchart <- function (y,x,...) {
  x <- as.matrix(x)
  z <- NULL
  for (i in 1:dim(x)[2]) {
    q <- quantile(x[,i])
    for (j in 1:4) {
      ya <- y[ q[j] <= x[,i] & x[,i] <= q[j+1] ]
      z <- rbind(z, quantile(ya))
      rownames(z)[dim(z)[1]] <- paste(colnames(x)[i], as.character(j))
    }
  }
  xmax <- max(z)
  xmin <- min(z)
  n <- dim(z)[1]
  plot( z[,3], 1:n, xlim=c(xmin,xmax), ylim=c(1,n), axes=F, frame.plot=T, pch='.', ... )
  axis(1)
  axis(2, at=1:n, las=1)
  abline( h=1:n, lty=3 )
  # médiane
  points( z[,3], 1:n, pch=16, cex=3 )
  # quartiles
  segments( z[,2], 1:n, z[,4], 1:n, lwd=7 )
  # min et max
  segments( z[,1], 1:n, z[,5], 1:n )
}
my.dotchart(y,x, xlab="speed", ylab="distance")

*

On vient en fait de réinventer la boite à moustaches...

Données bivariées qualitative/quantitative

Boites à moustaches

On peut faire une boite à moustaches par valeur de la variable qualitative (ici, on compare l'efficacité de différents insecticides).

data(InsectSprays)
boxplot(count ~ spray, data = InsectSprays,
        xlab = "Type of spray", ylab = "Insect count",
        main = "InsectSprays data", varwidth = TRUE, col = "lightgray")

*

Les violons sont une variante des boites à moustaches, qui représentent aussi les densités. On peut ainsi repérer plus facilement des densités bimodales.

library(Simple)
n <- 1000
k <- 10
x <- factor(sample(1:5, n, replace=T))
m <- rnorm(k,sd=2)
s <- sample( c(rep(1,k-1),2) )
y <- rnorm(n, m[x], s[x])
x[x %in% order(m)[c(1,5)]] <- order(m)[1]
simple.violinplot(y~x, col='pink')

*

On peut reprendre la fonction my.dotchart, définie plus haut, et la modifier pour qu'elle accepte aussi les variables qualitatives.

my.dotchart <- function (y,x,...) {
  x <- data.frame(x)
  z <- NULL
  cn <- NULL
  for (i in 1:dim(x)[2]) {
    if( is.numeric(x[,i]) ) {
      q <- quantile(x[,i])
      for (j in 1:4) {
        ya <- y[ q[j] <= x[,i] & x[,i] <= q[j+1] ]
        z <- rbind(z, quantile(ya))
        cn <- append(cn, paste(colnames(x)[i], as.character(j)))
      }
    } else {
      for (j in levels(x[,i])) {
        ya <- y[ x[,i] == j ]
        z <- rbind(z, quantile(ya))
        cn <- append(cn, paste(colnames(x)[i], as.character(j)))
      }
    }
  }
  xmax <- max(z)
  xmin <- min(z)
  n <- dim(z)[1]
  plot( z[,3], 1:n, xlim=c(xmin,xmax), ylim=c(1,n), axes=F, frame.plot=T, pch='.', ... )
  axis(1)
  axis(2, at=1:n, labels=cn, las=1)
  abline( h=1:n, lty=3 )
  # médiane
  points( z[,3], 1:n, pch=16, cex=3 )
  # quartiles
  segments( z[,2], 1:n, z[,4], 1:n, lwd=7 )
  # min et max
  segments( z[,1], 1:n, z[,5], 1:n )
}
spray <- InsectSprays$spray
y <- InsectSprays$count
my.dotchart(y,spray, xlab="count", ylab="spray")

*

Diagrammes de dispersion parallèles

stripchart(InsectSprays$count ~ InsectSprays$spray, method='jitter')

*

Diagramme de dispersion coloré

On aimerait faire un « stripchart » avec différentes couleurs pour distinguer les classes, un peu comme on sait faire en dimensions supérieures.

data(iris)
plot(iris[1:4], pch=21, bg=c("red", "green", "blue")[as.numeric(iris$Species)])

*

Le problème, c'est que la commande stripchart n'accepte pas cet argument « bg ». On fait donc la même chose à la main. En fait, ça n'était pas une bonne idée : on ne voit rien...

a <- InsectSprays$count
b <- rnorm(length(a))
plot(b~a, pch=21, 
     bg=c("red", "green", "blue", "cyan", "yellow", "black")
          [as.numeric(InsectSprays$spray)])

*

a <- as.vector(t(iris[1]))
b <- rnorm(length(a))
plot(b~a, pch=21, bg=c("red", "green", "blue")[as.numeric(iris$Species)])

*

Regardons sur des données construites à cet effet.

do.it <- function (v) {
  n <- 100
  y <- sample( 1:3, n, replace=T )
  a <- runif(1)
  b <- runif(1)
  c <- runif(1)
  x <- ifelse( y==1, a+v*rnorm(n), 
       ifelse( y==2, b+v*rnorm(n), c+v*rnorm(n) ))
  r <- rnorm(n)
  plot( r ~ x, pch=21, bg=c('red', 'green', 'blue')[y] )
}
do.it(.1)

*

Il faut vraiment que les groupes soient bien séparés pour y voir quelque chose.

do.it(.05)

*

Histogrammes

On peut aussi faire plusieurs histogrammes.

hists <- function (x, y, ...) {
  y <- factor(y)
  n <- length(levels(y))
  op <- par( mfcol=c(n,1) )    
  b <- hist(x, ..., plot=F)$breaks
  for (l in levels(y)){
    hist(x[y==l], breaks=b, probability=T, ylim=c(0,.3), main=l, col='lightblue', ...)
    points(density(x[y==l]), type='l', lwd=3, col='red')
  }
  par(op)
}
hists(InsectSprays$count, InsectSprays$spray)

*

Treillis

On peut aussi faire ça avec des treillis.

library(lattice)
histogram( ~ count | spray, data=InsectSprays)

*

densityplot( ~ count | spray, data = InsectSprays )

*

bwplot( ~ count | spray, data = InsectSprays )

*

bwplot( ~ count | spray, data = InsectSprays, layout=c(1,6) )

*

Rapport de corrélation

On peut calculer un « rapport de corrélation » : si X est la variable qualitative et Y la variable quantitative, on peut écrire la variance de Y comme somme d'une « variance expliquée par » et d'une « variance résiduelle » ; le rapport de corrélation est le quotient (parfois on prend la racine de ce quotient, parfois non...) de la variance expliquée et de la variance de Y.

> aov( count ~ spray, data = InsectSprays )
Call:
   aov(formula = r)

Terms:
                   spray Residuals
Sum of Squares  2668.833  1015.167
Deg. of Freedom        5        66

Residual standard error: 3.921902
Estimated effects may be unbalanced

Ici, la variance expliquée est 2669, la variance résiduelle est 1015. Le rapport de corrélation est donc (quotient de la variance expliquée et de la variance totale),

> s <- summary(aov( count ~ spray, data = InsectSprays ))[[1]][,2]
> s[1]/(s[1]+s[2])
[1] 0.7244

On résume cela en disant que le type d'insecticide explique 70% de la variance du nombre d'insectes.

On peut avoir cette valeur autrement (au milieu de plein d'autres choses, sur lesquelles nous reviendrons quand nous aborderons la régression et l'anova (analyse de la variance)).

> summary( lm( count ~ spray, data = InsectSprays ) )

Call:
lm(formula = count ~ spray, data = InsectSprays)

Residuals:
   Min     1Q Median     3Q    Max
-8.333 -1.958 -0.500  1.667  9.333

Coefficients:
            Estimate Std. Error t value Pr(>|t|)
(Intercept)  14.5000     1.1322  12.807  < 2e-16 ***
sprayB        0.8333     1.6011   0.520    0.604
sprayC      -12.4167     1.6011  -7.755 7.27e-11 ***
sprayD       -9.5833     1.6011  -5.985 9.82e-08 ***
sprayE      -11.0000     1.6011  -6.870 2.75e-09 ***
sprayF        2.1667     1.6011   1.353    0.181
---
Signif. codes:  0 `***' 0.001 `**' 0.01 `*' 0.05 `.' 0.1 ` ' 1
 
Residual standard error: 3.922 on 66 degrees of freedom
Multiple R-Squared: 0.7244,     Adjusted R-squared: 0.7036
F-statistic:  34.7 on 5 and 66 DF,  p-value: < 2.2e-16

Données bivariées qualitatives

Tables

On peut présenter ces données sous forme d'un tableau, les lignes représentant les différentes valeurs de la première variable, les colonnes celles de la seconde, les cases du tableau contiennent les effectifs.

data(HairEyeColor)
a <- as.table( apply(HairEyeColor, c(1,2), sum) )
barplot(a, legend.text = attr(a, "dimnames")$Hair)

*

La table a contient :

       Eye
Hair    Brown Blue Hazel Green
  Black    68   20    15     5
  Brown   119   84    54    29
  Red      26   17    14    14
  Blond     7   94    10    16

Voici d'autres manières de présenter la même chose.

barplot(a, beside=TRUE, legend.text = attr(a, "dimnames")$Hair)

*

barplot(t(a), legend.text = attr(a, "dimnames")$Eye)

*

barplot(t(a), beside=TRUE, legend.text = attr(a, "dimnames")$Eye)

*

Profils-lignes et profils-colonnes

On peut remplacer les lignes par les « profils-lignes », i.e., des fréquences marginales (on peut aussi faire ça avec les colonnes). On peut ensuite représenter chaque ligne par une barre.

> b <- a / apply(a, 1, sum)
> apply(b, 1, sum)
Black Brown   Red Blond
    1     1     1     1
> options(digits=2)
> b
       Eye
Hair    Brown Blue Hazel Green
  Black 0.630 0.19 0.139 0.046
  Brown 0.416 0.29 0.189 0.101
  Red   0.366 0.24 0.197 0.197
  Blond 0.055 0.74 0.079 0.126

> c <- t( t(a) / apply(a, 2, sum) )
> apply(c, 2, sum)
Brown  Blue Hazel Green
    1     1     1     1
> c
       Eye
Hair    Brown  Blue Hazel Green
  Black 0.309 0.093  0.16 0.078
  Brown 0.541 0.391  0.58 0.453
  Red   0.118 0.079  0.15 0.219
  Blond 0.032 0.437  0.11 0.250

b <- a / apply(a, 1, sum)
barplot(t(b))

*

c <- t( t(a) / apply(a, 2, sum) )
barplot(c)

*

C'est déjà ce que fait la commande mosaicplot (en plus, la largeur des barres dépend des effectifs marginaux).

plot(a)

*

plot(t(a))

*

On peut ajouter des couleurs :

plot(a, col=heat.colors(dim(a)[2]))

*

ou plus simplement :

plot(a, color=T)

*

Je n'ai pas réussi à avoir simplement à la fois des couleurs (pour distinguer les valeurs de la première variable) et des hachures (pour la seconde).

On peut aussi demander à R d'indiquer quelles sont les classes dont les effectifs sont significativement importants ou faibles.

plot(a, shade=T)

*

plot(t(a), shade=T)

*

Plusieurs courbes colorées

On peut représenter chaque ligne du tableau par une courbe (l'axe des abscisses est alors utilisé pour l'autre variable qualitative et l'axe vertical représente les effectifs).

data(HairEyeColor)
a <- apply(HairEyeColor, c(1,2) , sum)
qualplot <- function (a) {
  matplot( row(a), a, type='l', axes=F,
           col = 1:dim(a)[2]+1,
           lty = 1:dim(a)[2],
           lwd=3,
           xlab=names(dimnames(a))[1], ylab=names(dimnames(a))[2] )
  axis(1, 1:dim(a)[1], row.names(a))
  axis(2)
  legend(1, max(a), row.names(t(a)),
         lwd=3, cex=1.5,
         col=1:dim(a)[2]+1, lty=1:dim(a)[2])
}
# Pour une utilisation interactive
qualplots <- function (a) {
  op = par(ask=T)
  qualplot(a)
  qualplot(t(a))
  par(op)
}
qualplot(a)

*

qualplot(t(a))

*

On peut préalablement modifier le tableau pour que les effectifs marginaux soient égaux, i.e., remplacer le tableau par les profils-lignes ou par les profils-colonnes.

qualplotfreq <- function (a) {
  a <- t( t(a) / apply(a,2,sum) )
  qualplot(a)
}
qualplotsfreq <- function (a) {
  op = par(ask=T)
  qualplotfreq(a)
  qualplotfreq(t(a))
  par(op)
}
qualplotfreq(a)

*

qualplotfreq(t(a))

*

Fourfoldplot

Si les variables n'ont que deux valeurs, on peut aussi utiliser la commande fourfoldplot.

data(bacteria)
fourfoldplot( table(bacteria$y, bacteria$ap) )

*

La superficie des quarts de cercle est proportiionnelle aux effectifs. Les deux variables sont liées si les quarts de cercle opposés (ceux de même couleur) ont des tailles comparables et différentes de celles des deux autres quarts de cercle. Pour faciliter la comparaison, on trace aussi les intervalles de confiance à 95% : s'ils ne se chevauchent pas, la différence est statistiquement significative. Dans l'exemple précédent, la différence est significative. Dans les exemples suivants, les variables sont liées pour la semaine 6, mais pas pour les autres (ou du moins, pas de manière statistiquement significative).

fourfoldplot( table(bacteria$y, bacteria$ap, bacteria$week) )

*

Effets graphiques

Bubble chart

On peut représenter trois variables quantitatives par des disques dans le plan : les deux premières variables fournissent les coordonnées du centre, la troisième le diamètre (si on veut montrer que les variations de cette variable sont fortes) ou l'aire (si on veut montrer qu'elles sont faibles, ou si on est honnète).

n <- 50
x <- rnorm(n)
y <- rnorm(n)
z <- rnorm(n)
my.renorm <- function (z) {
  z <- abs(z)
  z <- 10*z/max(z)
  z
}
z <- my.renorm(z)
plot(x,y,cex=z)

*

plot(x,y,cex=z,pch=16,col='red')
points(x,y,cex=z)

*

Il y a des variantes : on peut ajouter une variable (le plus souvent qualitative) en faisant varier la couleur des disques ;

u <- sample(c('red','green','blue'),n,replace=T)
plot(x,y,cex=z,pch=16,col=u)
points(x,y,cex=z)

*

on peut ajouter plusieurs variables quantitatives en remplaçant le disque par des cercles concentriques, etc.

z2 <- rnorm(n)
z2 <- my.renorm(z2)
plot(x,y,cex=z)
points(x,y,cex=z2,col='red')

*

# Autre renormalisation (pertinente s'il n'y a pas de zéro)
my.renorm <- function (z) { 
  z <- (z-min(z)) / (max(z)-min(z))
  z <- 1+9*z
  z
}
z <- my.renorm(z)
z2 <- my.renorm(z2)
plot(x,y,cex=z)
points(x,y,cex=z2,col='red')

*

On peut aussi remplacer les cercles concentriques par des diagrammes étoilés.

n <- 50
x <- runif(n)
y <- runif(n)
z1 <- rnorm(n)
z2 <- rnorm(n)
z3 <- rnorm(n)
z4 <- rnorm(n)
z5 <- rnorm(n)
stars( data.frame(z1,z2,z3,z4,z5), location=cbind(x,y), 
       labels=NULL, len=1/sqrt(n)/2 )

*

v <- .2
n <- 50
x <- runif(n)
y <- runif(n)
z1 <- x+y+v*rnorm(n)
z2 <- x*y+v*rnorm(n)
z3 <- x^2 + y^2 + v*rlnorm(n)
stars( data.frame(z1,z2,z3), location=cbind(x,y), 
       labels=NULL, len=1/sqrt(n)/2, axes=T,
       draw.segments=T, col.segments=1:5 )

*

Line chart

Il s'agit de tracer plusieurs variables Y1, Y2, etc. en fonction de X. On pourrait superposer les courbes les unes aux autres, avec la commande matplot.

n <- 10
d <- data.frame(y1=abs(rnorm(n)),
                y2=abs(rnorm(n)),
                y3=abs(rnorm(n)),
                y4=abs(rnorm(n)),
                y5=abs(rnorm(n))
               )
matplot(d, type='l')

*

On pourrait utiliser un diagramme en barres.

barplot(t(as.matrix(d)))

*

Pour faire un "line chart", on procède ainsi. On commence par tracer Y1 en fonction de X, on colorie en dessous de la courbe, on trace Y1+Y2 en fonction de X, on colorie entre les deux courbes, etc.

line.chart <- function (d) {
  m <- d
  m <- t(apply(m,1,cumsum))
  print(m)
  n1 <- dim(m)[1]
  n2 <- dim(m)[2]
  col = rainbow(n)
  plot.new()
  plot.window(xlim=c(1,n1), ylim=c(min(m),max(m)))
  axis(1)
  axis(2)
  for (i in n2:1) {
    polygon(c(1:n1,n1,1), c(m[,i],0,0), col=col[i], border=0)
  }
  for (i in n2:1) {
    lines(m[,i], lwd=2)
  }
}
line.chart(d)

*

Décorations

On peut s'amuser à ajouter différents effets 3D -- mais R n'est pas un logiciel de dessin...

Un camembert avec une ombre.

shaded.pie <- function (...) {
  pie(...)
  op <- par(new=T)
  a <- seq(0,2*pi,length=100)
  for (i in (256:64)/256) {
    r <- .8-.1*(1-i)
    polygon( .1+r*cos(a), -.2+r*sin(a), border=NA, col=rgb(i,i,i))
  }
  par(new=T)
  pie(...)
  par(op)
}
x <- rpois(10,5)
x <- x[x>0]
shaded.pie(x)

*

Des barres en 3D (avec kchart : je laisse au lecteur le soin de faire pareil avec R).

*

On pourrait aussi mettre les barres les unes derrière les autres, on pourrait aussi faire des rubans en 3D.

On pourrait aussi déléguer la tache de dessin à un logiciel comme PoVRay...

povray.hist <- function (x, ...) {
  x <- hist(x, plot=F, ...)$counts
  if( min(x)<0 ) x <- x-min(x)
  x <- x/max(x)*1.5
  n <- length(x)
  w <- 2/n
  e <- .01
  my.cat <- function (s) { cat(s); cat("\n") }
  my.cat('#include "colors.inc"')
  my.cat('#include "stones.inc"')
  my.cat('#include "woods.inc"')
  my.cat('#declare rd=seed(0);')
  my.cat('#declare T1 = texture { T_Stone16 scale .2}')
  my.cat('#declare T2 = texture { T_Wood9 }')
  my.cat('background { color White }')
  my.cat('camera{')
  my.cat('  location <.6,2,-1.5>')
  my.cat('  look_at <0,.6,0>')
  my.cat('}')
  my.cat('light_source{<-1, 4, -2> color .5*White}')
  my.cat('light_source{<+1, 4, -2> color White}')
  my.cat('light_source{<1,.5,-2> color White}')
  my.cat('// Ground ')
  my.cat('#declare e = .1 ;')
  my.cat('box{ <-1-e, -.02, -.25-e>, <1+e, 0, .25+e> texture {T1} }')
  my.cat('// Boxes')
  for (i in 1:n) {
    my.cat(paste( 'box{<', -1+(i-1)*w +e, ',', x[i], ',', -w/2, '>, <',
               -1+i*w, ',', 0, ',', w/2, '> ',
               'texture{T2 rotate rand(rd)*90*x ',
               'translate 2*<rand(rd), rand(rd), rand(rd)> }}' ))
  }
}
povray.hist(faithful$eruptions)

*

Animations

Une animation, c'est simplement une suite d'images statiques : l'exemple suivant représente le graphique quantiles-quantiles de x^a, pour différentes valeurs de a. Dans un coin, on a mis l'histogramme de x^a, une estimation de sa densité et

n <- 500
set.seed(7236)
x <- rnorm(n,10)
N <- 50
a <- seq(.002,10,length=N)
for (i in factor(1:N)) {
  png(filename=sprintf("animation_1_%03d.png", i))
  y <- x^a[i]
  qqnorm(y, main=paste("a =", signif(a[i])))
  qqline(y, col='red')
  par(fig=c(.7,.9,.2,.4),mar=c(0,0,0,0),new=T)
  hist(y, col='light blue', probability=T,axes=F,main='')
  lines(density(y), col='red', lwd=3)
  curve(dnorm(x,mean(y),sd=sd(y)),col='blue',lty=2,lwd=3, add=T)
  dev.off()
}

On convertit cette suite d'images en une animation, par exemple avec ImageMagick.

convert -delay 50 -loop 50 *.png animation_1.gif

*

Le format MNG est au PNG ce que le GIF animé est au GIF :

convert -delay 50 -loop 50 *.png animation_1.mng

*

A FAIRE

Pour avoir du divx ???

Données multivariées quantitatives

Nuages de points

La commande pairs produit un tableau de nuages de points.

data(LifeCycleSavings)
plot(LifeCycleSavings)

*

On peut aussi demander le tableau des corrélations.

> cor(LifeCycleSavings)
         sr pop15 pop75   dpi  ddpi
sr     1.00 -0.46  0.32  0.22  0.30
pop15 -0.46  1.00 -0.91 -0.76 -0.05
pop75  0.32 -0.91  1.00  0.79  0.03
dpi    0.22 -0.76  0.79  1.00 -0.13
ddpi   0.30 -0.05  0.03 -0.13  1.00

On peut configurer le graphique, en mettant d'autres choses dans les cases : des histogrammes, des coefficients de corrélation, etc. (néanmoins, ce n'est pas suffisemment configurable à mon goût, car les fonctions que l'on passe en argument à la commande pairs ne connaissent pas le numéro de la ligne et de la colonne courrantes).

panel.hist <- function(x, ...) {
  usr <- par("usr"); 
  on.exit(par(usr))
  par(usr = c(usr[1:2], 0, 1.5) )
  h <- hist(x, plot = FALSE)
  breaks <- h$breaks; nB <- length(breaks)
  y <- h$counts; y <- y/max(y)
  rect(breaks[-nB], 0, breaks[-1], y, ...)
}
# Le coefficient de corrélation
my.panel.smooth <- 
function (x, y, col = par("col"), bg = NA, pch = par("pch"),
          cex = 1, col.smooth = "red", span = 2/3, iter = 3, ...)
{
  points(x, y, pch = pch, col = col, bg = bg, cex = cex)
  ok <- is.finite(x) & is.finite(y)
  if (any(ok))
    lines(lowess(x[ok], y[ok], f = span, iter = iter), col = col.smooth,
          ...)
  usr <- par('usr')
  text( (usr[1]+usr[2])/2, (usr[3]+9*usr[4])/10, 
        floor(100*cor(x,y))/100,
        col='blue', cex=3, adj=c(.5,1) )
}
pairs(LifeCycleSavings,
      diag.panel=panel.hist,
      upper.panel=panel.smooth,
      lower.panel=my.panel.smooth);

*

On peut aussi représenter les coefficients de corrélation par des couleurs. Dans l'exemple suivant, on voit que y est corrélée avec x1, x4 et x5 et que x4 et x5 sont corrélées. (Nous retrouverons ce genre d'exemple quand nous parlerons de régression multiple et de sélection des variables : face à de telles données, si on voulait prédire y en fonction des xi, on se limiterait aux prédictives x1 et x4 ou x1 et x5 (comme x4 et x5 sont fortement corrélées, une seule suffit)).

cor.plot <- function (x) {
  n <- dim(x)[1]
  m <- dim(x)[2]
  N <- 1000
  col = topo.colors(N)
  plot(NA, xlim=c(0,1.2), ylim=c(-1,0))
  for (i in 1:n) {
    for (j in 1:m) {
      polygon( c((j-1)/m, (j-1)/m, j/m, j/m),
               -c((i-1)/m, i/m, i/m, (i-1)/m),
               col = col[ N*(x[i,j]+1)/2 ] )
    }
  }
  for (i in 1:N) {
    polygon( c(1.1, 1.1, 1.2, 1.2),
             -c((i-1)/N, i/N, i/N, (i-1)/N),
             col = col[N-i+1],
             border=NA )
  }
  # Exercice : ajouter une légende et le nom des variables
}

n <- 200
x1 <- rnorm(n)
x2 <- rnorm(n)
x3 <- rnorm(n)
x <- rnorm(n)
x4 <- x + .1*rnorm(n)
x5 <- x + .1*rnorm(n)
y <- 1 + x1 + x4 + rnorm(n)
d <- data.frame(y,x1,x2,x3,x4,x5)
cor.plot(cor(d))

*

dotplot

On peut aussi faire une boite à moustache des valeurs de Y sur chaque quantile de chaque variable Xi (ça n'est pas symétrique : une variable joue une rôle particulier).

my.dotchart(LifeCycleSavings[,1], LifeCycleSavings[,-1],
            xlab='savings', ylab='')

*

Avec des vraies boites à moustaches :

to.factor.vector <- function (x, number=4) {
  resultat <- NULL
  intervalles <- co.intervals(x,number,overlap=0)
  for (i in 1:number) {
    if( i==1 ) intervalles[i,1] = min(x)
    else 
      intervalles[i,1] <- intervalles[i-1,2]
    if( i==number )
      intervalles[i,2] <- max(x)
  }
  for (valeur in x) {
    r <- NA
    for (i in 1:number) {
      if( valeur >= intervalles[i,1] & valeur <= intervalles[i,2] )
        r <- i
    }
    resultat <- append(resultat, r)
  }
  factor(resultat, levels=1:number)
}
to.factor <- function (x, number=4) {
  if(is.vector(x)) 
    r <- to.factor.vector(x, number)
  else {
    r <- NULL
    for (v in x) {
      a <- to.factor.vector(v)
      if( is.null(r) ) 
        r <- data.frame(a)
      else 
        r <- data.frame(r,a)
    }
    names(r) <- names(x)
  }
  r
}
x <- to.factor(LifeCycleSavings[,-1])
y <- LifeCycleSavings[,1]
y <- as.vector(matrix(y,nr=length(y),ncol=dim(x)[2]))
for (i in names(x)) {
  levels(x[[i]]) <- paste(i, levels(x[[i]]))
}
col <- gl( dim(x)[2], length(levels(x[,1])), 
           labels=rainbow( dim(x)[2] ))
col <- as.vector(col)
x <- factor(as.vector(as.matrix(x)))
boxplot(y~x, horizontal=T, las=1, col=col)

*

On doit aussi pouvoir faire ça avec des treillis (je n'ai pas réussi à mettre ça dans une boucle : rien ne s'affichait).

bwplot( ~ LifeCycleSavings[,1] | equal.count(LifeCycleSavings[,2], number=4),
        layout=c(1,4) )

*

bwplot( ~ LifeCycleSavings[,1] | equal.count(LifeCycleSavings[,3], number=4),
        layout=c(1,4) )

*

bwplot( ~ LifeCycleSavings[,1] | equal.count(LifeCycleSavings[,4], number=4),
        layout=c(1,4) )

*

bwplot( ~ LifeCycleSavings[,1] | equal.count(LifeCycleSavings[,5], number=4),
        layout=c(1,4) )

*

Graphiques étoilés (star plots or radar plots)

Si on dispose de beaucoup de variables (une demi-douzaine, quantitatives) et peu d'individus (une dizaine), on peut faire des graphiques étoilés.

data(mtcars)
stars(mtcars[, 1:7], key.loc = c(14, 2),
      main = "Motor Trend Cars : stars(*, full = F)", full = FALSE)

*

stars(mtcars[, 1:7], key.loc = c(14, 1.5),
      main = "Motor Trend Cars : full stars()",flip.labels=FALSE)

*

palette(rainbow(12, s = 0.6, v = 0.75))
stars(mtcars[, 1:7], len = 0.8, key.loc = c(12, 1.5),
      main = "Motor Trend Cars", draw.segments = TRUE)

*

stars(mtcars[, 1:7], locations = c(0,0), radius = FALSE,
      key.loc=c(0,0), main="Motor Trend Cars", lty = 2)

*

3D

R sait tracer des surfaces définie par une équation de la forme z = f(x,y). Elles sont décrites par deux vecteurs -- les valeurs des deux premières coordonnées -- et un tableau -- les valeurs de la dernière coordonnée.

On peut représenter une telle surface sous forme de fil de fer

# Tiré du manuel
x <- seq(-10, 10, length=50)
y <- x
f <- function(x,y) {
  r <- sqrt(x^2+y^2)
  10 * sin(r)/r
}
z <- outer(x, y, f)
z[is.na(z)] <- 1
persp(x, y, z, theta = 30, phi = 30, expand = 0.5, col = "lightblue",
      shade=.5,
      xlab = "X", ylab = "Y", zlab = "Z")

*

# Exemple du manuel
data(volcano)
z <- 2 * volcano        # Exaggerate the relief
x <- 10 * (1:nrow(z))   # 10 meter spacing (S to N)
y <- 10 * (1:ncol(z))   # 10 meter spacing (E to W)
persp(x, y, z, theta = 120, phi = 15, scale = FALSE, axes = FALSE)
# Voir aussi les autres exemples dans 
#   demo(persp)

*

de lignes de niveau

# Toujours un exemple du manuel
data("volcano")
rx <- range(x <- 10*1:nrow(volcano))
ry <- range(y <- 10*1:ncol(volcano))
ry <- ry + c(-1,1) * (diff(rx) - diff(ry))/2
tcol <- terrain.colors(12)
opar <- par(pty = "s", bg = "lightcyan")
plot(x = 0, y = 0,type = "n", xlim = rx, ylim = ry, xlab = "", ylab = "")
u <- par("usr")
rect(u[1], u[3], u[2], u[4], col = tcol[8], border = "red")
contour(x, y, volcano, col = tcol[2], lty = "solid", add = TRUE,
        vfont = c("sans serif", "plain"))
title("A Topographic Map of Maunga Whau", font = 4)
abline(h = 200*0:4, v = 200*0:4, col = "lightgray", lty = 2, lwd = 0.1)
par(opar)

*

avec éventuellement des couleurs.

# Exemple du manuel
data(volcano)
x <- 10*(1:nrow(volcano))
y <- 10*(1:ncol(volcano))
image(x, y, volcano, col = terrain.colors(100), axes = FALSE)
contour(x, y, volcano, levels = seq(90, 200, by=5), add = TRUE, col = "peru")
axis(1, at = seq(100, 800, by = 100))
axis(2, at = seq(100, 600, by = 100))
box()
title(main = "Maunga Whau Volcano", font.main = 4)

*

La bibliothèque lattice contient des fonctions wireframe et cloud.

data(volcano)
x <- 10*(1:nrow(volcano))
x <- rep(x, ncol(volcano))
y <- 10*(1:ncol(volcano))
y <- rep(y, each=nrow(volcano))
z <- as.vector(volcano)
wireframe( z ~ x * y )

*

cloud( z ~ x * y )

*

Pour ceux qui disposent des lunettes adéquates, on peut aussi faire des dessins stéréoscopiques.

data(iris)
print(cloud(Sepal.Length ~ Petal.Length * Petal.Width,
            data = iris, cex = .8,
            groups = Species,
            subpanel = panel.superpose,
            main = "Stereo",
            screen = list(z = 20, x = -70, y = 3)),
      split = c(1,1,2,1), more = TRUE)
print(cloud(Sepal.Length ~ Petal.Length * Petal.Width,
            data = iris, cex = .8,
            groups = Species,
            subpanel = panel.superpose,
            main = "Stereo",
            screen = list(z = 20, x = -70, y = 0)),
      split = c(2,1,2,1))

*

Animations

R lui-même ne sait pas faire d'animations, mais on peut exporter les données pour les observer dans un autre logiciel, par exemple xgobi. (Quand la fenêtre d'xgobi apparait, appuyez sur 'g' pour lancer l'animation.)

library(xgobi) 
n <- 50
x <- seq(-10, 10, length=n)
y <- x
f <- function(x,y) {
  r <- sqrt(x^2+y^2)
  10 * sin(r)/r
}
z <- outer(x, y, f)
z[is.na(z)] <- 1
x <- matrix(x,nr=n,nc=n)
y <- matrix(y,nr=n,nc=n,byrow=T)
sombrero <- data.frame(x=as.vector(x),y=as.vector(y),z=as.vector(z))
xgobi(sombrero)

*

C'est aussi très utile pour observer des données statistiques dans des espaces de dimension plus grande.

http://www.public.iastate.edu/~dicook/JSS/paper/paper.html

A FAIRE : on peut donner différents fichiers à xgobi :
  x.dat  données
  x.row  row names
  x.col  col names
  x.glyphs
  x.colors
mais quand on l'appelle depuis R ???

Quelques tutoriels :

http://industry.ebi.ac.uk/%257Ealan/VisWorkshop99/XGobi_Tutorial/index.html
http://www.public.iastate.edu/~dicook/stat503.html

La nouvelle version de xgobi s'appelle ggobi :

http://www.ggobi.org/

Interaction

Si les données sont assez complexes, on peut demander à xgobi de les afficher en même temps dans plusieurs fenêtres, sélectionner certains points dans l'une d'entre elle et voir où ils sont dans les autres (en anglais, ça s'appelle "brushing").

n <- 1000
x <- runif(3) + rnorm(n)
y <- runif(3) + rnorm(n)
z <- runif(3) + rnorm(n)
t <- c(-5,-5,5) + rnorm(n)
u <- c(-5,5,5) + rnorm(n)
three.clusters <- data.frame(x,y,z,t,u)
xgobi(three.clusters)

*

Xgobi permet aussi de faire des "graphes parallèles" : à chaque point correspond une ligne brisée, dont les ordonnées successives sont les différentes coordonnées du point. Très souvent, on ne voit rien dessus. Mais on peut demander à xgobi de ne tracer que certains des points et le graphe peut devenir lisible.

n <- 100
m <- matrix( rnorm(5*n)+c(1,-1,3,0,2), nr=n, nc=5, byrow=T )
matplot(1:5, t(m), type='l')
title(main="Nuage de points homogène")

*

n <- 50
k <- 2
m <- matrix( rnorm(5*k*n)+runif(5*k,min=-10,max=10), nr=n, nc=5, byrow=T )
matplot(1:5, t(m), type='l')
title(main="Nuage de points avec des (2) grumeaux")

*

n <- 50
k <- 5
m <- matrix( rnorm(5*k*n)+runif(5*k,min=-10,max=10), nr=n, nc=5, byrow=T )
matplot(1:5, t(m), type='l')
title(main="Nuage de points avec des (5) grumeaux")

*

matplot(1:5, t(princomp(m)$scores), type='l')
title(main="idem, après ACP")

*

matplot(1:5, t(m), type='l')
title(main="Nuage de points avec des grumeaux moins visibles")

*

Il y a aussi une fonction parallel dans la bibliothèque lattice.

library(lattice)
?parallel

3D

A FAIRE :

http://wsopuppenkiste.wiso.uni-goettingen.de/~dadler/rgl/

Graphe en coordonnées barycentriques (ternary plot)

On dispose de trois variables quantitatives (très souvent, des pourcentages, en chimie).

library(MASS)
data(Skye)

ternary <- function(X, pch = par("pch"), lcex = 1,
                    add = FALSE, ord = 1:3, ...)
{
  X <- as.matrix(X)
  if(any(X) < 0) stop("X must be non-negative")
  s <- drop(X %*% rep(1, ncol(X)))
  if(any(s<=0)) stop("each row of X must have a positive sum")
  if(max(abs(s-1)) > 1e-6) {
    warning("row(s) of X will be rescaled")
    X <- X / s
  }
  X <- X[, ord]
  s3 <- sqrt(1/3)
  if(!add)
  {
    oldpty <- par("pty")
    on.exit(par(pty=oldpty))
    par(pty="s")
    plot(c(-s3, s3), c(0.5-s3, 0.5+s3), type="n", axes=FALSE,
         xlab="", ylab="")
    polygon(c(0, -s3, s3), c(1, 0, 0), density=0)
    lab <- NULL
    if(!is.null(dn <- dimnames(X))) lab <- dn[[2]]
    if(length(lab) < 3) lab <- as.character(1:3)
    eps <- 0.05 * lcex
    text(c(0, s3+eps*0.7, -s3-eps*0.7),
         c(1+eps, -0.1*eps, -0.1*eps), lab, cex=lcex)
  }
  points((X[,2] - X[,3])*s3, X[,1], ...)
}

ternary(Skye/100, ord=c(1,3,2))

*

Ici, les lignes du tableau ont une somme égale à 100.

> sum( apply(Skye,1,sum) != 100 )
[1] 0

Voici une autre manière de les tracer, d'après http://finzi.psych.upenn.edu/R/Rhelp01/archive/1000.html

tri <-
function(a, f, m, symb = 2, grid = F, ...)
{
  ta <- paste(substitute(a))
  tf <- paste(substitute(f))
  tm <- paste(substitute(m))

  tot <- 100/(a + f +m)
  b <- f * tot
  y <- b * .878
  x <- m * tot + b/2
  par(pty = "s")
  oldcol <- par("col")
  plot(x, y, axes = F, xlab = "", ylab = "", 
       xlim = c(-10, 110), ylim= c(-10, 110), type = "n", ...)
  points(x,y,pch=symb)
  par(col = oldcol)
  trigrid(grid)
  text(-5, -5, ta)
  text(105, -5, tm)
  text(50, 93, tf)
  par(pty = "m")
  invisible()
}

trigrid  <-
function(grid = F)
{
  lines(c(0, 50, 100, 0), c(0, 87.8, 0, 0)) #draw frame
  if(!grid) {
    for(i in 1:4 * 20) {
      lines(c(i, i - 1), c(0, 2 * .878)) #side a-c (base)
      lines(c(i, i + 1), c(0, 2 * .878))
      T.j <- i/2 #side a-b (left)
      lines(c(T.j, T.j + 2), c(i * .878, i * .878))
      lines(c(T.j, T.j + 1), c(i * .878, (i - 2) * .878))
      T.j <- 100 - i/2 #side b-c (right)
      lines(c(T.j, T.j - 2), c(i * .878, i * .878))
      lines(c(T.j, T.j - 1), c(i * .878, (i - 2) * .878))
    }
  } else {
    for(i in 1:4 * 20) {
      # draw dotted grid
      lines(c(i, i/2), c(0, i * .878), lty = 4, col = 3)
      lines(c(i, (50 + i/2)), c(0, .878 * (100 - i)), lty = 4, col = 3)
      lines(c(i/2, (100 - i/2)), c(i * .878, i * .878), lty = 4, col = 3)
    }
    par(lty = 1, col = 1)
  }
}

# some random data in three variables
c1<- runif(5, 10, 20)
c2<- runif(5, 1, 5)
c3 <- runif(5, 15, 25)
# basic plot
tri(c1,c2,c3)

*

# plot with different symbols and a grid
tri(c1,c2,c3, symb=7, grid=T)

*

C'est un bon exercice d'écrire soi-même sa propre fonction. Par exemple, en plus de la grille, seriez-vous capable de remplir le triangle avec un dégradé (à chaque sommet on associe une couleur) ?

Graphe en coordonnées barycentriques

On peut ajouter une dimension, et utiliser un logiciel tel xgobi pour visualiser le résultat.

library(xgobi)
data(PaulKAI)
quadplot(PaulKAI, normalize = TRUE)

*

Son

Il est aussi possible de transformer les données en son (c'est ce que font les dauphins pour se répérer, c'est ainsi que fonctionnent les radars, etc.)...

http://www.matthiasheymann.de/Download/Sonification.pdf
http://www.matthiasheymann.de/Download/sound-manual.pdf

Interlude musical : certains s'amusent à transformer l'ADN en musique...

http://www.bbc.co.uk/radio4/science/thematerialworld_20031120.shtml
http://www.dnamusiccentral.com/#About%20DNA

Données multivariées en partie qualitatives

Une ou deux variables qualitatives et plusieurs variables quantitatives

On peut représenter les différentes valeurs de la variable qualitative par des symboles ou des couleurs différentes.

data(iris)
plot(iris[1:4], pch=21, 
     bg=c("red", "green", "blue")[as.numeric(iris$Species)])

*

fourfoldplot

Si l'une des variables qualitative est binaire, on peut aussi utiliser la commande fourfoldplot, comme vu plus haut.

Plusieurs variables qualitatives : boites à moustaches

Quand on a juste une variable qualitative et une variable quantitative, on a vu qu'on pouvait faire des graphes à moustaches.

Avec deux variables qualitatives et une variable quantitative, ce n'est plus lisible, car les graphes à moustaches se superposent mal.

a <- rnorm(10)
b <- 1+ rnorm(10)
c <- 1+ rnorm(10)
d <- rnorm(10)
x <- c(a,b,c,d)
y <- factor(c( rep("A",20), rep("B",20)))
z <- factor(c( rep("U",10), rep("V",20), rep("U",10) ))
op = par(mfrow=c(2,2))
plot(x~y)
plot(x~z)
plot(x[z=="U"] ~ y[z=="U"], border="red", ylim=c(min(x),max(x)))
plot(x[z=="V"] ~ y[z=="V"], border="blue", add=T)
plot(x[y=="A"] ~ z[y=="A"], border="red", ylim=c(min(x),max(x)))
plot(x[y=="B"] ~ z[y=="B"], border="blue", add=T)
par(op)

*

On peut juste les mettres côte à côte (si les variables n'ont que deux valeurs, comme ici, ça reste lisible).

l <- rep("",length(x))
for (i in 1:length(x)){
  l[i] <- paste(y[i],z[i])
}
l <- factor(l)
boxplot(x~l)

*

Plusieurs variables qualitatives : matplot

Par contre, au lieu de superposer des graphes à moustaches, on peut superposer des courbes correspondant à la moyenne des échantillons.

# l est une liste à deux éléments
myplot1 <- function (x, l, ...) {
  t <- tapply(x,l,mean)
  l1 <- levels(l[[1]])
  l2 <- levels(l[[2]])
  matplot(t,
          type='l', lty=1, col=1:length(l2),
          axes=F, ...)
  axis(1, 1:2, l1)
  axis(2)
  lim <- par("usr")
  legend(lim[1] + .05*(lim[2]-lim[1]), lim[4],
         l2, lwd=1, lty=1, col=1:length(l2) )
}
op <- par(mfrow=c(1,2))
myplot1( x, list(y,z), ylim=c(0,2) )
myplot1( x, list(z,y), ylim=c(0,2) )
par(op)

*

Si le dessin n'est pas trop chargé, on peut tenter de l'enrichir en faisant figurer, en pointillés, les quartiles et les valeurs extrèmes de chaque échantillon.

# l est une liste à deux éléments
myplot3 <- function (x, l, ...) {
  l1 <- levels(l[[1]])
  l2 <- levels(l[[2]])
  t0 <- tapply(x,l,min)
  t1 <- tapply(x,l,function(x)quantile(x,.25))
  t2 <- tapply(x,l,median)
  t3 <- tapply(x,l,function(x)quantile(x,.75))
  t4 <- tapply(x,l,max)
  matplot(cbind(t0,t1,t2,t3,t4),
          type='l', 
          lty=c(rep(3,length(l2)), rep(2,length(l2)), 
                rep(1,length(l2)), rep(2,length(l2)),
                rep(3,length(l2)) ),
          col=1:length(l2),
          axes=F, ...)
  axis(1, 1:2, l1)
  axis(2)
  lim <- par("usr")
  legend(lim[1] + .05*(lim[2]-lim[1]), lim[4],
         l2, lwd=1, lty=1, col=1:length(l2) )
}
op <- par(mfrow=c(1,2))
myplot3( x, list(y,z) )
myplot3( x, list(z,y) )
par(op)

*

Treillis

Treillis (découpage en tranches)

http://cm.bell-labs.com/cm/ms/departments/sia/project/trellis/

Les graphiques en treillis consistent à dessiner un nuage de points en 3 dimensions (ou 4, ou 5), à découper l'espace en tranches, et à projeter ces tranches sur des sous-espaces de dimension 2.

On peut aussi les voirs comme des graphiques dans lesquels on sépare les individus en plusieurs groupes (les tranches ci-dessus ou les différentes valeurs d'une (ou plusieurs) variable(s) qualitative(s)) pour faire un graphique pour chacun d'entre eux. On se retrouve alors aves plusieurs graphiques, et ne pas un seul.

library(help=lattice)
library(lattice)
?Lattice
?xyplot

L'exemple suivant (trois variables quantitatives) représente les épicentres de tremblements de terre : on les a représentés dans un espace de dimension 3, que l'on coupe en tranches.

data(quakes)
Depth <- equal.count(quakes$depth, number=8, overlap=.1)
xyplot(lat ~ long | Depth, data = quakes)

*

C'est beaucoup plus lisible que la projection sur l'un des plans de coordonnées (car on oublie alors l'une des variables), que les trois projections, ou que la projection sur un plan "judicieusement choisi" (nous verrons plus loin comment il est choisi : c'est l'analyse en composantes principales).

plot(lat ~ long, data=quakes)

*

op <- par(mfrow=c(2,2))
plot(lat ~ long, data=quakes)
plot(lat ~ -depth, data=quakes)
plot(-depth ~ long, data=quakes)
par(op)

*

library(mva)
biplot(princomp(quakes[1:3]))

*

pairs( princomp(quakes[1:3])$scores )

*

Un exemple avec une variable quantitative et 3 variables qualitatives, dans lequel, si on fixe les variables quantitatives, il n'y a qu'un seul individu.

data(barley)
barchart(yield ~ variety | year * site, data=barley)

*

L'argument scales permet de modifier l'apparence des axes et de leurs graduations (ici, on évite que les lables se chevauchent).

barchart(yield ~ variety | year * site, data = barley,
         ylab = "Barley Yield (bushels/acre)",
         scales = list(x = list(0, abbreviate = TRUE,
                       minlength = 5)))

*

# Raté...
dotplot(yield ~ variety | year * site, data = barley)

*

dotplot(variety ~ yield | year * site, data = barley)

*

On représente les différentes valeurs d'une des variables qualitatives par des couleurs ou des symboles différents (et on remarque que pour l'une des fermes, le rendement a augmenté au lieu de diminuer : en fait, les données des deux années ont été interverties), à l'aide de l'argument groups.

data(barley)
dotplot(variety ~ yield | site, groups = year,
        data = barley, 
        layout = c(1, 6), aspect = .5, pch = 16,
        col.line = c("grey", "transparent"),
        panel = "panel.superpose",
        panel.groups = "panel.dotplot")

*

Un exemple avec une variable quantitative, des variables qualitatives, mais dans lequel il y a plusieurs individus dans chaque classe.

library(nlme)
data(bdf)
d <- data.frame( iq=bdf$IQ.perf, sex=bdf$sex, den=bdf$denomina )
d <- d[1:100,] 
bwplot( ~ d$iq | d$sex + d$den )

*

histogram( ~ d$iq | d$sex + d$den )

*

densityplot( ~ d$iq | d$sex + d$den )

*

stripplot( ~ d$iq | d$sex + d$den )

Autre exemple : une variable quantitative en fonction d'une autre variable quantitative et d'une variable qualitative. (On transforme la variable quantitative en shingle à l'aide de la commande equal.count.)

d <- data.frame( x=bdf$aritPOST, y=bdf$sex, z=equal.count(bdf$langPRET) )
bwplot( ~ x | y + z, data=d )

*

histogram( ~ x | y + z, data=d )

*

densityplot( ~ x | y + z, data=d )

*

Un exemple avec uniquement des variables qualitatives (on calcule préalablement les effectifs, à l'aide de la commande table).

d <- data.frame( x= (bdf$IQ.perf>11), y=bdf$sex, z=bdf$denomina )
d <- as.data.frame(table(d))
barchart( Freq ~ x | y * z, data=d )

*

Formules

Les formules utilisées par la bibliothèque lattice ont, de prime abord, un air un peu compliqué.

x ~ y              Représente x en fonction de y (un seul dessin)
x ~ y | z          Représente x en fonction de y après avoir découpé
                   les données selon les valeurs de z
x ~ y | z1 * z2    Idem, selon les valeurs de (z1,z2)
x~y|z, groups=t    Idem, mais on utilise un symbole différent (ou une
                   couleur différente) selon les valeurs de t

Les exemples suivants représentent des données univariées dans chacune des cases (sauf le dernier).

~ y
~ y | z
~ y | z1 * z2
~ y | z1 = z2, groups=t

Si la variable utilisée pour découper l'ensemble des observations en morceaux est qualitative, le découpage se fait selon ces valeurs, mais si elle est quantitative, il faut préalablement la convertir en une variable qualitative à l'aide (par exemple) de la fonction equal.count.

Positionnement

A un niveau très élémentaire, la bibliothèque lattice permet de positionner précisément plusieurs graphiques.

n <- 200
x <- rnorm(n)
y <- x^3+rnorm(n)
plot1 <- xyplot(y~x)
plot2 <- bwplot(x)
# Attention : l'ordre est xmin, ymin, xmax, ymax
print(plot1, position=c(0,.2,1,1), more=T)
print(plot2, position=c(0,0,1,.2), more=F)

*

Mais on pouvait déjà faire ça avec par(fig=...)

n <- 200
x <- rnorm(n)
y <- x^4+rnorm(n)
k <- .7
op <- par(mar=c(0,0,0,0))
# Attention : l'ordre est xmin, xmax, ymin, ymax
par(fig=c(0,k,0,k))
plot(y~x)
par(fig=c(0,k,k,1), new=T)
boxplot(x, horizontal=T)  
par(fig=c(k,1,0,k), new=T)
boxplot(y, horizontal=F)  
par(op)

*

A FAIRE : Mettre cet exemple un peu plus haut, quand je parlais de par() ?

Configuration

En fait, les commandes xyplot, dotplot, histogram, densityplot, stripplot se contentent de découper les données en tranches et délèguent le tracé de chacun des sous-graphiques à d'autres fonctions (panel.xyplot, etc.), que l'on peut modifier. Dans ces fonctions, il ne faut pas utiliser les commandes usuelles de tracé (points, lignes, segments, polygon, abline, etc.), mais celles de la bibliothèque grid (lpoints, llines, lsegments, , panel.abline, etc.)

Voici l'un des exemples du manuel.

dotplot(variety ~ yield | site, data = barley, groups = year,
        panel = function(x, y, subscripts, ...) {
            dot.line <- trellis.par.get("dot.line")
            panel.abline(h = y, col = dot.line$col,
                         lty = dot.line$lty)
            panel.superpose(x, y, subscripts, ...)
        },
        key = list(space="right", transparent = TRUE,
                   points=list(pch=trellis.par.get("superpose.symbol")$pch[1:2],
                               col=trellis.par.get("superpose.symbol")$col[1:2]),
                   text=list(c("1932", "1931"))),
        xlab = "Barley Yield (bushels/acre) ",
        aspect=0.5, layout = c(1,6), ylab=NULL)

*

La fonction treillis.par.get est l'équivalent de la fonction par pour les graphiques standard : elle permet d'accéder aux paramètres graphiques. On peut les modifier à l'aide de la commande treillis.par.set.

> str(trellis.par.get())
List of 23
 $ background      :List of 1
  ..$ col: chr "#909090"
 $ add.line        :List of 3
  ..$ col: chr "#000000"
  ..$ lty: num 1
  ..$ lwd: num 1
 $ add.text        :List of 3
  ..$ cex : num 1
  ..$ col : chr "#000000"
  ..$ font: num 1
 $ bar.fill        :List of 1
  ..$ col: chr "#00ffff"
 $ box.dot         :List of 4
  ..$ col : chr "#000000"
  ..$ cex : num 1
  ..$ font: num 1
  ..$ pch : num 16
 $ box.rectangle   :List of 3
  ..$ col: chr "#00ffff"
  ..$ lty: num 1
  ..$ lwd: num 1
 $ box.umbrella    :List of 3
  ..$ col: chr "#00ffff"
  ..$ lty: num 2
  ..$ lwd: num 1
 $ dot.line        :List of 3
  ..$ col: chr "#aaaaaa"
  ..$ lty: num 1
  ..$ lwd: num 1
 $ dot.symbol      :List of 4
  ..$ cex : num 0.8
  ..$ col : chr "#00ffff"
  ..$ font: num 1
  ..$ pch : num 16
 $ plot.line       :List of 3
  ..$ col: chr "#00ffff"
  ..$ lty: num 1
  ..$ lwd: num 1
 $ plot.symbol     :List of 4
  ..$ cex : num 0.8
  ..$ col : chr "#00ffff"
  ..$ font: num 1
  ..$ pch : num 1
 $ reference.line  :List of 3
  ..$ col: chr "#aaaaaa"
  ..$ lty: num 1
  ..$ lwd: num 1
 $ strip.background:List of 1
  ..$ col: chr [1:7] "#ffd18f" "#c8ffc8" "#c6ffff" "#a9e2ff" ...
 $ strip.shingle   :List of 1
  ..$ col: chr [1:7] "#ff7f00" "#00ff00" "#00ffff" "#007eff" ...
 $ superpose.line  :List of 3
  ..$ col: chr [1:7] "#00ffff" "#ff00ff" "#00ff00" "#ff7f00" ...
  ..$ lty: num [1:7] 1 1 1 1 1 1 1
  ..$ lwd: num [1:7] 1 1 1 1 1 1 1
 $ regions         :List of 1
  ..$ col: chr [1:100] "#FF80FF" "#FF82FF" "#FF85FF" "#FF87FF" ...
 $ superpose.symbol:List of 4
  ..$ cex : num [1:7] 0.8 0.8 0.8 0.8 0.8 0.8 0.8
  ..$ col : chr [1:7] "#00ffff" "#ff00ff" "#00ff00" "#ff7f00" ...
  ..$ font: num [1:7] 1 1 1 1 1 1 1
  ..$ pch : chr [1:7] "o" "o" "o" "o" ...
 $ axis.line       :List of 4
  ..$ line: num 0
  ..$ col : chr "#000000"
  ..$ lty : num 1
  ..$ lwd : num 1
 $ box.3d          :List of 3
  ..$ col: chr "#000000"
  ..$ lty: num 1
  ..$ lwd: num 1
 $ par.xlab.text   :List of 3
  ..$ cex : num 1
  ..$ col : chr "#000000"
  ..$ font: num 1
 $ par.ylab.text   :List of 3
  ..$ cex : num 1
  ..$ col : chr "#000000"
  ..$ font: num 1
 $ par.main.text   :List of 3
  ..$ cex : num 1.2
  ..$ col : chr "#000000"
  ..$ font: num 2
 $ par.sub.text    :List of 3
  ..$ cex : num 1
  ..$ col : chr "#000000"
  ..$ font: num 2

La commande show.setings permet de voir à quoi tout cela correspond.

show.settings()

*

La fonction panel.abline trace des droites (ici, on donne un argument h, donc il s'agit de lignes horizontales, mais on pourrait aussi tracer des lignes verticales (argument v), des droites données par leur équation ou par le résultat d'une régression.

La fonction panel.superpose permet d'utiliser différentes couleurs ou différents symboles selon les valeur d'une des variables.

L'argument key définit la légende.

Voici un autre exemple : on superpose histogramme, densité et densité normale.

y <- cars$dist
x <- cars$speed
vitesse <- shingle(x, co.intervals(x, number=6))
histogram(~ x | vitesse, type = "density",
          panel = function(x, ...) {
            ps <- trellis.par.get('plot.symbol')
            nps <- ps
            nps$cex <- 1
            trellis.par.set('plot.symbol', nps)
            panel.histogram(x, ...)
            panel.densityplot(x, col = 'brown', lwd=3) 
            panel.xyplot(x = jitter(x), y = rep(0, length(x)), col='brown' )
            panel.mathdensity(dmath = dnorm,
                              args = list(mean=mean(x),sd=sd(x)),
                              lwd=3, lty=2, col='white')
            trellis.par.set('plot.symbol', ps)
          })

*

Voici une liste des fonctions (de haut niveau) qu'on peut avoir envie d'utiliser.

> apropos("^panel\..*")
 [1] "panel.3dscatter"     "panel.3dscatter.new" "panel.3dwire"
 [4] "panel.abline"        "panel.barchart"      "panel.bwplot"
 [7] "panel.cloud"         "panel.densityplot"   "panel.dotplot"
[10] "panel.fill"          "panel.grid"          "panel.histogram"
[13] "panel.levelplot"     "panel.linejoin"      "panel.lmline"
[16] "panel.loess"         "panel.mathdensity"   "panel.pairs"
[19] "panel.parallel"      "panel.qq"            "panel.qqmath"
[22] "panel.qqmathline"    "panel.splom"         "panel.stripplot"
[25] "panel.superpose"     "panel.superpose.2"   "panel.tmd"
[28] "panel.wireframe"     "panel.xyplot"        "panel.smooth"

> apropos("^prepanel\..*")
 [1] "prepanel.default.bwplot"      "prepanel.default.cloud"
 [3] "prepanel.default.densityplot" "prepanel.default.histogram"
 [5] "prepanel.default.levelplot"   "prepanel.default.parallel"
 [7] "prepanel.default.qq"          "prepanel.default.qqmath"
 [9] "prepanel.default.splom"       "prepanel.default.tmd"
[11] "prepanel.default.xyplot"      "prepanel.lmline"
[13] "prepanel.loess"               "prepanel.qqmathline"

La commande panel.superpose est un remplacement de panel.xyplot s'il y a un argument "groups". Je crois que les autres sont directement compréhensibles.

panel.smooth      trace les points (comme panel.xyplot) et ajoute 
                  une courbe de régression

Banking

Quand on trace des courbes, on constate qu'on parvient mieux à comparer la pente des tangentes (i.e., à dire dans quel graphique la variable augmente le plus vite) si cette pente est voisine de 45 degrés. On qualifie de "banking" le choix des unités permettant de se rapprocher de cette pente.

C'est particulièrement utile pour les séries temporelles : dans la deuxième figure, il saute aux yeux que la décroissance est plus lente. On peut aussi le voir dans la première figure, mais il faut regarder attentivement.

## banking
data(sunspot)
xyplot(sunspot ~ 1:37 ,type = "l",
       scales = list(y = list(log = TRUE)),
       sub = "log scales")

*

xyplot(sunspot ~ 1:37 ,type = "l", aspect="xy",
       scales = list(y = list(log = TRUE)),
       sub = "log scales")

*

Code commenté

Exercice : prendre le code d'une de ces fonctions, par exemple xyplot ou splom et le commenter. (Attention, c'est long!)

A FAIRE

Vincent Zoonekynd
<zoonek@math.jussieu.fr>
latest modification on Wed Oct 13 22:32:38 BST 2004