Freakonometrics

To content | To menu | To search

Tag - splines

Entries feed - Comments feed

Monday, November 12 2012

Qui sort en avance lors des examens?

Tous les profs qui surveillent leur examen se posent la même question: que signifie un départ au bout d'une heure et demi, si l'examen est supposé durer trois heures? Que l'examen était trop facile? trop dur? 

Loin de moi l'idée de blâmer les étudiants qui partent tôt ! Tout d'abord parce que j'ai toujours souvent été dans les premiers à quitter la salle lors d'examens, ou de concours. Et parce que je ne pense pas qu'il y ait un lien évident entre le fait de partir tôt, ou tard, et la note obtenue. 

Mais on peut regarder (et répondre au commentaire de @kl suite à l'examen de la session passée). Car l'examen du cours ACT6420 avait lieu la semaine passée, et il était court. Autrement dit, 95% des étudiants étaient sortis avant que j'annonce la fin de l'épreuve. Et j'ai noté sur toutes les copies l'heure de sortie (pour ceux qui veulent plus d'information sur le protocole de l'expérience, les étudiants ne savaient pas que je notais l'heure).

Il s'agissait d'un examen à choix multiples, ce qui garantit une forme d'impartialité dans la correction (sinon on pourrait objecter que si je note en prenant le paquet de copie tel qu'il est arrivé, les notes pour les élèves partis tôt - corrigés en dernier - et ceux partis à la fin - corrigés en premier - pourraient ne pas être vraiment comparables). Et comme à la dernière session, je demandais aux étudiants de prédire leur nombre de réponses. La question était facultative, mais les élèves pouvaient gagner un point en répondant (à condition de prédire exactement le bon nombre).

Les données sont (partiellement) en ligne ici

> act6420=read.table(
+ "http://freakonometrics.blog.free.fr/public/
data/baseact6420a1.txt",
+ sep=";",header=FALSE,skip=1)
> names(act6420)=c("n","heure","obs","pred")

On peut alors comparer les notes en fonction de l'heure de sortie, ainsi que la note prédites (les notes sont sur 30, ou plutôt les nombres de bonnes réponses), en faisant des régressions linéaires simples,

> summary(lm(obs~heure,data=act6420))
 
Call:
lm(formula = obs ~ heure, data = act6420)
 
Residuals:
Min      1Q  Median      3Q     Max
-6.1122 -1.6326 -0.1876  1.9351  8.9224
 
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept)   3.1519     6.0546   0.521   0.6039
heure         1.3209     0.5391   2.450   0.0162 *
---
Signif. codes:  0***0.001**0.01*0.05 ‘.’ 0.1 ‘ ’ 1
 
Residual standard error: 2.819 on 93 degrees of freedom
Multiple R-squared: 0.06063,	Adjusted R-squared: 0.05053
F-statistic: 6.002 on 1 and 93 DF,  p-value: 0.01616
 
> summary(lm(pred~heure,data=act6420))
 
Call:
lm(formula = pred ~ heure, data = act6420)
 
Residuals:
Min       1Q   Median       3Q      Max
-11.9427  -2.5208   0.0052   3.2404   8.3733
 
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept)   1.1178     8.8373   0.126   0.8996
heure         1.7234     0.7889   2.185   0.0317 *
---
Signif. codes:  0***0.001**0.01*0.05 ‘.’ 0.1 ‘ ’ 1
 
Residual standard error: 3.999 on 85 degrees of freedom
(8 observations deleted due to missingness)
Multiple R-squared: 0.05316,	Adjusted R-squared: 0.04202
F-statistic: 4.773 on 1 and 85 DF,  p-value: 0.03167 

On observe qu'à la fois les bons élèves (ceux avec une bonne note) et les élèves qui se pensent bons (ceux qui ont prédit une note élevée) sortent plutôt tard. Avec dans les deux cas,  une significativité de la variable heure de sortie sur la prédiction de la note. Mais peut-être est-ce plus complexe que ça, et qu'il se cache un effet non-linéaire. On essayer de lisser un peu, avec des splines

> D=data.frame(Y=note,X=heure)
> library(splines)
> rg=lm(Y~bs(X,df=5),data=D)
> newheure=seq(10.2,12.1,by=.01)
> notep=predict(rg,newdata=data.frame(X=
+ newheure),interval="confidence")

On obtient les régressions (non-linéaires) suivantes

ou, si on souhaite rajouter des intervalles de confiance autour de la prédiction faite par nos modèles,

(avec en abscisse l'heure de sortie, l'examen finissant un peu après midi). L'analyse est un peu plus complexe qu'il n'y paraissait, avec quelques bons élèves qui finissent très tôt. Si on souhaite d'ailleurs mieux comprendre le lien entre la note obtenue (ou plutôt, encore une fois le nombre de bonnes réponses données, car aucun bonus n'est pris en compte ici), on peut faire un graphique proche de celui fait à la session passée

Le trait bleu est ici la régression obtenue à la session passée. On retrouve exactement la même chose sur les deux sessions, étonnant, non?

Mais j’arrête là pour l'analyse de ces notes. Pour ceux qui veulent en savoir plus, je mettrais bientôt en ligne une correction, avec des statistiques par réponse. On en profitera pour faire une discussion sur l'utilisation de règles démocratiques, à savoir, la majorité a-t-elle toujours raison? Et pour ceux qui n'auraient pas encore compris, on a fait ici une analyse de corrélations entre les heures de sortie, et les notes, et non pas une analyse de causalité. Donc inutile de se forcer à attendre la fin de l'examen pour sortir la prochaine fois, ça ne fera a pas (a priori) monter la note.

Saturday, March 17 2012

Sondages et prévisions de séries temporelles

Ce soir, proposait sur son blog un billet passionnant sur l'élection présidentielle en France, avec des graphiques superbes, basé sur un lissage temporel des résultats des sondages pour le premier tour de l'élection présidentielle en France (qu'il a fait sous excel, on saluera la performance !). En reprenant les données du site http://www.lemonde.fr/, on peut obtenir la même chose assez simplement (j'ai repris - ou presque - les couleurs utilisées sur le site, François Hollande en rose, Nicolas Sarkozy en bleu, François Bayrou en orange, et Marine Le Pen en noir...). Pour commencer, le code en R pour importer les données et les manipuler est le suivant

sondage=read.table("http://freakonometrics.blog.free.fr/
public/data/sondage1ertour2012.csv",
header=TRUE,sep=";",dec=",")
sondage[36,1]="11/03/12"
sondeur=unique(sondage$SONDEUR)
sondage$date=as.Date(as.character(sondage$DATE),
"%d/%m/%y")

(je fais manuellement une correction de date car je me suis trompé en saisissant les chiffres à la main). A partir de là, on peut reprendre l'idée de faire une régression locale pour trouver une tendance,

CL=brewer.pal(6, "RdBu")
couleur=c(CL[1],CL[6],"orange","grey")
datefin=as.Date("2012/04/12")
k=1
plot(sondage$date,sondage[,k+2],col=couleur[k],
pch=19,cex=.7,xlab="",ylab="",ylim=c(0,40),
xlim=c(min(sondage$date),datefin))
rl=lowess(sondage$date,sondage[,k+2])
lines(rl,lwd=2,col=couleur[k])
for(k in 2:4){
points(sondage$date,sondage[,k+2],
col=couleur[k],pch=19,cex=.7)
rl=lowess(sondage$date,sondage[,k+2])
lines(rl,lwd=2,col=couleur[k])
}

Le code a l'air long mais il faut définir les couleurs, générer une fenêtre graphique, mettre les régressions locales dedans, etc. En tant que telle, la commande qui permet de faire le lissage est juste

rl=lowess(sondage$date,sondage[,k+2])

Bon, on peut d'ailleurs faire toutes sortes de lissages, avec des splines par exemple (ce que j'ai davantage tendance à utiliser),

D=data.frame(date=min(sondage$date)+0:148)
library(splines)
k=1
plot(sondage$date,sondage[,k+2],
col=couleur[k],pch=19,cex=.7,
xlab="",ylab="",ylim=c(0,40),xlim=
c(min(sondage$date),datefin))
rs=lm(sondage[,k+2]~bs(date),data=sondage)
prl=predict(rs,newdata=D)
prlse=sqrt(prl/100*(1-prl/100)/1000)*100
polygon(c(D$date,rev(D$date)),c(prl+2*prlse,
rev(prl-2*prlse)),col=CL[3],border=NA)
lines(D$date,prl,lwd=2,col=CL[1])
points(sondage$date,sondage[,k+2],
col=couleur[k],pch=19,cex=.7,)
for(k in 2:4){
points(sondage$date,sondage[,k+2],
col=couleur[k],pch=19,cex=.7)
}

avec, là encore, la commande suivante pour lisser

rs=lm(sondage[,k+2]~bs(date),data=sondage)

Cette fois le code est un peu plus long, parce que j'ai tracé un intervalle de confiance à 95% (intervalle de confiance classique, en supposant que 1000 personnes ont été interrogées, comme évoqué dans d'anciens billets),

http://freakonometrics.blog.free.fr/public/perso5/adic10.gif

On a ici la courbe suivante

avec l'intervalle de confiance pour François Hollande, mais on peut faire la même chose pour Nicolas Sarkozy,

k=2
plot(sondage$date,sondage[,k+2],
col=couleur[k],pch=19,cex=.7,
xlab="",ylab="",ylim=c(0,40),xlim=
c(min(sondage$date),datefin))
rs=lm(sondage[,k+2]~bs(date),data=sondage)
prl=predict(rs,newdata=D)
prlse=sqrt(prl/100*(1-prl/100)/1000)*100
polygon(c(D$date,rev(D$date)),c(prl+2*prlse,
rev(prl-2*prlse)),col=CL[4],border=NA)
lines(D$date,prl,lwd=2,col=CL[6])
points(sondage$date,sondage[,k+2],col=
couleur[k],pch=19,cex=.7,)
for(k in c(1,3:4)){
points(sondage$date,sondage[,k+2],col=
couleur[k],pch=19,cex=.7)
}

Mais comme auparavant, on peut aussi visualiser les "tendances" des quatre candidats en tête,

Voilà pour le début. En fait, idéalement, on voudrait faire un peu de prévision... Le hic est que les code usuel pour faire de la prévision (avec des ARIMA, du lissage exponentiel ou tout autre modèle classique) nécessitent de travailler avec des séries temporelles, telles qu'elles sont classiquement définies, c'est à dire "régulièrement espacées dans le temps".

Je ne vais pas commencer à réclamer plus de sondages, loin de moins cette idée, mais pour mon modèle, j'avoue que j'aurais préféré avoir plus de points. Beaucoup plus de points. Un sondage par jour aurait été idéal en fait...

Qu'à cela ne tienne, on va simuler des sondages. L'idée est simple: on a une tendance qui nous donne une probabilité jour par jour (c'est exactement ce qui a été calculé pour tracer les courbes), via

D=data.frame(date=min(sondage$date)+0:148)
prl=predict(rs,newdata=D)

On va ensuite utiliser une hypothèse de normalité multivariée sur nos 4 candidats, auquel j'en ai en rajouté un cinquième fictif (mais ça ne servait à rien) en utilisant l'analogue multivariée de l'expression suivante (évoqué dans le premier billet de l'année sur les élections)

http://freakonometrics.blog.free.fr/public/perso5/ic-sondage01.gif

On peut alors générer, jour après jour, des sondages, en simulant des vecteurs Gaussiens. Je vais les générer indépendants les uns des autres, parce que c'est plus simple, et que cela ne me semble pas être une trop grosse hypothèse. Pour générer un ensemble de sondages, on utilise la fonction suivante

library(mnormt)
simulation=function(S){
proba=c(S,100-sum(S))/100
variance= -proba%*%t(proba)
diag(variance)=proba*(1-proba)
variance=variance/1000
return(rmnorm(1,proba[1:4],variance[1:4,1:4]))}
simulsondages=function(M){
Z=rep(NA,ncol(M))
for(i in 1:nrow(M)){
Z=rbind(Z,simulation(M[i,]))
}
return(Z[-1,])}
prediction4=matrix(NA,nrow(D),4)
for(k in 1:4){
rs=lm(sondage[,k+2]~bs(date),data=sondage)
prediction4[,k]=predict(rs,newdata=D)
}

Par exemple, si on la fait tourner une fois, on obtient le graphique suivant

set.seed(1)
S4=simulsondages(prediction4)
S100=100*S4
k=1
plot(D$date,S100[,k],col=couleur[k],
pch=19,cex=.7,
xlab="",ylab="",ylim=c(0,40),xlim=
c(min(sondage$date),datefin))
rs=lm(sondage[,k+2]~bs(date),data=sondage)
lines(D$date,predict(rs,newdata=D),
lwd=2,col=couleur[k])
for(k in 2:4){
points(D$date,S100[,k],col=couleur[k],
pch=19,cex=.7)
rs=lm(sondage[,k+2]~bs(date),data=sondage)
lines(D$date,predict(rs,newdata=D),
lwd=2,col=couleur[k])
}

(les courbes lissées sont celles obtenues sur les vrais sondages). Là on peut être heureux, parce qu'on a des vraies séries temporelles. On peut alors faire de la prévision, par exemple en faisant du lissage exponentiel (optimisé),

library(forecast)
k=1
X=S100[,k]
ETS=ets(X)
F=forecast(ETS,h=60)
fdate=max(D$date)+1:60
k=1
plot(D$date,S100[,k],col=couleur[k],
pch=19,cex=.7,
xlab="",ylab="",ylim=c(0,40),xlim=
c(min(sondage$date),datefin))
rs=lm(sondage[,k+2]~bs(date),data=sondage)
lines(D$date,predict(rs,newdata=D),
lwd=2,col=couleur[k])
for(k in 2:4){
points(D$date,S100[,k],col=couleur[k],
pch=19,cex=.7)
rs=lm(sondage[,k+2]~bs(date),data=sondage)
lines(D$date,predict(rs,newdata=D),lwd=2,col=couleur[k])
}
polygon(c(fdate,rev(fdate)),c(as.numeric(F$lower[,2]),
rev(as.numeric(F$upper[,2]))),col=CL[3],border=NA)
polygon(c(fdate,rev(fdate)),
c(as.numeric(F$lower[,1]),rev(as.numeric(F$upper[,1]))),
col=CL[2],border=NA)
lines(fdate,as.numeric(F$mean),lwd=2,col=CL[1])

Là encore le code peut paraître long, mais c'est surtout la partie associée au graphique qui prend de la place (par soucis purement esthétique, on passe du temps sur les codes graphiques depuis le début). Le code qui modélise la série, et qui la projette, est ici donné par les deux lignes suivantes

ETS=ets(X)
F=forecast(ETS,h=60)

Pour François Hollande, sur la simulation des sondages passés que l'on vient d'effectuer, on obtient la prévision suivante

On peut bien entendu faire une projection similaire pour Nicolas Sarkozy,

k=2
X=S100[,k]
ETS = ets(X)
F=forecast(ETS,h=60)
k=1
plot(D$date,S100[,k],col=couleur[k],pch=19,cex=.7,
xlab="",ylab="",ylim=c(0,40),xlim=
c(min(sondage$date),datefin))
rs=lm(sondage[,k+2]~bs(date),data=sondage)
lines(D$date,predict(rs,newdata=D),
lwd=2,col=couleur[k])
for(k in 2:4){
points(D$date,S100[,k],col=couleur[k],
pch=19,cex=.7)
rs=lm(sondage[,k+2]~bs(date),data=sondage)
lines(D$date,predict(rs,newdata=D),
lwd=2,col=couleur[k])
}
polygon(c(fdate,rev(fdate)),c(as.numeric(F$lower[,2]),
rev(as.numeric(F$upper[,2]))),col=CL[4],border=NA)
polygon(c(fdate,rev(fdate)),
c(as.numeric(F$lower[,1]),rev(as.numeric(F$upper[,1]))),
col=CL[5],border=NA)
lines(fdate,as.numeric(F$mean),lwd=2,col=CL[6])

On notera que je ne me suis pas trop fatigué sur la projection: autant sur la génération de sondages passés on a tenu compte de corrélation (i.e. si un candidat a un score élevé, ça se fera au détriment des autres, d'où la corrélation négative utilisée dans la simulation), autant ici, les projections sont faites de manière complétement indépendantes. En se fatiguant un peu plus, on pourrait se lancer dans un modèle vectoriel gaussien. Ou mieux, on pourrait travailler sur des processus de Dirichlet (surtout que R a un package dédié à ce genre de modèle) parce qu'on travaille depuis le début sur des taux, comme évoqué auparavant. Mais commençons par faire simple pour un premier billet rapide sur ce sujet.

On peut ensuite s'amuser à générer plusieurs jeux de sondages, et de lancer des prévisions dessus,

set.seed(1)
for(sim in 1:20){
Ssim=simulsondages(prediction4)
F1=forecast(ets(100*Ssim[,1]),h=60)
F2=forecast(ets(100*Ssim[,2]),h=60)
lines(fdate,as.numeric(F1$mean),col=CL[1])
lines(fdate,as.numeric(F2$mean),col=CL[6])}

Une fois qu'on a fait tout ça, on a presque fini. On peut regarder au 22 avril qui est en tête (voire, par scénario, calculer la probabilité qu'un des deux candidats soit en tête), c'est à dire au soir du 1er tour

set.seed(1)
VICTOIRE=rep(NA,1000)
for(sim in 1:1000){
Ssim=simulsondages(prediction4)
F1=forecast(ets(100*Ssim[,1]),h=60)
F2=forecast(ets(100*Ssim[,2]),h=60)
VICTOIRE[sim]=(F1$mean[30]>F2$mean[30])}
mean(VICTOIRE)

Et voilà. Ah oui, je n'ai pas laissé le résultat. Tout d'abord parce que je suis un statisticien, pas un prédicateur. Mais aussi parce que si ce genre de pronostic amuse des gens... ils n'ont qu'à se mettre à R pour faire tourner les codes !

Tuesday, January 17 2012

Infidelity and econometrics

On http://www.bakadesuyo.com, there was recently an interesting discussion about infidelity, the key question being "at what ages are men and women most likely to have affairs?" The discussion is based on some graphs, e.g.

The source is a paper by Donald Cox. Based on a sample of 36 men and 22 women 3,432 respondent (NHSLS dataset) . And to be honest, I have been surprised by the shape of the curves. Especially for men... In order to compare, it is possible to use another dataset that can be found in R,

> library(Ecdat)
> data(Fair)
> tail(Fair)
sex age   ym child religious education occupation
596   male  47 15.0   yes         3        16          4
597   male  22  1.5   yes         1        12          2
598 female  32 10.0   yes         2        18          5
599   male  32 10.0   yes         2        17          6
600   male  22  7.0   yes         3        18          6
601 female  32 15.0   yes         3        14          1
rate nbaffairs
596    2         7
597    5         1
598    4         7
599    5         2
600    2         2
601    5         1
with 601 observations (from Fair (1977)). It is possible to run a Poisson regression to describe the number of affairs in the past year. E.g for men
> library(splines)
> regM=glm(nbaffairs~bs(age),family=poisson,
+ data=Fair[Fair$sex=="male",])
> a=seq(20,60)
> N=predict(regM,newdata=data.frame(age=a),type="response")
> plot(a,N,type="l",lwd=2,col="red")

or for women,
> regF=glm(nbaffairs~bs(age),family=poisson,
+ data=Fair[Fair$sex=="female",])
> N=predict(regF,newdata=data.frame(age=a),type="response")
> plot(a,N,type="l",lwd=2,col="red",lty=2)

On that (larger) dataset, we obtain curves that are more intuitive... But maybe the Poisson distribution is not an appropriate model. For instance, having no affairs do not mean that the person did not want to... So perhaps, a more interesting model would be a Poisson model with a zero-inflation, i.e. some people are honest and do not want to have affairs (and appear as 0), while some do want to have some affairs, and the number of affairs is Poisson distributed (and can take the value 0). If we focus on people wo do not want to have affairs, the model (and the prediction) is the following, where we plot the probability of not being interested in having an affair,
> library(pscl)
> regM0=zeroinfl(nbaffairs~bs(age)|bs(age),family=poisson,
+ link="logit",data=Fair[Fair$sex=="male",])
> N0=predict(regM0,newdata=data.frame(age=a),type="zero")
> plot(a,N0,type="l",lwd=2,col="blue")

For those willing to have an affair, here is the parameter of the Poisson distribution of the number of affairs,
> Nc=predict(regM0,newdata=data.frame(age=a),type="count")
> plot(a,Nc,type="l",lwd=2,col="purple")

The same can be done for women, with the probability of no-willing to have an affair,

and to Poisson rate for women willing to have an affair,

If we focus on people willing to have an affair, the curves are the following,

i.e. men below 40 have more interested, but after 40, the probability drops, while women are still more and more likely to be willing to have an affair. On the other hand, young women having affairs might be less, but they usually have much more affairs than men...

Sunday, November 7 2010

Updating meteorological forecasts, part 1

As Mark Twain said "the art of prophecy is very difficult, especially about the future" (well, actually I am not sure Mark Twain was the  first one to say so, but if you're interested by that sentence, you can look here). I have been rather surprised to see how Canadians can be interested in weather, and weather forecasts (see e.g. here for a first study). For instance, on that website (here), we can have forecasts of the temperature (but also rain and wind, which is interesting when you ride a bike) over the next week, almost on an hourly basis (I show here only GFS forecasts (Global Forecast System,here), which is quite standard, see here or there, but other meteorological models are considered on the website)

As a statistician (or maybe more as a bike rider) I was wondering if I should trust those forecasts... and if a mistake today means an error tomorrow, too...
So, to look at this problem, Vincent (alias @Vicnent) helped me (again, since he helped me already to get backups of betting websites during the soccer world cup, in June) to make backups of those tables...
  • A descriptive study
For instance, if we look at forecasts made on the 21st of October (00:00) we have the following for the temperature in °C, for different time horizon,
6 hours latter (21st of October (06:00)), we have the following

On the other hand, consider forecasts made for the 25th of October (11:00) we have

while for 3 hours later (25th of October (14:00)) we have

and again for 3 hours later (25th of October (17:00)) we have

So obviously, 5 days earlier, there has been a bump of optimism, or perhaps simply a typing mistake since we jump from 4 before and after to 14 during 6 hours*. If we can expect to have different predictions for different days (even within the day since it is usually cooler during the night), we can try to see if bumps, or forecast errors can be understood.
Here, it is difficult to observe the scatterplot of temperature,

That graph was based on the following points, with here the scatterplot of prediction made on the 21st of October (00:00)

with a smoothed version below (using standard splines),

or, if we consider forecasts made for the 25th of October (11:00) we have

with again, a smoothed version,

The code is the  following,
donnees=read.table("http://perso.univ-rennes1.fr/
arthur.charpentier/pred-meteo2.txt"
)
x=donnees$DATE1+donnees$HEURE1/24
x[x<15]=x[x<15]+31
y=donnees$DATE2+donnees$HEURE2/24
y[y<15]=y[y<15]+31
z=y-x
h=donnees$TEMPERATURE
D=data.frame(x,y,z,h)
X0=sort(unique(x))
Y0=sort(unique(y))
Z0=sort(unique(z))
matY.X=matrix(NA,length(X0),length(Y0))
library(splines)
for(i in 1:length(X0)){
dd=D[D$x==X0[i],]
ax=dd$y
ay=dd$h
a=data.frame(ax,ay)
ra=lm(ay~bs(ax,df=5),data=ba)
X=Y0[(Y0>min(ax))&(Y0<max(ax))]
iX=which((Y0>min(ax))&(Y0<max(ax)))
Y=predict(ra,newdata=data.frame(ax=X))
matY.X[i,iX]=Y
}
So, let us look at levels of iso-temperature, as a function of the date the prediction is made (the x-axis) and the horizon (the y-axis). First, we we fix the date of the prediction, we get (from the code above)

and then, when we fix the horizon of the prediction

It looks like the small heat wave we experienced at the end of October has been perfectly predicted... And here, it looks like the only pattern we can observe is the standard evolution of temperature: there are no bumps due to forecasting problems..
  • Modeling predicted temperatures
Let us use some additive or multiplicative models, i.e. either
http://perso.univ-rennes1.fr/arthur.charpentier/latex/predictemp01.png
where http://perso.univ-rennes1.fr/arthur.charpentier/latex/predictemp0x.png denotes the date the prediction was made and http://perso.univ-rennes1.fr/arthur.charpentier/latex/predictemp0y.png the date the horizon, or
http://perso.univ-rennes1.fr/arthur.charpentier/latex/predictemp02.png
This was easy with the gam framework... For instance, consider the following code (some regressions mentioned below will be used later on)
library(mgcv)
library(splines)
REG1=gam(h~s(x,y),data=D,family=gaussian)
REG2=lm(h~bs(x)+bs(y),data=D)
REG3=gam(h~s(x)+s(y),data=D,family=gaussian)
REG4=gam(h~s(x)+s(y),data=D,family=gaussian(link="log"))
REG5=gam(h~s(y),data=D,family=gaussian)
D$e=residuals(REG5)
REG6=gam(e~s(x,y),data=D,family=gaussian)
Here is the prediction we had with the additive model
and for the multiplicative model

(colors are different because of the log scaling but the shape is similar).

For the component on http://perso.univ-rennes1.fr/arthur.charpentier/latex/predictemp0x.png (in blue), we have, with the additive model

and for the multiplicative model

On the other hand, for the component on http://perso.univ-rennes1.fr/arthur.charpentier/latex/predictemp0y.png (in red) we have, with the additive model

and for the multiplicative model 

 Note that we observe something which looks like the true temperature (we actually had)

So finally, on average, those predictions are consistent with the true temperature we experienced. But so far, we cannot say anything about prediction errors. If we consider a bivariate spline regression, i.e.
http://perso.univ-rennes1.fr/arthur.charpentier/latex/predictemp04.png
for some smoothed function, we have, again, the same kind of predictions,

It looks like what we've seen before. And because we have those horizontal lines, only the horizon http://perso.univ-rennes1.fr/arthur.charpentier/latex/predictemp0y.png should be considered.... So let us study the following model
http://perso.univ-rennes1.fr/arthur.charpentier/latex/predictemp05.png
where we obtain the following prediction

(which is similar with previous graph) but if we fit a spline on residuals obtained from that regression, i.e.
http://perso.univ-rennes1.fr/arthur.charpentier/latex/predictemp07.png
where
http://perso.univ-rennes1.fr/arthur.charpentier/latex/predictemp06.png
we have the following distribution for http://perso.univ-rennes1.fr/arthur.charpentier/latex/predictemp0h.png

This is where we can see prediction errors. We can observe that meteorologists have been a bit surprised by the heat-wave we observed just after the 25th. But let us wait a few more days to get more data, and perhaps we'll have a look at something that I do care about: prediction of rainfall....
* perhaps I should mention that we do not have backups of tables, but backups of html pages... then I use simple linguistic tools available on R to extract the information where it should be....

Thursday, November 4 2010

Splines: opening the (black) box...

Splines in regression is something which looks like a black box (or maybe like some dishes you get when you travel away from home: it tastes good, but you don't what's inside... even if you might have some clues, you never know for sure*). With splines, it is the same: there are knots, then we consider polynomial interpolations on parts between knots, and we make sure that there is no discontinuity (on the prediction, but on the derivative as well).
That sounds nice, but when you look at the output of the regression... you got figures, but you barely see how to interpret them... So let us have a look at the box, and I mean what is inside that box...
So, consider the following dataset, with the following spline regression,
> library(splines)
> K=c(14,20)
> plot(cars)
> reg=lm(dist~bs(speed,knots=c(K),
+ degree=2),data=cars)
> u=seq(4,25,by=.1)
> B=data.frame(speed=u)
> Y=predict(reg,newdata=B)
> lines(u,Y,lwd=2,col="red")
I.e. we have the following (nice) picture,

But if we look at the output of the regression, we get this
> summary(reg)
Call:
lm(formula = dist ~ bs(speed, knots = c(K), degree = 2), data = cars)
Residuals:
    Min      1Q  Median      3Q     Max
-21.848  -8.702  -1.667   6.508  42.514
Coefficients:
                                     Estimate Std. Error t value Pr(>|t|)   
(Intercept)                             5.991      9.692   0.618 0.539596   
bs(speed, knots = c(K), degree = 2)1    8.087     15.278   0.529 0.599174   
bs(speed, knots = c(K), degree = 2)2   45.540     10.735   4.242 0.000109 ***
bs(speed, knots = c(K), degree = 2)3   49.789     15.704   3.170 0.002738 **
bs(speed, knots = c(K), degree = 2)4   95.550     13.651   7.000 1.02e-08 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1
Residual standard error: 15 on 45 degrees of freedom
Multiple R-squared: 0.6888,     Adjusted R-squared: 0.6611
F-statistic: 24.89 on 4 and 45 DF,  p-value: 6.49e-11
So, what can we do with those numbers ?
First, assume know that we consider only one knot (we have to start somewhere), and we consider a b-spline interpolation of degree 1 (i.e. linear by parts).
> K=c(14)
> reg=lm(dist~bs(speed,knots=c(K),degree=1),data=cars)
> summary(reg)
Call:
lm(formula = dist ~ bs(speed, knots = c(K), degree = 1), data = cars)
Coefficients:
                                     Estimate Std. Error t value Pr(>|t|)   
(Intercept)                             3.290      7.746   0.425   0.6730   
bs(speed, knots = c(K), degree = 1)1   31.483      9.670   3.256   0.0021 **
bs(speed, knots = c(K), degree = 1)2   80.525      9.038   8.910 1.16e-11 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1
Residual standard error: 15.41 on 47 degrees of freedom
Multiple R-squared: 0.657,      Adjusted R-squared: 0.6424
F-statistic: 45.01 on 2 and 47 DF,  p-value: 1.203e-11

Recall that b-splines work like that:given knots http://perso.univ-rennes1.fr/arthur.charpentier/latex/spl01.png (we define splines on the unit interval), then
http://perso.univ-rennes1.fr/arthur.charpentier/latex/spl02.png
for all http://perso.univ-rennes1.fr/arthur.charpentier/latex/spl03.png, while
http://perso.univ-rennes1.fr/arthur.charpentier/latex/spl04.png
for all http://perso.univ-rennes1.fr/arthur.charpentier/latex/spl05.png. The code is something like that
> B=function(x,j,n,K){
+ b=0
+ a1=a2=0
+ if(((K[j+n+1]>K[j+1])&(j+n<=length(K))&(n>0))==TRUE){a2=(K[j+n+1]-x)/
+          (K[j+n+1]-K[j+1])*B(x,j+1,n-1,K) }
+ if(((K[j+n]>K[j])&(n>0))==TRUE){a1=(x-K[j])/
+          (K[j+n]-K[j])*B(x,j,n-1,K)}
+ if(n==0){ b=((x>K[j])&(x<=K[j+1]))*1 }
+ if(n>0){  b=a1+a2}
+ return(b)
+ }
So, for instance, for splines of degree 1, we have
> u=seq(0,1,by=.01)
> plot(u,B(u,1,1,c(0,.4,1,1)),lwd=2,col="red",type="l",ylim=c(0,1))
> lines(u,B(u,2,1,c(0,.4,1,1)),lwd=2,col="blue")
> abline(v=c(0,.4,1),lty=2)

and for splines of degree 2, the basis is
> u=seq(0,1,by=.01)
> plot(u,B(u,1,2,c(0,0,.4,1,1,1)),lwd=2,col="red",type="l",ylim=c(0,1))
> lines(u,B(u,2,2,c(0,0,.4,1,1,1)),lwd=2,col="blue")
> lines(u,B(u,3,2,c(0,0,.4,1,1,1)),lwd=2,col="green")
> abline(v=c(0,.4,1),lty=2)

...etc. Note that I need to duplicate sometimes starting and end points (but it should be possible to fix it in the function).
So, how do we use that, here ? Actually, there are two steps:
  1. we get from http://perso.univ-rennes1.fr/arthur.charpentier/latex/spl06.png to the unit interval (using a simple affine transformation)
  2. we consider http://perso.univ-rennes1.fr/arthur.charpentier/latex/sp07.png
Here, based on the graph above (with the basis function), note that we can use
> u0=seq(0,1,by=.01)
> v=reg$coefficients[2]*u0+reg$coefficients[1]
> x1=seq(min(cars$speed),K,length=length(u0))
> lines(x1,v,col="green",lwd=2)
> u0=seq(0,1,by=.01)
> v=(reg$coefficients[3]-reg$coefficients[2])*u0+ reg$coefficients[1]+reg$coefficients[2]
> x2=seq(K,max(cars$speed),length=length(u0))
> lines(x2,v,col="blue",lwd=2)

which gives us exactly the graph we obtained previously. But we can also consider
> plot(cars)
> reg=lm(dist~bs(speed,knots=c(K),degree=1),data=cars)
> k=(K-min(cars$speed))/(max(cars$speed)-min(cars$speed))
> u0=seq(0,1,by=.01)
> v=reg$coefficients[1]+
+   reg$coefficients[2]*B(u0,1,1,c(0,k,1,1))+
+   reg$coefficients[3]*B(u0,2,1,c(0,k,1,1))
> lines(min(cars$speed)+u0*(max(cars$speed)-min(cars$speed)),
+ v,col="purple",lwd=2)
> abline(v=K,lty=2,col="red")

So, we should be able to try with two knots (but we keep it linear, so far). Hence
> K=c(14,20)
> plot(cars)
> reg=lm(dist~bs(speed,knots=c(K),degree=1),data=cars)
> u=seq(4,25,by=.1)
> B=data.frame(speed=u)
> Y=predict(reg,newdata=B)
> lines(u,Y,lwd=2,col="red")
> abline(v=K,lty=2,col="red")
First, we can plot our basis functions, with two knots,
> u=seq(0,1,by=.01)
> plot(u,B(u,1,1,c(0,.4,.7,1)),lwd=2,col="red",type="l",ylim=c(0,1))
> lines(u,B(u,2,1,c(0,.4,.7,1,1)),lwd=2,col="blue")
> lines(u,B(u,3,1,c(0,.4,.7,1,1)),lwd=2,col="green")
> abline(v=c(0,.4,.7,1),lty=2)

so we can use those functions here, as we did before,
> plot(cars)
> reg=lm(dist~bs(speed,knots=c(K),degree=1),data=cars)
> k=(K-min(cars$speed))/(max(cars$speed)-min(cars$speed))
> u0=seq(0,1,by=.01)
> v=reg$coefficients[1]+
+   reg$coefficients[2]*B(u0,1,1,c(0,k,1,1))+
+   reg$coefficients[3]*B(u0,2,1,c(0,k,1,1))+
+   reg$coefficients[4]*B(u0,3,1,c(0,k,1,1))
> lines(min(cars$speed)+u0*(max(cars$speed)-min(cars$speed)),
+ v,col="red",lwd=2)
> abline(v=K,lty=2,col="red")

Great, it looks promising.... Let us look finally at the case we have two knots, and some quadratic splines. Here, with two knots, the basis is
> u=seq(0,1,by=.01)
> plot(u,B(u,1,2,c(0,0,.4,.7,1,1,1)),lwd=2,col="red",type="l",ylim=c(0,1))
> lines(u,B(u,2,2,c(0,0,.4,.7,1,1,1)),lwd=2,col="blue")
> lines(u,B(u,3,2,c(0,0,.4,.7,1,1,1)),lwd=2,col="green")
> lines(u,B(u,4,2,c(0,0,.4,.7,1,1,1)),lwd=2,col="orange")
> abline(v=c(0,.4,.7,1),lty=2)

so if we just rewrite our previous function, we have
> plot(cars)
> reg=lm(dist~bs(speed,knots=c(K),degree=2),data=cars)
> k=(K-min(cars$speed))/(max(cars$speed)-min(cars$speed))
> u0=seq(0,1,by=.01)
> v=reg$coefficients[1]+
+   reg$coefficients[2]*B(u0,1,2,c(0,0,k,1,1,1))+
+   reg$coefficients[3]*B(u0,2,2,c(0,0,k,1,1,1))+
+   reg$coefficients[4]*B(u0,3,2,c(0,0,k,1,1,1))+
+   reg$coefficients[5]*B(u0,4,2,c(0,0,k,1,1,1))
> lines(min(cars$speed)+u0*(max(cars$speed)-min(cars$speed)),
+ v,col="purple",lwd=2)
> abline(v=K,lty=2,col="red")

And just one final comment: how do I optimally choose my knots ?... A simple idea can be to consider all possible knots, and consider the model which gives us the residuals with the smaller variance,
> vk=seq(.05,.95,by=.05)
> SSR=matrix(NA,length(vk))
> for(i in 1:(length(vk))){
+ k=vk[i]
+ K=min(cars$speed)+k*(max(cars$speed)-min(cars$speed))
+ reg=lm(dist~bs(speed,knots=c(K),degree=2),data=cars)
+ SSR[i]=sum(residuals(reg)^2)
+ }
> plot(vk,SSR,type="b",col="blue")

Here, the best model is obtained when we split 3/4-1/4...

* and I know what I am talking about: I have been living in China for more than a year....

Thursday, October 7 2010

Studying joint effects in a regression

We've seen in the previous post (here)  how important the *-cartesian product to model joint effected in the regression. Consider the case of two explanatory variates, one continuous (http://perso.univ-rennes1.fr/arthur.charpentier/latex/prodcart01.png, the age of the driver) and one qualitative (http://perso.univ-rennes1.fr/arthur.charpentier/latex/prodcart02.png, gasoline versus diesel).

  • The additive model
Assume here that
http://perso.univ-rennes1.fr/arthur.charpentier/latex/prodcart03.png
Then, given http://perso.univ-rennes1.fr/arthur.charpentier/latex/prodcart04.png (the exposure, assumed to be constant) and http://perso.univ-rennes1.fr/arthur.charpentier/latex/prodcart01.png
http://perso.univ-rennes1.fr/arthur.charpentier/latex/prodcart05.png
Thus, there is a multplicative effect of the qualitative variate.
> reg=glm(nbre~bs(ageconducteur)+carburant+offset(exposition),
+     data=sinistres,family="poisson")
> ageD=data.frame(ageconducteur=seq(17,90),carburant="D",exposition=1)
> ageE=data.frame(ageconducteur=seq(17,90),carburant="E",exposition=1)
> yD=predict(reg,newdata=ageD,type="response")
> yE=predict(reg,newdata=ageE,type="response")
> lines(ageD$ageconducteur,yD,col="blue",lwd=2)
> lines(ageE$ageconducteur,yE,col="red",lwd=2)

On the graph below, we can see that the ratio
http://perso.univ-rennes1.fr/arthur.charpentier/latex/prodcart06.png
is constant (and independent of the age http://perso.univ-rennes1.fr/arthur.charpentier/latex/prodcart01.png).
> plot(ageD$ageconducteur,yD/yE)

  • The nonadditive model
In order to take into accound a more complex (non constant) interaction between the two explanatory variates, consider the following product model,
 > reg=glm(nbre~bs(ageconducteur)*carburant+offset(exposition),
+     data=sinistres,family="poisson")
> ageD=data.frame(ageconducteur=seq(17,90),carburant="D",exposition=1)
> ageE=data.frame(ageconducteur=seq(17,90),carburant="E",exposition=1)
> yD=predict(reg,newdata=ageD,type="response")
> yE=predict(reg,newdata=ageE,type="response")
> lines(ageD$ageconducteur,yD,col="blue",lwd=2)
> lines(ageE$ageconducteur,yE,col="red",lwd=2)

Here, the ratio
http://perso.univ-rennes1.fr/arthur.charpentier/latex/prodcart06.png
is not constant any longer,

  • Mixing additive and nonadditive
It is also possible to consider a model in between: we believe that there is no interaction for young people (say), while there is for older ones. Assume that the beak occurs at age 50,
> reg=glm(nbre~bs(ageconducteur*(ageconducteur<50))+
+     bs(ageconducteur*(ageconducteur>=50))*carburant+offset(exposition),
+     data=sinistres,family="poisson")
> ageD=data.frame(ageconducteur=seq(17,90,by=.1),carburant="D",exposition=1)
> ageE=data.frame(ageconducteur=seq(17,90,by=.1),carburant="E",exposition=1)
> yD=predict(reg,newdata=ageD,type="response")
> yE=predict(reg,newdata=ageE,type="response")
> lines(ageD$ageconducteur,yD,col="blue",lwd=2)
> lines(ageE$ageconducteur,yE,col="red",lwd=2)

Here, the ratio
http://perso.univ-rennes1.fr/arthur.charpentier/latex/prodcart06.png
is constant for young people, while it will change for older ones,

Thursday, July 22 2010

Bonus-malus et non déclaration de sinistres

Le mécanisme bonus-malus a de très nombreuses vertus, dont celui de renforcer la solidarité. Mais il semble qu'il incite aussi à ne pas déclarer certains petits sinistres à son assureur. C'est ce que nous allons montrer ici.
 La distribution des personnes ayant un bonus de 50% (ce qui est le niveau le plus bas que l'on puisse atteindre, en théorie) a la forme suivante, en fonction de l'âge du conducteur,
> library(splines) ;  library(pscl)
> reg <- glm(bonus~bs(ageconducteur),base=sinistres,family=binomial)
> base <- data.frame(ageconducteur=seq(18,80))
> y=predict(reg,newdata=base)
> plot(seq(18,80),y)

http://blogperso.univ-rennes1.fr/arthur.charpentier/public/perso3/.BLEUproportion-bonus-50_m.jpg
Comme je le notais déjà ici, une personne qui a un niveau de bonus bas peut être incitée à ne pas déclarer un petit sinistre à son assureur (et de proposer  un arrangement à l'amiable avec la contrepartie).
Cette sur-représentation des 0 dans la base pour les très bas niveaux de bonus peut être prise en compte à l'aide des modèles dits zero-inflated.
Classiquement, on supposait que
dans le cas d'un modèle de Poisson. On va supposer ici que l'assuré peut décider de ne pas déclarer certains sinistres. Autrement dit
et pour  
On peut considérer un modèle logistique pour modéliser cette probabilité de non-déclaration, 
alors que pour le modèle de Poisson
En fait, si l'on suppose que l'impact d'une variable n'est pas linéaire, on peut introduire des splines pour estimer la transformation optimale,
> library(splines) ;  library(pscl)
> reg=zeroinfl(nombre~bs(ageconducteur,df=4) | bs(ageconducteur), data = nombre,
+ dist = "poisson",link="logit",offset=log(exposition))
> age=seq(18,80)
> DT=data.frame(ageconducteur=age,exposition=1)
> Y=predict(reg,newdata=DT,type="zero")
> plot(age,Y)
Ce qui permet de ne faire une prédiction que sur la composante d'inflation zero. Sur une base de données sur laquelle je devrais revenir à la rentrée, on obtient la tendance suivante,
Malheureusement, l'interprétation est plus délicate, car avec une régression binomiale négative, qui autorise plus de variance, on obtient des ordres de grandeur très différents
> reg=zeroinfl(nombre~bs(ageconducteur,df=4) | bs(ageconducteur), data = nombre,
+ dist = "negbin",link="logit",offset=log(exposition))

(on oubliera le comportement de bord pour les âges élevés, peu de personnes âgées appartenant à ce portefeuille, les résultats sont peu robustes à droite).
On retrouve toutefois une fonction croissante (au moins entre 20 et 60 ans), ce qui peut être relié avec la distribution du bonus en fonction de l'âge: plus on est âgé, plus on a de chance d'avoir un très bon bonus, et plus on a de chances de ne pas déclarer un sinistre à son assureur...
D'ailleurs, si on fait la régression directement sur le niveau de bonus, et plus sur l'âge, on a l'impact suivant
> reg=zeroinfl(nombre~bs(bonus,df=4) | bs(bonus), data = nombre,
+ dist = "poisson",link="logit",offset=log(exposition))

Moralité, c'est bien le niveau élevé de bonus qui incite les assurés à ne pas déclarer de sinistres à leurs assureurs (et pas forcément un effet d'âge que l'on pourrait associer à de l'Alzheimer).